Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`positronick research`**: the agent-facing "what's new" feed over positronick.com's published blog posts, mirrored GitHub releases, and mirrored news links — newest first, so agents avoid stale knowledge. Filter with `--kind` (article/release/link), `--category`, `--tag`, or a free-text query, and poll just the delta with `--since <iso>`; the printed `latest` timestamp is the value to pass back next time. Read-only and unauthenticated, like the soul/listing reads. Backed by the public `GET /api/research` endpoint.
- **`positronick blog`**: read the positronick.com blog from the terminal — `blog list` (newest first, optional `--kind` article/release/link) and `blog show <slug>`, with `--raw` printing the markdown body verbatim and did-you-mean hints (exit 3) on a missing slug. Read-only and unauthenticated, mirroring the soul/listing reads. Backed by the public `GET /api/blog`, `/api/blog/{slug}`, and `/api/blog/{slug}.md` endpoints.
- **`positronick feed` (admin)**: manage the blog feed sources (GitHub release / RSS mirroring) the ingestor polls — `feed list`, `feed create --label <l> --feed-url <u> --kind github_release|rss --category <c>` (`--author`/`--listing` attribution, repeatable `--tag`, `--auto-publish`, `--enabled`), `feed update <id>` (`--enabled=false` pauses a feed — there is no delete verb), and `feed sync <id>` (ingest one feed now, surfacing the fetch summary; a fetch/parse failure maps the API's 502 to a clear error). Backed by the `/api/admin/feeds` API. Ingesting every feed on a schedule stays the cron's job — no CLI subcommand carries that admin key.
- **`positronick post` (admin)**: create, update and list blog posts — `post create --file <md>` (YAML frontmatter + markdown body, field flags override), `post update <id-or-slug>` and `post list`. New posts default to **draft** — an agent post never self-publishes — and there is no delete verb (unpublish with `--status draft`). Editing a feed-mirrored post takes `api` ownership so the ingestor stops refreshing it (`--source feed` hands it back). Hidden and revealed only for a cached admin; the server is the authorization authority (401/403 → exit 4). Backed by the new `POST`/`GET`/`PATCH /api/admin/posts` API.
- **API type sync**: the wire types now mirror the latest `src/lib/types.ts` field-for-field, so `--json` no longer silently drops fields the server sends. Souls and listings gain `chargeCount` (the "energy boost" count); listings also gain `profileTier` (the author's seal), `hasAsset`/`assetVersion`/`assetContentHash` (hosted SKILL.md asset metadata — lets a client skip identical re-downloads), and `LoopData`/`SkillData` gain `bundles`. Human detail views surface `CHARGES`, a skill's `ASSET VERSION`, and loop/skill `BUNDLES`.

## [0.1.2] - 2026-06-16
Expand Down
68 changes: 68 additions & 0 deletions internal/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ type AdminProfile struct {
Source string `json:"source"`
}

// AdminPost is a blog post as the admin API returns it: the full public Post
// (markdown body included) plus Source, the ownership marker. For posts the
// values are "feed" (mirrored from a feed source by the ingestor, which keeps
// it fresh) and "api" (authored or edited through the write API, which the
// ingestor leaves alone). Editing a feed-owned post flips it to api, reported
// as tookOwnership.
type AdminPost struct {
Post
Source string `json:"source"`
}

// CreateSoul creates a soul: POST /api/admin/souls. fields is sent verbatim
// as the JSON body — the server validates with the seed's own validator and
// answers 422 with the validator message, 409 on a slug conflict. The id is
Expand Down Expand Up @@ -231,3 +242,60 @@ func (c *Client) SyncFeed(ctx context.Context, id string) (*FeedSyncSummary, err
}
return nil, err
}

// CreatePost creates a blog post: POST /api/admin/posts. fields is sent
// verbatim as the JSON body — the server validates it and answers 422 with the
// validator message, 409 on a slug conflict. The id is server-assigned (the
// server rejects a client-supplied one) and status defaults to "draft": an
// agent-authored post cannot self-publish, it must be promoted deliberately.
func (c *Client) CreatePost(ctx context.Context, fields map[string]any) (*AdminPost, error) {
var out struct {
Post AdminPost `json:"post"`
}
if err := c.do(ctx, http.MethodPost, "/api/admin/posts", nil, fields, &out); err != nil {
return nil, err
}
return &out.Post, nil
}

// AdminPost fetches one post by id, any status: GET /api/admin/posts/{id}.
func (c *Client) AdminPost(ctx context.Context, id string) (*AdminPost, error) {
var out struct {
Post AdminPost `json:"post"`
}
path := "/api/admin/posts/" + url.PathEscape(id)
if err := c.do(ctx, http.MethodGet, path, nil, nil, &out); err != nil {
return nil, err
}
return &out.Post, nil
}

// AdminPosts lists every post (any status, source included): GET
// /api/admin/posts. Admin only — the server answers 401/403 for non-admins.
// Named AdminPosts (not Posts) to leave the public, published-only blog gallery
// reader Posts(ctx, kind) untouched.
func (c *Client) AdminPosts(ctx context.Context) ([]AdminPost, error) {
var out struct {
Posts []AdminPost `json:"posts"`
}
if err := c.do(ctx, http.MethodGet, "/api/admin/posts", nil, nil, &out); err != nil {
return nil, err
}
return out.Posts, nil
}

// UpdatePost patches a post: PATCH /api/admin/posts/{id}. patch carries only
// the fields to change — {"status":"draft"} unpublishes (there is no delete
// verb). tookOwnership is true when the row was feed-owned and this edit flipped
// it to api-owned: the ingestor will no longer refresh it from its source.
func (c *Client) UpdatePost(ctx context.Context, id string, patch map[string]any) (*AdminPost, bool, error) {
var out struct {
Post AdminPost `json:"post"`
TookOwnership bool `json:"tookOwnership"`
}
path := "/api/admin/posts/" + url.PathEscape(id)
if err := c.do(ctx, http.MethodPatch, path, nil, patch, &out); err != nil {
return nil, false, err
}
return &out.Post, out.TookOwnership, nil
}
1 change: 1 addition & 0 deletions internal/cli/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func registerAdminCommands(root *cobra.Command) {
root.AddCommand(newListingCmd())
root.AddCommand(newProfileCmd())
root.AddCommand(newFeedCmd())
root.AddCommand(newPostCmd())
revealAdminCommands(root)
}

Expand Down
5 changes: 5 additions & 0 deletions internal/cli/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ var adminCommandPaths = [][]string{
{"feed", "create"},
{"feed", "update"},
{"feed", "sync"},
{"post"},
{"post", "create"},
{"post", "update"},
{"post", "list"},
}

// The admin commands are hidden by default and flip visible from the CACHED
Expand Down Expand Up @@ -253,6 +257,7 @@ func TestRevealAdminCommands(t *testing.T) {
"positronick profile create", "positronick profile list",
"positronick feed list", "positronick feed create",
"positronick feed update", "positronick feed sync",
"positronick post create", "positronick post update", "positronick post list",
} {
if !strings.Contains(stdout, "## "+want) {
t.Errorf("agent-docs for an admin must document %q", want)
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/agentdocs.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ const agentDocsIntro = "`positronick` is the command-line client for positronick
"driven by coding agents — pass `--json` to any command for stable machine-readable JSON on " +
"stdout, read progress and errors from stderr, and branch on the exit code. Read commands " +
"never prompt. Hidden admin commands (create/update for souls and listings, create/list for " +
"profiles, and list/create/update/sync for blog feed sources) exist and appear in help and " +
"in these docs after logging in with an admin account."
"profiles, list/create/update/sync for blog feed sources, and create/update/list for blog " +
"posts) exist and appear in help and in these docs after logging in with an admin account."

func newAgentDocsCmd() *cobra.Command {
return &cobra.Command{
Expand Down
Loading