Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

CLI Tags Control (manage the tag library from external AI)

The eight local HTTP routes that let an outside AI agent or script list, propose, edit, and soft-delete your tag library, and apply or remove tags on individual sessions — including which routes need your approval in the inbox and which apply immediately.

What it is

Omniscio's tag library — the curated colored labels you attach to sessions for triage (bug, urgent, client-acme, followup) — is reachable from outside Omniscio over the same 127.0.0.1:19519 localhost HTTP server that already exposes cron jobs, automations, recipes, and settings. An external AI agent (Claude Code in another project, ChatGPT with the omniscio-control skill, a curl script) holding the Omniscio bearer token can list every library tag, propose new ones, edit existing ones, soft-delete obsolete ones, and apply or unapply tags to specific sessions — all from a shell.

The library itself is the curated tag system described in tag-manager.md: tags rows with name / color / description / icon / scope (global or specific projects), applied to sessions via the session_tags join. This page covers the eight CLI routes that expose that system over HTTP. The free-form per-session tag strings stored on the sessions.tags JSON column (described in session-tags.md) are NOT reachable through CLI in v1 — only library tags.

The gating model is split across the eight routes by blast radius:

  • Library mutations are approval-gated. POST /tags (create), PATCH /tags/:id (update), and DELETE /tags/:id (soft-delete with cascade) all enqueue tags.create / tags.update / tags.delete rows in the same cli_pending_actions queue used by cron jobs, automations, settings PATCH, and session lifecycle. The route returns HTTP 202 immediately; the change does not apply until the user clicks Approve in the Omniscio inbox. The user-in-the-loop guarantee is identical to the twenty-one other approval-gated CLI capabilities documented in cli-pending-actions.md, and shares the same open-pending cap.
  • Per-session apply / unapply is immediate. POST /sessions/:id/tags (apply a library tag to one session) and DELETE /sessions/:id/tags/:tagId (remove that application) apply inline with no inbox row. The reasoning matches the in-app T-hotkey picker — applying a tag to a single session is reversible by removing the chip, the per-session 10-tag cap is enforced at insert time, and the project-scope visibility rule is checked synchronously, so adding an approval gate would just add latency without any new safety.
  • Reads are bearer-gated and read-budgeted. GET /tags (list every active library tag with session counts) and GET /sessions/:id/tags (list applied tags for one session) require the bearer token and share the 60-reads-per-minute-per-token-hash budget with all other auth-gated GETs on this server.

The split mirrors the in-app surface: library management lives in Settings → Tags (deliberate, dialog-driven, low-frequency), per-session apply lives behind the T hotkey (frequent, lightweight, instantly reversible).

Where to find it

There is no Omniscio screen for any of this — the whole feature lives outside the app. Its surface is the local HTTP control server on your own machine, the same one that already answers for cron jobs, automations, recipes, and settings; an outside AI agent or a script reaches it by holding your Omniscio token. Anything that changes your tag library is not applied straight away: it is queued and appears in the Omniscio inbox as a card you approve or reject by hand.

How it behaves

How to use it

The eight routes:

Method Path Approval-gated? Status Purpose
GET /tags n/a (read) 200 OK List every active library tag with derived scope, projectIds, sessionCount. Optional ?projectId=X narrows to tags visible in that project.
POST /tags Yes 202 Accepted Queue creation of a new library tag. Body: { name, color?, description?, icon?, projectIds? }.
PATCH /tags/:id Yes 202 Accepted Queue an update. Body is a partial — only included fields change; omitted fields are preserved. At least one field is required.
DELETE /tags/:id Yes 202 Accepted Queue soft-delete. Preview text embeds the cascade size (Delete tag "foo" (used by 3 sessions)).
GET /tags/:id/sessions n/a (read) 200 OK Reverse lookup — every session id carrying this library tag (what the Tags view uses to show "everything tagged X"). Unknown id returns an empty list.
POST /sessions/:id/tags No (immediate) 200 OK Apply a library tag to one session. Body: { tagId }.
DELETE /sessions/:id/tags/:tagId No (immediate) 200 OK Remove that application. No body.
GET /sessions/:id/tags n/a (read) 200 OK List TagApplication rows ({ sessionId, tagId, appliedAt, appliedBy }) for one session.

All eight require the bearer token in Authorization: Bearer <token> (or ?token=<token>). The mutating five share the global 10-mutations-per-minute rate limit; the three GETs share the 60-reads-per-minute read budget per token hash.

