Poindexter CLI Reference
Last Updated: 2026-06-21 Thepoindexter command is installed as a console script when you
pip install -e src/cofounder_agent. It’s the primary operator
interface for everything you’d otherwise do by hand with psql or
curl.
All commands take --help for inline documentation. This page is the
consolidated reference.
Quick reference
| Command group | What it does |
|---|---|
setup | First-run wizard — generates secrets, tests DB, writes bootstrap |
memory | Query and write the shared pgvector memory store |
tasks | Manage the content pipeline task queue |
posts | Query and manage published/draft blog posts |
settings | Read and write app_settings (DB-first config) |
costs | Pipeline spending and operational metrics |
vercel | Vercel deployment status via the REST API |
premium | Manage Poindexter Pro subscription license |
schedule | Scheduled-publish queue (batch/list/show/shift/clear/at) + publish-approval gate (approve/reject/pending/show-pending) |
topics | Topic-decision approval queue (list/show/approve/reject/propose) |
gates | HITL pipeline gates — approve/reject/pending/show + list/set toggles |
migrate | Schema migration runner (status / up / down) |
poindexter --help for the top-level list and
poindexter <group> --help for subcommands.
Deprecated flat aliases (#1652). The former top-level verbsapprove/reject/list-pending/show-pending(now undergates),approve-publish/reject-publish/list-pending-publish/show-pending-publish(now underschedule),publish-at(nowschedule at), and the singularpostgroup (nowposts) still work as hidden, deprecated aliases — each prints a one-line deprecation notice to stderr and delegates to its grouped command. Update scripts to the grouped form; the aliases will be removed in a later cleanup.
setup
First-run wizard. Generates secrets, tests DB connectivity, and writes
~/.poindexter/bootstrap.toml.
--auto mode spins up local Postgres automatically if Docker is
available and no --db-url is provided. Use --check in CI to
validate that an operator’s config is well-formed.
memory
Semantic memory store (pgvector). Shared spine for all agents.
memory search <query>
Semantic search across the memory store. Top-k cosine similarity.
--writer TEXT— filter by writer (e.g.brain,claude,operator)--source-table TEXT— filter by source (brain,audit,posts,memory,claude_sessions,issues)--min-similarity FLOAT— default 0.5--limit INT— default 5--json-output— machine-readable JSON
memory status
Aggregate counts per source_table and per writer.
memory store
Store a memory note.
--text TEXTor--file PATH— content source (pick one)--writer TEXT— required--source-id TEXT— optional ID to link back--source-table TEXT— defaults tomemory--tags TEXT— comma-separated
memory embed <text>
Print the raw 768-dim embedding vector for debugging.
memory backfill-posts
Re-embed all published posts into pgvector (source_table=‘posts’).
tasks
Content pipeline task queue.
tasks list
tasks get <task_id>
tasks create <topic>
Queue a new content task.
tasks approve <task_id> / tasks reject <task_id>
--feedback is required on reject — the worker API enforces it.
tasks publish <task_id>
Manually publish an approved task (normally handled automatically on
approval, but this is the manual override).
posts
Published/draft blog post management.
posts create
Upload a finished markdown post you wrote by hand — the manual counterpart
to the AI pipeline (the create_post MCP tool / POST /api/tasks, which
generates a post from a topic). You supply the body; a complete posts row is
inserted directly and no writer LLM runs.
--from-file PATH or stdin); an empty body fails
loud. Flags:
--from-file PATH— markdown file to upload. Omit to read the body from stdin.--title TEXT— post title. Omit to derive it from the first markdown# H1in the body; if neither is present the command fails loud.--slug TEXT— URL slug. Omit to derive from the slugified title plus a short random suffix.--niche SLUG— stored inmetadata.niche_slug(e.g.ai_ml,gaming,pc_hardware). Omitting it warns — niche-gated publishing skips a post with no niche.--excerpt TEXT— optional excerpt / SEO description.--status {draft,awaiting_approval}— initial status (defaultdraft).awaiting_approvalroutes it into the approval queue.--media A,B— media to generate at publish time (podcast,video,video_short;shortis an alias forvideo_short). Defaults todefault_media_to_generate.--force— bypass the semantic dedup guard and idempotency.--json— machine-readable output.
- Semantic dedup — the resolved title is checked against already-published
posts and refused if cosine similarity ≥
create_post_dedup_threshold(default0.75). Re-run with--forceto override. - Idempotency (#338) — a key derived from
body + operatormakes an accidental same-file re-upload withincli_post_create_idempotency_window_minutes(default 30) return the existing post instead of a duplicate; an edited body is a new post.--forcebypasses it, or setcli_post_create_idempotency_enabled=falseto disable.
posts list
posts search <query>
posts get <slug>
posts publish <post_id> / posts archive <post_id> / posts unpublish <post_id>
unpublish to roll back a wrong/bad publish right away — it reverts
the post to draft and pulls its JSON from storage + revalidates, so the
page stops being served immediately (unlike archive/the PATCH status flip,
which only changes the column and waits for the next rebuild to drop the post).
Idempotent: a post that isn’t currently published reports No change. Also
exposed as POST /api/posts/{post_id}/unpublish.
posts retitle <post_id> <title>
settings
Read and write app_settings — the DB-first config plane.
settings list
settings get <key>
--reveal decrypts an is_secret row and prints the bare plaintext to
stdout (a warning goes to stderr), so it’s safe to capture in a script —
e.g. a webhook HMAC test-fire. Without --reveal, encrypted values stay
redacted as ******* (encrypted).
settings set <key> <value>
Upsert — creates if missing (with --allow-new), updates if present.
--secret stores the value encrypted at rest (pgcrypto) and flags the
row is_secret=true, routing through the same path as the per-surface
<surface> set-secret commands. The plaintext is never echoed back, the
category defaults to secrets (override with --category), and
--allow-new is implied (operator credentials aren’t phantom-key-guarded).
Read one back with settings get <key> --reveal.
settings disable <key> / settings enable <key>
Soft-delete a setting (is_active=false) while preserving the value.
costs
Spend and operational metrics.
costs budget
Month-to-date spend vs. the configured monthly budget.
costs operational
Task counts, worker state, websocket connections.
vercel
Vercel deployment status via the Vercel REST API. Requires
vercel_token in app_settings.
premium
Manage your Poindexter Pro subscription license.
premium activate <license_key>
premium deactivate
Frees your activation slot (useful when moving to a new machine).
premium status
Shows the current license state, expiration, and activation count.
topics
Operator interface for the topic-decision approval queue
(Glad-Labs/poindexter#146). When
pipeline_gate_topic_decision = on, anticipation_engine output and
manual proposals both land in the same queue rather than auto-running.
Drain the queue at your own pace.
The gate is opt-in. Enable it with:
poindexter gates list shows two groups: the DEFAULT PUBLISH GATE
— the always-on awaiting_approval per-post sign-off that actually holds
every post, with its live pending count and the global auto-publish posture
(auto_publish_threshold, require_human_approval, and any armed niches) —
and the CONFIGURABLE GATES table, each toggleable gate with its STATE
(enabled/disabled), WIRED INTO (where it fires, e.g. canonical_blog,
seo_refresh, scheduled_publisher), and PENDING count. --json emits the
full rows (a superset of the table, including each gate’s mechanism:
graph-node / imperative-hold / default).
With topic_decision off, manual topics propose calls land at
status='pending' and the worker runs them end-to-end exactly like
auto-discovered topics — the gate is purely additive.
topics list [--source NAME] [--json]
Show every topic currently paused at topic_decision. Oldest-first so
you work the queue chronologically.
TASK_ID (first 8 chars of UUID), AGE (relative —
5m, 2h, 1d), SOURCE, TOPIC. Empty queues exit zero with a
friendly message — that’s a normal state, not an error.
topics show <task_id> [--json]
Pretty-print the full artifact for one queued topic. The artifact
shape:
topics approve <task_id> [--feedback TEXT] [--json]
Approve a queued topic — flips it to pending and resumes the
pipeline.
poindexter gates approve <task_id> --gate topic_decision with
the gate name asserted explicitly so a misrouted task fails loudly.
topics reject <task_id> [--reason TEXT] [--json]
Reject a queued topic — flips it to dismissed (the topic-decision
gate’s reject status) and ends the task.
topics propose --topic "..." [flags]
Manually inject a topic into the queue. Lands the same way an
anticipation_engine auto-proposal would — at
awaiting_gate='topic_decision' when the gate is on, otherwise at
status='pending'.
--topic TEXT(required) — non-empty topic string.--keyword TEXT— primary SEO keyword. Falls back to the first tag.--tags A,B,C— comma-separated tag list.--category SLUG— category hint (hardware,ai-ml,gaming).--source LABEL— origin label recorded on the artifact (defaultmanual); use this to tag queue items by sub-source if you’ve got several injection paths feedingtopics propose.--target-length N— target word count for the eventual draft (default1500).--style/--tone— pipeline parameters (defaultstechnical/professional).--json— machine-readable output.
topic_discovery_max_pending (default 50)
when the gate is enabled — drain pending topics first. Empty topic
text exits non-zero rather than silently inserting a junk row.
Worked example — drain the queue
Telegram-side flow
When a topic lands in the queue, the operator gets a Telegram message (iftelegram_ops is configured in webhook_endpoints) with the
artifact summary. The reply path mirrors the CLI:
/approve_topic <task_id>— equivalent totopics approve./reject_topic <task_id> [reason]— equivalent totopics reject.
mcp-server-gladlabs topics_approve / topics_reject tools the
CLI uses. Single source of truth: services.approval_service /
services.topic_proposal_service.
schedule
Operator interface for the scheduled-publishing queue
(Glad-Labs/poindexter#147). Poindexter already has a
background loop (services/scheduled_publisher.py) that publishes
posts whose posts.published_at slot has arrived and whose
status='scheduled'. These commands populate, inspect, and rewrite
that queue. The group also hosts the publish-approval gate verbs
(approve / reject / pending / show-pending) for the post-scheduling
HITL gate.
A post becomes eligible for batch scheduling once it has been
approved (status IN ('approved', 'awaiting_approval')) and has no
publish slot yet (published_at IS NULL). The batch command pulls
the oldest-approved-first by default; pass --ordered-by to change
the source ordering.
schedule at <post_id> <when>
Schedule a single post. (Formerly the top-level publish-at, which
still works as a hidden deprecated alias.)
--in DUR— relative scheduling (30m,2h,1d,1h30m).--force— overwrite an existing schedule.--json— machine-readable output.
schedule approve / reject / pending / show-pending
Operate the final_publish_approval gate on the posts table — the operator’s
last veto before a scheduled post is auto-promoted to published. When the
gate is enabled, scheduled_publisher pauses each due scheduled post here
instead of publishing it (writing posts.awaiting_gate), notifies the operator,
and waits; approve clears the gate and the next publisher tick (≤60s) promotes
it. This gate only fires on the scheduled→published timer path — the manual
approve→go-live flow is already a human action and is unaffected. Formerly the
top-level approve-publish / reject-publish / list-pending-publish /
show-pending-publish, which still work as hidden deprecated aliases.
poindexter gates set final_publish_approval on.
schedule batch
Bulk-assign slots to the approved queue.
--count N(required) — how many posts to schedule.--interval DUR(required) — slot spacing (30m,1h,1h30m,1d).--start TIME(required) — first slot. ISO 8601,now,tomorrow 9am, ornext monday 14:00.--quiet-hours HH:MM-HH:MM— slots inside the window are skipped to the next allowed time. Falls back to thepublish_quiet_hoursapp_setting when omitted.--ordered-by—approved_at(default) |created_at|id|title.--force— re-schedule posts that already have a slot.--json— machine-readable output.
schedule list [--all] [--json]
Print the upcoming queue in publish-time order. Pass --all to
include past schedules.
schedule show <post_id> [--json]
Schedule + status detail for a single post.
schedule shift <post_id> --by DUR / schedule shift --all --by DUR
Push one schedule (or every still-future scheduled post with --all)
back by DUR. Past slots are intentionally untouched — shifting an
already-published post does not un-publish it.
schedule clear <post_id> / schedule clear --all
Drop the schedule on a single post (or every still-future scheduled
post). Clearing rolls the row back to status='approved' so it can
be re-scheduled.
Worked examples
1. Schedule 20 approved posts to publish every 30m starting tomorrow at 9am, skipping 22:00-07:00 quiet hourspoindexter schedule list
prints publish-time-ordered rows.
3. Push the entire queue back by 1 hour
published_at advanced by
one hour. The audit trail records the operation as a single
schedule.shifted event with the post-id list in the payload.
migrate
Schema migration runner (Glad-Labs/poindexter#226). The
worker auto-runs migrations on boot, but operators who don’t restart
their container can have unapplied migrations sitting on disk. This
group exposes the same runner as a direct CLI surface — no worker
round-trip, no container restart.
The CLI operates on the same services/migrations/ directory and the
schema_migrations tracking table the worker uses, so it’s safe to
mix and match (CLI migrate up then a worker reboot, or vice versa).
migrate status [--json]
List every migration file on disk alongside its applied state.
0000_baseline.py is the only legacy 4-digit file remaining — it
captures the squashed history (collapsed into one baseline, re-rolled
most recently by the Phase F squash on 2026-06-22). Every NEW migration uses a UTC timestamp
prefix YYYYMMDD_HHMMSS_<slug>.py per Glad-Labs/poindexter#378
(2026-05-05). See migrations.md for the naming
convention.
migrate up [--to NAME] [--json]
Apply all pending migrations. Idempotent — already-applied migrations
are skipped via schema_migrations.
--to accepts either a timestamp prefix (YYYYMMDD_HHMMSS) or a full
filename. The summary line at the end reads
applied N, skipped M, failed K, pending P.
migrate down [--to NAME] [--all] [--yes] [--json]
Roll back applied migrations. Migrations are rolled back in reverse
order. Without flags, only the most recent applied migration is
rolled back.
async def down(pool)— pool-based, the modern conventionasync def rollback_migration(conn)— connection-based, the older convention
migrate down skips it and exits
non-zero with a no down()/rollback_migration() defined — skipped
notice. The schema_migrations row is left intact in that case so
nothing silently disappears from the tracker.
--all and --to are mutually exclusive. --all prompts for
confirmation unless --yes is set or --json is set (JSON mode
suppresses the interactive prompt).
Environment variables
The CLI respects a small set of env vars for non-interactive use:| Variable | Purpose |
|---|---|
DATABASE_URL | PostgreSQL DSN. Overrides bootstrap.toml if set. |
POINDEXTER_API_URL | Base URL for the worker API (default http://localhost:8002). |
POINDEXTER_TOKEN | Bearer token for the worker API. Overrides bootstrap.toml if set. |
~/.poindexter/bootstrap.toml and the
app_settings DB table.
JSON output mode
Every list/search/get subcommand supports--json-output. Use it for
piping into jq or building shell automation: