Skip to main content

Poindexter CLI Reference

Last Updated: 2026-06-21 The poindexter 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 groupWhat it does
setupFirst-run wizard — generates secrets, tests DB, writes bootstrap
memoryQuery and write the shared pgvector memory store
tasksManage the content pipeline task queue
postsQuery and manage published/draft blog posts
settingsRead and write app_settings (DB-first config)
costsPipeline spending and operational metrics
vercelVercel deployment status via the REST API
premiumManage Poindexter Pro subscription license
scheduleScheduled-publish queue (batch/list/show/shift/clear/at) + publish-approval gate (approve/reject/pending/show-pending)
topicsTopic-decision approval queue (list/show/approve/reject/propose)
gatesHITL pipeline gates — approve/reject/pending/show + list/set toggles
migrateSchema migration runner (status / up / down)
Run poindexter --help for the top-level list and poindexter <group> --help for subcommands.
Deprecated flat aliases (#1652). The former top-level verbs approve / reject / list-pending / show-pending (now under gates), approve-publish / reject-publish / list-pending-publish / show-pending-publish (now under schedule), publish-at (now schedule at), and the singular post group (now posts) 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.
The --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.
Flags:
  • --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.
Flags:
  • --text TEXT or --file PATH — content source (pick one)
  • --writer TEXT — required
  • --source-id TEXT — optional ID to link back
  • --source-table TEXT — defaults to memory
  • --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.
The body is required (from --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 # H1 in 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 in metadata.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 (default draft). awaiting_approval routes it into the approval queue.
  • --media A,B — media to generate at publish time (podcast, video, video_short; short is an alias for video_short). Defaults to default_media_to_generate.
  • --force — bypass the semantic dedup guard and idempotency.
  • --json — machine-readable output.
Two safety rails, shared with the AI path:
  • Semantic dedup — the resolved title is checked against already-published posts and refused if cosine similarity ≥ create_post_dedup_threshold (default 0.75). Re-run with --force to override.
  • Idempotency (#338) — a key derived from body + operator makes an accidental same-file re-upload within cli_post_create_idempotency_window_minutes (default 30) return the existing post instead of a duplicate; an edited body is a new post. --force bypasses it, or set cli_post_create_idempotency_enabled=false to disable.

posts list

posts search <query>

posts get <slug>

posts publish <post_id> / posts archive <post_id> / posts unpublish <post_id>

Reach for 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>

Useful when you need to fix a title without going through the full approval queue again (e.g. remove a first-person pronoun caught late).

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>

Activates a Lemon Squeezy license and unlocks premium content (prompts, additional dashboards, book chapters).

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.
Output columns: 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.
Alias for 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'.
Flags:
  • --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 (default manual); use this to tag queue items by sub-source if you’ve got several injection paths feeding topics propose.
  • --target-length N — target word count for the eventual draft (default 1500).
  • --style / --tone — pipeline parameters (defaults technical / professional).
  • --json — machine-readable output.
Refuses to propose past 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 (if telegram_ops is configured in webhook_endpoints) with the artifact summary. The reply path mirrors the CLI:
  • /approve_topic <task_id> — equivalent to topics approve.
  • /reject_topic <task_id> [reason] — equivalent to topics reject.
The Telegram bot is OpenClaw, which calls into the same 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.)
Flags:
  • --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.
Enable the gate like any other: poindexter gates set final_publish_approval on.

schedule batch

Bulk-assign slots to the approved queue.
Flags:
  • --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, or next monday 14:00.
  • --quiet-hours HH:MM-HH:MM — slots inside the window are skipped to the next allowed time. Falls back to the publish_quiet_hours app_setting when omitted.
  • --ordered-byapproved_at (default) | created_at | id | title.
  • --force — re-schedule posts that already have a slot.
  • --json — machine-readable output.
If no posts are eligible the command exits non-zero with an informative message — no silent no-op.

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 hours
Slots that would land between 22:00 and 07:00 jump forward to the next 07:00; subsequent slots step from there. So if the 11th slot would be 02:00, it becomes 07:00 the next morning, and slots 12, 13… continue at 07:30, 08:00, … 2. Show what’s scheduled in the next 7 days
For a quick human-readable view, plain poindexter schedule list prints publish-time-ordered rows. 3. Push the entire queue back by 1 hour
Every post still in the future has its 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.
Sample output:
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.
A migration’s rollback path is whichever of these the migration module exposes (in priority order):
  • async def down(pool) — pool-based, the modern convention
  • async def rollback_migration(conn) — connection-based, the older convention
If a migration defines neither, 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:
VariablePurpose
DATABASE_URLPostgreSQL DSN. Overrides bootstrap.toml if set.
POINDEXTER_API_URLBase URL for the worker API (default http://localhost:8002).
POINDEXTER_TOKENBearer token for the worker API. Overrides bootstrap.toml if set.
Everything else comes from ~/.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:
Pretty-printed tables are the default for humans. JSON is for scripts.