Workflow

  1. Discover what's already there with GET /tags before proposing duplicates. The response includes scope ('global' | 'project'), projectIds (empty array when global), and sessionCount per row, so you know what's in use and what's free.
  2. Propose a new tag with POST /tags — name + color + scope. Send X-Client-Request-Id (≤64 chars) so retries are idempotent in a 30-day window. The route returns HTTP 202 with the queued row's id. Tell the user "open Omniscio's inbox and approve to apply" and stop. The tag does not exist until the user clicks Approve.
  3. Edit a tag with PATCH /tags/:id — partial body, only the fields you want to change. Omitted fields are preserved at dispatch time (the dispatcher merges the patch into the existing row), so you can rename a tag without re-sending its color and scope. Same approval flow as create.
  4. Delete a tag with DELETE /tags/:id — no body. Approval card preview embeds the cascade size — "Delete tag 'foo' (used by 3 sessions)" — so the user sees the blast radius before approving. Approval cascades: the dispatcher hard-deletes every session_tags row for that tag, then soft-deletes the library row (is_deleted = 1). Free-form tag strings of the same name on sessions.tags are NOT touched.
  5. Apply a library tag to a session with POST /sessions/:id/tags body { tagId }. Returns 200 immediately on success. Returns 409 with reason: 'cap' if that session already carries 10 tags (combined library + free-form), or reason: 'visibility' if the tag's project scope excludes this session's project.
  6. Remove an application with DELETE /sessions/:id/tags/:tagId. Returns 200 if a row was actually deleted, 404 if the tag wasn't applied to that session.
  7. Inspect what's applied with GET /sessions/:id/tags — returns the raw join rows. Pair with GET /tags if you need the tag color / name.

Approval flow recap

For the three approval-gated routes (POST /tags, PATCH /tags/:id, DELETE /tags/:id):

  1. Route returns HTTP 202 with { ok: true, data: { id, actionKind, status: 'pending', previewText, ... } }.
  2. The pending row appears in Omniscio's inbox as a yellow card titled with the preview text (e.g. Create tag "urgent", Update tag "bug", Delete tag "stale" (used by 7 sessions)).
  3. The user clicks the card — a dedicated TagsApprovalPane opens (separate from the generic JSON-payload pane) showing a per-kind preview: name + color swatch + scope for create, before/after for update, name + cascade size for delete.
  4. Approve → the dispatcher executes the side effect (create row + scope rows / merge patch / cascade-delete + soft-delete), emits tags:changed (with affectedSessionIds for delete), and stamps dispatched_at. Reject → row flips to rejected with the reason, no side effect.
  5. The renderer's open clients receive the tags:changed push and refresh the library + session chip strips automatically.

The dispatcher distinguishes permanent from transient dispatch failures. Permanent — name collides at approve-time (someone else created the same name in-app), tag soft-deleted between enqueue and approval, target row missing — flips the pending row to rejected with PermanentDispatchError so the user doesn't loop forever clicking Approve. Transient — DB lock, network blip — reverts the row to pending for retry.

Idempotency

The three approval-gated routes accept an X-Client-Request-Id header (≤64 chars). When set, the same id within a 30-day window returns the existing pending row (HTTP 200, body includes idempotent: true) instead of queuing a duplicate. The lookup is scoped per action_kind, so the same id can map to a tags.create and a tags.update and a tags.delete independently.

The two immediate routes (POST /sessions/:id/tags, DELETE /sessions/:id/tags/:tagId) are idempotent at the database layer (INSERT OR IGNORE on apply, no-op delete on absent join row), so client-side idempotency keys aren't needed there.

Examples

# Read the bearer token from Omniscio's auto-delivered file
TOKEN=$(<~/.amc/cli-token)

# 1. List every library tag with session counts
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/tags

# 2. Propose creating a global "urgent" tag (red, with description)
curl -X POST http://127.0.0.1:19519/tags \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Client-Request-Id: tag-create-urgent-2026-05-07" \
  -d '{
    "name": "urgent",
    "color": "#ff4444",
    "description": "Needs response within 1 hour",
    "icon": "alert"
  }'
# Returns 202 with the pending row. The tag doesn't exist until the user
# approves in Omniscio's inbox.

# 3. Propose creating a project-scoped tag (visible only to two projects)
curl -X POST http://127.0.0.1:19519/tags \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Client-Request-Id: tag-create-acme-2026-05-07" \
  -d '{
    "name": "client-acme",
    "color": "#1f9d55",
    "projectIds": ["proj-uuid-a", "proj-uuid-b"]
  }'

# 4. Rename a tag (PATCH only the name; color, description, scope preserved)
curl -X PATCH http://127.0.0.1:19519/tags/<tag-uuid> \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Client-Request-Id: tag-rename-2026-05-07" \
  -d '{"name": "high-priority"}'

# 5. Delete a tag (cascade size shown in approval preview)
curl -X DELETE http://127.0.0.1:19519/tags/<tag-uuid> \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Client-Request-Id: tag-delete-stale-2026-05-07"

# 6. Apply a library tag to a session (immediate — no approval)
curl -X POST http://127.0.0.1:19519/sessions/<session-uuid>/tags \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tagId": "<tag-uuid>"}'
# 200 OK on success
# 409 {"reason":"cap"} if the session already carries 10 tags
# 409 {"reason":"visibility"} if the tag's scope excludes this session's project

# 7. Remove a tag from a session (immediate)
curl -X DELETE http://127.0.0.1:19519/sessions/<session-uuid>/tags/<tag-uuid> \
  -H "Authorization: Bearer $TOKEN"
# 200 if a row was actually removed
# 404 if the tag wasn't applied to that session

# 8. Inspect what's applied to a session
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/sessions/<session-uuid>/tags
# {"ok":true,"data":{"applications":[{"sessionId":"...","tagId":"...","appliedAt":"...","appliedBy":"user"},...]}}

Out of scope (v1)

  • Free-form per-session tag strings are not reachable through the CLI — only library tags. Free-form tags live on sessions.tags JSON column and are managed exclusively through the in-app T-hotkey picker. (See session-tags.md for what those are.)
  • applied_by: 'cli' — the database has the field but the CLI routes hard-code applied_by: 'user' so a CLI-applied tag is indistinguishable from a user-applied tag in the UI. Distinguishing them was deferred until there's a UX surface that benefits from the distinction.
  • Bulk apply / unapply — there is no POST /sessions/tags/bulk route. Apply per-session if you need to fan out across many sessions; the 10-mutations-per-minute rate limit caps the burst at 10 per minute (and the in-app UI doesn't have a bulk-apply surface either).

For agents

How it works

The eight routes live in src/main/services/cli/cli-server-tags-routes.ts, registered once at Omniscio startup from src/main/index.ts (after registerBookmarkRoutes()). Tests register them explicitly inside beforeAll after startCliServer() to keep the canonical real-HTTP + real-SQLite test pattern.

Schemas (Zod, strict-mode):

  • cliTagCreateSchema — name required (1-30 chars, trimmed), color / description / icon / projectIds all optional. Lives in src/shared/ipc-schemas.ts at line 3933.
  • cliTagUpdateSchema — same fields all optional, plus a .refine() that requires at least one key (so the dispatcher always has something to do).
  • cliSessionTagApplySchema — strict { tagId: string }.

The schemas are deliberately a subset of the in-app tagCreateSchema / tagUpdateSchema accepted by the IPC handlers — every field a CLI caller can send is a valid value for in-app dispatch, so there's no approval-time-rejection footgun where a CLI-built payload passes the route's Zod check but fails the dispatcher's revalidate.

Approval-gated route pipeline (POST /tags, PATCH /tags/:id, DELETE /tags/:id):

  1. Rate-limit check — checkRateLimit() from cli-server.ts (10 mutations / minute global). 429 on bust.
  2. Body parse — cliTagCreateSchema.safeParse(body). 400 on validation failure.
  3. Existence pre-check (PATCH / DELETE only) — getTagById(id) → 404 if missing. Saves the user from approving a card whose target is already gone.
  4. Idempotency lookup — findIdempotent(clientRequestId, actionKind) from queries-cli-pending.ts returns the existing row if the same (clientRequestId, action_kind) was inserted within the last 30 days. The lookup uses the unique partial index idx_cli_pending_request ON (client_request_id, action_kind) WHERE client_request_id IS NOT NULL. Returns HTTP 200 with idempotent: true on hit.
  5. Open-cap check — getOpenCount() >= CLI_PENDING_MAX_OPEN (20). 409 on bust.
  6. Insert pending row — insertPending({actionKind, targetId, payloadJson, previewText, clientRequestId}). The previewText is built per-kind: Create tag "<name>", Update tag "<existing-name>", Delete tag "<existing-name>" (used by N session(s)). All preview text is sliced to 80 chars.
  7. Emit push — emitPush(IPC.CLI_PENDING_CHANGED, { id: row.id }) so the renderer's inbox refreshes.
  8. Track event — trackEvent('cli_control', 'hit', { path: '/tags/<verb>' }).
  9. Respond — 202 { ok: true, data: row } on insert, 200 { ok: true, data: row, idempotent: true } on idempotent replay.

Immediate route pipeline (POST /sessions/:id/tags, DELETE /sessions/:id/tags/:tagId):

  1. Rate-limit check (10/min).
  2. Body parse (apply only — unapply has no body).
  3. Direct call into the underlying query: applyTagToSession(sessionId, tagId, 'user') or unapplyTagFromSession(sessionId, tagId) from queries-tags.ts. The applyTagToSession query enforces the 10-cap and the project-scope visibility rule inside a single transaction; the unapplyTagFromSession query just deletes the join row.
  4. Map internal reason codes to friendlier external names: TAG_CAP_REACHED → reason: 'cap', TAG_NOT_VISIBLE → reason: 'visibility'. Both surface as 409. External CLI consumers don't pin to internal reason names.
  5. Emit IPC.SESSION_TAGS_CHANGED with { sessionId } on success.
  6. Respond — 200 { ok: true } on apply / unapply success, 409 { ok: false, reason, error } on cap / visibility, 404 { ok: false, error: 'Tag is not applied to this session' } on unapply-of-absent.

GET routes:

  • GET /tags — bearer-gated, read-budgeted (60/min/token-hash). Optional ?projectId=X narrows to listTags({ projectId }); otherwise listTags() returns every active tag. Response: { ok: true, data: { tags } }.
  • GET /sessions/:id/tags — bearer-gated, read-budgeted, pre-checks getSession(id) for 404 (so unknown / soft-deleted ids surface explicitly rather than returning []), then returns tagsAppliedToSession(sessionId).

Dispatcher arms for the three approval-gated kinds live in src/main/services/cli/cli-pending-dispatcher.ts:

  • tags.create — re-validates the payload, checks for a name collision against the live library (case-insensitive on LOWER(name) WHERE is_deleted = 0; throws PermanentDispatchError with TAG_NAME_TAKEN on hit), calls createTag({...}), emits tags:changed.
  • tags.update — re-resolves the target row by id (404 → PermanentDispatchError), checks for a rename collision if name is in the patch, calls updateTag(targetId, payload) (which performs the partial merge — undefined fields preserved), emits tags:changed.
  • tags.delete — calls softDeleteTag(targetId) which atomically DELETEs every session_tags row for that tag, then UPDATEs the library row to is_deleted = 1, returns { affectedSessionIds }. The dispatcher emits tags:changed with affectedSessionIds so the renderer can invalidate per-session caches.

The dedicated TagsApprovalPane at src/renderer/src/features/cli-pending/TagsApprovalPane.tsx is selected by CliPendingApprovalModal early-return when row.actionKind is one of the three tag kinds. It parses the payload JSON per-kind and renders a structured preview: name + color swatch + icon + scope label for create, target + per-field "New X" rows for update (only the keys that were patched), and tag name for delete (the cascade count is in the row's previewText shown above the pane). Approve / reject go through the standard resolveInboxApproval('cli-pending', row.id, decision) consumer in stores/inbox-actions.ts, so post-resolution navigation is identical to every other CLI pending kind.

Related

The in-app library these routes drive is described on the tag manager page; the free-form per-session tags that sit outside them are on the session tags page; the approval queue every mutation lands in is on the CLI pending actions page; and the server itself — token handling and the full route catalog — is on the CLI control page.

  • tag-manager.md — the in-app library system (Settings → Tags), scope rules, sticky behaviour, deletion cascade, IPC contract.
  • session-tags.md — the free-form per-session tag flavour that coexists with library tags but is NOT reachable through CLI in v1.
  • cli-pending-actions.md — the shared approval queue every approval-gated CLI mutation lands in.
  • cli-control.md — the CLI server itself, token handling, the full route catalog.
  • omniscio-control skill — the external-AI side of the contract: how an AI queues these actions in practice.

Last verified 2026-09-28