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

Quick Replies via CLI (read, create, move, delete, export and import the library from an AI agent)

Quick Replies via CLI exposes the personal Quick Replies library — snippets, dividers and folders — to an outside AI agent over Omniscio's local control server: ten routes to read, create, edit, reparent, reorder, delete, restore, export the whole library as a portable bundle, and import one back. Covers the gating model, the flat and tree read shapes, the add-only import merge, and what is deliberately out of scope.

What it is

Omniscio ships a Quick Replies library — the user's personal collection of pre-written messages they can paste into a running Claude session with a single click. Each row in the library is one of three shapes:

  • snippet — an actual reply with a label and a body text (e.g. label "Hi", text "Hello — thanks for the message."). An optional autoSubmit: true flag means clicking the snippet inserts AND sends the message in one step.
  • divider — a visual section break with a label only (no body). Renders as a horizontal rule with its label in the picker.
  • folder — a container that can hold snippets, dividers, or sub-folders. Has a label, no body, and can nest arbitrarily.

Every row has an optional parentId (the string id of its enclosing folder, or null for top-level rows) and a displayOrder integer used to sort siblings within the same parent. The full structure is therefore a tree — top-level rows have parentId: null, everything else points at a folder row's id. The in-app picker walks that tree depth-first sorted by displayOrder at every level. The in-app companion page is quick-replies.md; this page covers the ten CLI routes that expose the same library over the 127.0.0.1:19519 localhost HTTP server so an external AI agent (Claude Code in another project, ChatGPT with the omniscio-control skill, a curl script) holding the Omniscio bearer token can read, create, edit, reparent, reorder, delete, export and import entries.

The gating model is uniform across all ten routes — and deliberately permissive compared to the rest of the CLI surface:

  • Every mutation is apply-immediately. There is no inbox approval round-trip. POST /quick-replies/create, POST /quick-replies/move, PATCH /quick-replies/:id, DELETE /quick-replies/:id, POST /quick-replies/reorder, POST /quick-replies/restore, and POST /quick-replies/import all execute inline and return 200/201 the moment the change has landed. Quick Replies are personal text templates — no session spawn, no cost, fully reversible by re-creating or moving — so the inbox round-trip every approval-gated surface uses (cron, automations, settings, tag-library, recipes, sessions) would just add latency without any new safety. Open Omniscio clients refresh via the QUICK_REPLIES_CHANGED push.
  • No idempotency dedup. Unlike the cli-tags / cli-cron / cli-automations surfaces, which honor X-Client-Request-Id to fold a network-hiccup retry into the same pending row, the Quick Replies create body accepts a clientRequestId field for backward compatibility but the routes do not consult it. A blind retry on POST /quick-replies/create will produce two rows. Treat each mutation as one-shot; if you need to know whether a row landed, call GET /quick-replies/list — that is the source of truth.
  • Reads are bearer-gated but NOT read-budgeted. GET /quick-replies/list (flat), GET /quick-replies/tree (nested), and GET /quick-replies/export (the whole library as a bundle) require the bearer token in Authorization: Bearer <token> and return 401 when it is missing or invalid. They do not share the 60-reads-per-minute-per-token-hash budget that gates the rest of the auth-gated GET routes on this server — /list predates that budget, and /tree and /export were deliberately given the same plain-bearer tier so that diverging gates between read shapes of the same data would not surprise.
  • Mutations share the global 10-mutations-per-minute bucket with every other write route on this server, and return 401 when the bearer is missing or invalid. Every route on this surface — reads and mutations alike — answers a missing/invalid bearer with 401 unauthorized; 403 is reserved server-wide for an authenticated-but-denied request, and this surface has no such case (L05-F10).

The route family was renamed from /snippet/* to /quick-replies/* in schema v228 — anything you may have seen referring to /snippet/list or /snippet/create belongs to a prior surface and no longer exists.

Where to find it

Day to day the library is used from inside the app — the Quick Replies picker, where snippets, dividers and folders appear as a tree you can click, drag and edit. This page is about the other end of the same library: an AI agent reads and changes those rows from outside Omniscio, so the picker and the agent always see one set of entries. There is no separate screen to open for the agent side — it reaches the library over the same local control server the rest of Omniscio's automation is driven from, and needs the same access token.

How it behaves

Quick Replies is one of the few surfaces an AI can change with no approval step: every change is applied the instant it is made, and every open window is told to refresh itself, because these rows are personal text with nothing expensive or hard to undo behind them. The trade is that each request stands alone — repeating one that already half-succeeded makes a second copy rather than folding into the first — so the dependable way to find out what actually happened is to read the library back. Everything the agent can reach is also visible to you in the picker, and anything it gets wrong can be undone there in a couple of clicks.

For agents

How to use it

The ten routes:

Method Path Tier Status Purpose
GET /quick-replies/list read (bearer → 401 on missing) 200 OK Flat QuickReply[] — every row, regardless of parentId. Each row carries its own parentId so callers can rebuild the tree themselves.
GET /quick-replies/tree read (bearer → 401 on missing) 200 OK Nested QuickReplyTreeNode[] — top-level array, each node { item, children }, recursively. Children at every level are sorted by displayOrder ascending. The endpoint to call when an AI needs to "see what's inside what".
POST /quick-replies/create mutation (gateAuthMutation) 201 Created Create a snippet, divider, or folder. Body discriminates on type; parentId optional on every variant. Returns the created row.
PATCH /quick-replies/:id mutation 200 OK / 404 Partial update — any subset of the editable fields: label, text, autoSubmit, the auto-title group (autoTitleEnabled, autoTitleText, autoTitleAppendCount, autoTitleAppendTimestamp), the latestOverride* pair, prepend/appendQuickReplyIds, and the engine/model pin (pinnedProvider, pinnedModel, pinnedThinkingLevel). .strict() — rejects parentId, displayOrder, type, id. Reparent via move.
POST /quick-replies/move mutation 200 OK / 404 / 409 Reparent and/or reorder. Body { id, parentId, beforeId? }.
DELETE /quick-replies/:id mutation 200 OK / 400 Delete a snippet or divider (idempotent silent SQL — missing id still returns 200), OR delete a folder (requires explicit ?mode=cascade or ?mode=promote).
POST /quick-replies/reorder mutation 200 OK Bulk reorder via { orderedIds: string[] } (≤500 entries).

| POST | /quick-replies/restore | mutation | 201 Created / 409 | Undo a CLI delete: recreate a soft-deleted row from a full snapshot body ({ id, type, … } — the complete row, not a patch). 409 when a live row already holds that id: restore is fail-closed rather than overwriting a row that came back on its own. | | GET | /quick-replies/export | read (bearer → 401 on missing) | 200 OK | The whole library as ONE portable QuickReplyExportBundle ({ kind, version, exportedAt, appVersion, items }) — the identical structure the in-app Export writes to a file, so an agent can carry a library to another machine with no UI. Per-machine usage stats (useCount/lastUsedAt) are deliberately absent. | | POST | /quick-replies/import | mutation | 200 OK / 400 / 413 | Merge a bundle back. Body { bundle } (what /export returned) or { text } (that JSON as a string), plus an optional sourceLabel. Add-only — never deletes, replaces or edits an existing row — and runs the SAME shared merge the in-app Import button uses. Returns the counts: added, skipped, renamed, tagsCreated, keysDropped. No preview round-trip (nobody is watching a CLI call). |

All ten require the bearer token in Authorization: Bearer <token> (or ?token=<token>). The mutating seven share the global 10-mutations-per-minute rate limit. The three GETs are bearer-checked but not read-budgeted — see the gating-model paragraph above.

Workflow

  1. Discover what's already there with GET /quick-replies/tree before proposing new folders. The tree shape mirrors the in-app picker, so the response shows you what folders exist, what is inside each one, and the sibling order. Use /list instead if you want a flat array (e.g. to grep all snippet bodies for a phrase without walking the tree).
  2. Create a row with POST /quick-replies/create. The body's type field discriminates the variant — snippet (with text + optional autoSubmit), divider (label only), or folder (label only). Each variant accepts an optional parentId — omit (or send null) to land at the top level; pass a folder's id to drop the new row inside it. Returns 201 with the full new row including its server-assigned id. The change is immediately live in every open Omniscio client (push fires on success).
  3. Edit an existing row with PATCH /quick-replies/:id. Partial body — send any subset of the editable fields: label, text, autoSubmit, the auto-title group (autoTitleEnabled / autoTitleText / autoTitleAppendCount / autoTitleAppendTimestamp), latestOverrideEnabled/latestOverrideText, prepend/appendQuickReplyIds, and the engine/model pin (pinnedProvider / pinnedModel / pinnedThinkingLevel — the same pin the snippet create body accepts). The schema is .strict() — sending parentId, displayOrder, type, or id returns 400. To reparent or reorder, use POST /quick-replies/move instead. To convert a snippet into a folder, you cannot — delete and re-create.
  4. Reparent or reorder with POST /quick-replies/move. Body { id, parentId, beforeId? }. parentId: null moves the row to the top level; parentId: "<folder-id>" moves it inside that folder. beforeId (optional, defaults to null) controls position within the destination — null appends at the end; a sibling id inserts directly before that sibling. The sibling must already live inside the destination folder. Cycles (moving a folder into one of its own descendants) return 409; an unknown id returns 404.
  5. Delete a snippet or divider with DELETE /quick-replies/:id. No body, no query string. The legacy path is idempotent — a missing id still returns 200. ?mode= is ignored on non-folder rows.
  6. Delete a folder with DELETE /quick-replies/:id?mode=cascade or ?mode=promote. Folders are the one case on this surface where the route refuses an unqualified delete — to avoid silently orphaning everything inside:
    • ?mode=cascade — delete the folder AND every snippet, divider, and sub-folder inside it, recursively. One-shot wipe. Returns 200.
    • ?mode=promote — move every direct child up one level (into the deleted folder's parent), preserving sibling order, then delete the now-empty folder. Use this when the user wants to "ungroup" a folder without losing its contents. Returns 200.
    • Omitting ?mode= on a folder delete returns 400 with the error string Deleting a folder requires ?mode=cascade … or ?mode=promote ….
    • A missing id (no row exists with that id) falls through to the legacy idempotent path and returns 200, not 404. This matters: do not assume a 200 from a folder-delete means the folder existed — it means "after this call, no row with that id exists." If you need that distinction, GET /quick-replies/list first.
  7. Bulk reorder with POST /quick-replies/reorder. Body { orderedIds: ["id1", "id2", ...] } (≤500 entries). The ids are not required to share a parent — the route assigns each id a sequential displayOrder matching its position in the array. Use this when you have a known desired order across the whole library; use POST /quick-replies/move with beforeId when you only need to slot one row into place.
  8. Export the whole library with GET /quick-replies/export. No body; returns 200 with the bundle ({ kind, version, exportedAt, appVersion, items }). Plain JSON — save it, or pipe it straight into POST /quick-replies/import on another Omniscio machine. items is a flat array; each row names its own parentId and its prepend/append refs in the FILE's id space, so the tree round-trips. useCount/lastUsedAt are absent by design.
  9. Merge a bundle with POST /quick-replies/import. Body { bundle: <the /export payload> } or { text: "<the same JSON as a string>" }, plus an optional sourceLabel (defaults to "command line"). The merge is add-only and repeatable: an incoming row whose type AND label AND text already match a row here is skipped, so re-importing the same file adds nothing; a snippet label taken by different text arrives renamed "<label> (2)"; a numberKey survives only where the digit is free here and unclaimed by another row in the same file. Rows land in ONE transaction, so a failure adds nothing. Returns 200 with { added, skipped, renamed, tagsCreated, keysDropped }. A malformed / non-library / newer-version / over-cap payload returns 400 with a plain-English reason — there is no preview round-trip, because nobody is watching a CLI call.

What "apply-immediately" means here, vs the rest of the CLI

If you have used the cli-tags / cli-cron / cli-automations / settings / sessions CLI surfaces, the standard response shape for a mutating call is 202 Accepted — the change is queued in Omniscio's inbox and the user has to click Approve before it lands. None of that applies on this surface. A 200 or 201 from any Quick Replies mutation means the row already exists / has been updated / has been moved / has been deleted in Omniscio's database, and every open Omniscio client has been told via the QUICK_REPLIES_CHANGED push. Tell the user "Done — the change is already live in Omniscio's Quick Replies picker", not "open the inbox and approve."

Idempotency

There is none. The create body's Zod schema includes an optional clientRequestId field, but the route does not consult it — it is accepted for backward compatibility with older callers and does nothing. A network-hiccup retry on POST /quick-replies/create will produce two rows. If you do not know whether the previous call landed, call GET /quick-replies/list and search for the row instead of re-sending the create.

This is a deliberate KISS choice documented in the cli-quick-replies-contract.md § Architecture — current truth: Quick Replies are personal text templates, low-risk and fully reversible, so the cost of an accidental duplicate row (the user deletes it in two clicks) is lower than the cost of a real idempotency table the contract has to maintain. If your script genuinely needs at-most-once semantics, do a read-then-write yourself.

Examples

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

# 1. List everything (flat)
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/quick-replies/list

# 2. Read the library as a real tree
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/quick-replies/tree
# {
#   "ok": true,
#   "data": [
#     { "item": {"id":"f1","type":"folder","label":"Greetings","parentId":null,...},
#       "children": [
#         { "item": {"id":"qr1","type":"snippet","label":"Hi",...}, "children": [] }
#       ]
#     },
#     { "item": {"id":"qr-top","type":"snippet","label":"Standalone","parentId":null,...}, "children": [] }
#   ]
# }

# 3. Create a top-level "Greetings" folder
curl -X POST http://127.0.0.1:19519/quick-replies/create \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"quickReply":{"type":"folder","label":"Greetings"}}'
# 201 with {ok:true,data:{id:"<folder-uuid>",type:"folder",label:"Greetings",parentId:null,...}}

# 4. Create a "Hi" snippet INSIDE the Greetings folder
curl -X POST http://127.0.0.1:19519/quick-replies/create \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "quickReply": {
      "type": "snippet",
      "label": "Hi",
      "text": "Hello — thanks for the message.",
      "autoSubmit": false,
      "parentId": "<folder-uuid>"
    }
  }'

# 5. Create a divider at the top level
curl -X POST http://127.0.0.1:19519/quick-replies/create \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"quickReply":{"type":"divider","label":"— Greetings —"}}'

# 6. Rename a snippet / change its text (PATCH does NOT accept parentId)
curl -X PATCH http://127.0.0.1:19519/quick-replies/<id> \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label":"Hello there","text":"Hello — thanks for reaching out."}'

# 7. Move a snippet into the Greetings folder (append at the end)
curl -X POST http://127.0.0.1:19519/quick-replies/move \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"<snippet-id>","parentId":"<folder-id>"}'

# 8. Move a snippet into the Greetings folder, positioned before a specific sibling
curl -X POST http://127.0.0.1:19519/quick-replies/move \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"<snippet-id>","parentId":"<folder-id>","beforeId":"<sibling-id>"}'

# 9. Move a snippet back to the top level
curl -X POST http://127.0.0.1:19519/quick-replies/move \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"<snippet-id>","parentId":null}'

# 10. Delete a snippet (legacy idempotent path)
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/quick-replies/<snippet-id>

# 11. Delete a folder AND everything inside it
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:19519/quick-replies/<folder-id>?mode=cascade"

# 12. "Ungroup" a folder — move its children up one level then delete the empty folder
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:19519/quick-replies/<folder-id>?mode=promote"

# 13. Bulk-reorder the top-level rows
curl -X POST http://127.0.0.1:19519/quick-replies/reorder \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"orderedIds":["id-a","id-b","id-c","id-d"]}'

# 14. Export the whole library as a portable bundle
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/quick-replies/export
# {"ok":true,"data":{"kind":"omniscio.quick-replies","version":1,"items":[ ... ]}}

# 15. Import a bundle (add-only merge) — wrap the exported file in a "bundle" key
jq -c '{bundle: .}' library.json > import-body.json
curl -X POST http://127.0.0.1:19519/quick-replies/import \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @import-body.json
# 200 with {"ok":true,"data":{"added":12,"skipped":3,"renamed":1,"tagsCreated":2,"keysDropped":0}}

Errors

HTTP Meaning Recover
400 Body failed Zod validation (empty label, missing required field, unknown key on a .strict() schema — including parentId / displayOrder / type / id on PATCH), OR folder delete without ?mode=cascade or ?mode=promote, or double-encoded (mojibake) text in label/text/name, OR POST /quick-replies/import with a payload that is not a readable library (bad JSON, wrong kind, a version newer than this build, over the 2000-item / 5 MB cap, or an entry with no id) — the message says plainly which. Fix the body (re-send the text as clean UTF-8) / add ?mode= / pick a file Omniscio exported.
401 Missing or invalid bearer on any route — reads (GET /list, GET /tree, GET /export) and mutations (POST /create, POST /move, PATCH /:id, DELETE /:id, POST /reorder, POST /restore, POST /import) alike. This surface has no authenticated-but-denied case, so 403 never appears here. Re-read the token (see cli-control.md).
404 PATCH /quick-replies/:id on an unknown id, OR POST /quick-replies/move with an unknown id. Folder delete on an unknown id falls through to the idempotent legacy path and returns 200, NOT 404 — see workflow step 6. Re-list with GET /quick-replies/list.
409 POST /quick-replies/move would create a cycle — moving a folder into itself or into one of its own descendants. Pick a different parentId.
413 The request body is bigger than the route accepts. Every route defaults to a 1 MB body cap; POST /quick-replies/import raises its own to the feature's own 5 MB envelope plus slack for the { "bundle": … } wrapper, so this fires only past that. Rejected by the body parser, before the route runs. Nothing was written. Split the library into two files and import them one after the other — the merge is add-only and repeatable.
429 Shared 10-mutations-per-minute rate limit. Wait 60 seconds.
500 Server-side failure. Surface the error and tell the user to check Omniscio's logs.

How it works

The ten routes are registered by registerQuickReplyRoutes() in src/main/services/cli/cli-server-quick-reply-routes.ts, called once at Omniscio startup alongside the other route-registration functions on the same server.

Schemas (Zod, in src/shared/ipc-schemas/quick-replies.ts):

  • quickReplyRestCreateSchema (line 130) — a discriminated union on type. The snippet variant requires label (1-50 chars, trimmed) + text (1-500,000 chars — the same cap the IPC create and REST update schemas use), with optional autoSubmit: boolean (default false) and optional parentId: string | null. The divider variant requires label only, optional parentId. The folder variant requires label only, optional parentId. clientRequestId is accepted for backward compatibility but ignored by the route (see "Idempotency").
  • quickReplyRestUpdateSchema — .strict(). Accepts any subset of the editable fields: label, text, autoSubmit, the auto-title group (autoTitleEnabled, autoTitleText, autoTitleAppendCount, autoTitleAppendTimestamp), latestOverrideEnabled/latestOverrideText, and prepend/appendQuickReplyIds — at parity with the IPC updateQuickReplySchema. Every other key (including parentId, displayOrder, type, id) returns 400. Reparent via POST /quick-replies/move. The strictness is load-bearing — it stops a script that meant to reparent from accidentally silently no-op'ing because the route happily accepted and dropped the parentId.
  • quickReplyRestMoveSchema (line 205) — .strict(). Strict { id: string, parentId: string | null, beforeId?: string | null }. Maps 1:1 onto queries-quick-replies.moveItem(id, parentId, beforeId).
  • quickReplyRestReorderSchema (line 196) — { orderedIds: string[] } capped at 500 entries.
  • quickReplyCliImportSchema (line 516) — the body for POST /quick-replies/import: exactly one of bundle (object) or text (string), plus an optional sourceLabel. The route hands the chosen form to importQuickReplies(text, sourceLabel) in src/main/services/quick/quick-reply-transfer.ts — the SAME merge the in-app Import button runs (contract: both-doors-are-the-same-door) — and GET /quick-replies/export returns buildQuickReplyExport() from that same module. A QuickReplyImportPayloadError maps to 400 with its plain-English reason; anything else is a 500. The import emits the QUICK_REPLIES_CHANGED push and the imported_cli telemetry verb, with no metadata (reply text can hold PII, so the quick_reply registry entry allow-lists none — the counts ride the response and the log line).

Corruption guards. Every write schema (create/update for both IPC and REST, plus folder-create) carries .superRefine(noReplacementCharacterRefinement) (rejects U+FFFD) and .superRefine(noMojibakeRefinement) (rejects CP1252 double-encoded text — an em-dash arriving as â€", ≥2 corrupted runs, via the shared detectLikelyMojibake); both surface as 400. This stops a mis-decoded capture (the PowerShell Get-Content without -Encoding utf8 trap) from persisting garbled text that then replays on every use. See contract invariant I8.

Route pipeline (mutating routes — POST /create, POST /move, PATCH /:id, DELETE /:id, POST /reorder, POST /import):

  1. gateAuthMutation at the dispatcher level — checks bearer (401 on missing), enforces the global 10-mutations-per-minute bucket (429 on bust). This is the same gate every other mutation on the CLI server uses.

  2. Body parse via the matching Zod schema — 400 on validation failure with the Zod error message in the error field.

  3. Direct call into the underlying query in src/main/db/queries-quick-replies.ts:

    • createQuickReply(label, text, autoSubmit, {}, [], parentId) for snippets,
    • createQuickReplyDivider(label, parentId) for dividers,
    • createFolder(label, parentId) for folders (line 421),
    • updateQuickReply(id, patch) for PATCH,
    • moveItem(id, parentId, beforeId) for move (line 503),
    • deleteQuickReply(id) + clearQuickReplyReferences(id) for snippet / divider delete,
    • deleteFolder(id, mode) for folder delete (line 611) with mode: 'cascade' | 'promote',
    • reorderQuickReplies(orderedIds) for reorder.

    DELETE /quick-replies/:id looks up the row first via getQuickReply(id). If the row exists and has type === 'folder', the ?mode= gate runs and deleteFolder() dispatches; otherwise the route falls through to the snippet/divider path (which is idempotent — a missing id still returns 200). That type check is load-bearing: a folder silently demoted to a snippet would orphan its children at delete time.

  4. moveItem error mapping in cli-server-quick-reply-routes.ts:

    • 'Item not found' → 404 via notFoundResponse(res).
    • Message containing 'descendant' OR 'Cycle' → 409 { ok: false, error: <message> }.
    • Any other thrown error rethrows to the outer internalErrorResponse 500 handler.
  5. Emit push — emitPush(IPC.QUICK_REPLIES_CHANGED, {}) (zero payload — clients refetch on the signal).

  6. Track event — trackEvent('quick_reply', '<verb>_cli', { source: 'cli' }). The verbs are created_cli, updated_cli, moved_cli, deleted_cli, folder_deleted_cli, reordered_cli.

  7. Respond — 201 on create (with the new row), 200 on every other successful mutation.

Route pipeline (read routes — GET /list, GET /tree, GET /export):

  1. authenticateRequest() — plain bearer-check. Returns 401 { ok: false, error: 'unauthorized' } on missing or invalid token. Not gateAuthRead — the deliberate parity with the pre-existing /quick-replies/list is documented in the contract: both endpoints expose the same data, only in different shapes, and diverging gates between them would surprise.
  2. Direct call into listQuickReplies() (flat) or getFolderTree() (line 453, nested).
  3. Respond — 200 { ok: true, data: <rows-or-tree> }.

Tree node shape — QuickReplyTreeNode in src/shared/types/quick-replies.ts:

type QuickReplyTreeNode = {
  item: QuickReply
  children: QuickReplyTreeNode[]
}

Top-level nodes carry item.parentId === null. Children at every level are sorted by displayOrder ascending. getFolderTree() builds the tree in a single SQLite pass — there is no N+1.

Telemetry verbs: created_cli, updated_cli, moved_cli, deleted_cli, folder_deleted_cli, reordered_cli, restored_cli, imported_cli — the _cli suffix lets the in-app analytics distinguish CLI-driven changes from in-app picker edits.

Test coverage — every invariant on this surface is locked by a test:

  • tests/unit/quick-reply-rest-schemas.test.ts — schema discriminated-union shapes, .strict() rejections on PATCH and move, optional parentId defaulting to null, folder-variant acceptance.
  • tests/unit/cli-server-quick-replies-routes.test.ts — every route's auth tier (uniform 401 unauthorized on missing/invalid token, reads and mutations alike), parentId forwarding into the correct DB helper, tree shape, the folder-delete ?mode= gate, the 'Item not found' → 404 / descendant/Cycle → 409 error mapping, and the push + telemetry wiring on each mutation.
  • tests/unit/quick-reply-mojibake-guard.test.ts — the five write schemas reject CP1252 double-encoded (mojibake) text while accepting genuine em-dashes, arrows, smart quotes, emoji, and accented / non-English letters (contract I8).

The contract at .claude/memory/contracts/cli-quick-replies-contract.md enumerates the eight invariants I1–I8 and cites the exact describe(...) block in those tests that fails when each one is touched. Read it before changing this surface.

Out of scope (v1)

  • No idempotency dedup. clientRequestId is accepted in the create body for backward compatibility but ignored. A retry produces a duplicate row. Documented above; deliberate KISS choice. (POST /quick-replies/import is the one exception in spirit: re-importing the same bundle is a no-op because identical label+text rows are skipped, but that is a property of the merge, not a request-id dedup.)
  • No replace / overwrite mode on import. POST /quick-replies/import only ever ADDS rows — there is deliberately no mode that replaces, clears or edits the existing library, so a bundle can be applied without any risk to what is already there. A script that wants a clean slate must delete first with DELETE /quick-replies/:id.
  • No transactional bulk endpoint. There is no POST /quick-replies/bulk that creates a folder + its children atomically. Build the tree top-down: create the folder, read back its id, then create each child with that id as parentId. If you discover a mistake mid-script, walk the changes back individually with POST /quick-replies/move and DELETE /quick-replies/:id.
  • No type conversion via PATCH. The update schema does not accept type — you cannot turn a snippet into a folder or vice versa. Delete and re-create.
  • No AHK / hotkey CLI control. The Quick Replies CLI manages the library only. The flow that pastes a snippet's text into a running session's textarea via an AutoHotkey hotkey is configured in Settings → Quick Replies → Behavior and is not reachable through this surface.
  • No per-session apply route. Tags have a POST /sessions/:id/tags route for attaching a library tag to one session. Quick Replies do not — the picker lives on the user's screen, not on a session row, so applying a snippet "to a session" doesn't have a clean meaning at the CLI layer.

Related

How the picker itself is arranged and used from inside the app — dragging a snippet into a folder, the inline editor, and the two ways of deleting a folder — is on the Quick Replies page. If what you want is the moment a snippet is the right reply, see Use quick responses, and Quick reply declutter nudge covers keeping a library that keeps growing under control.

  • quick-replies.md — the in-app companion: the virtual project's three-tab sidebar, the drag-into-folder behavior, the inline editor's Content + Behavior tabs, the two-button cascade-vs-promote delete dialog, and the Export / Share / Import actions that share a code path with the two routes above.
  • cli-control.md — the CLI server itself: token handling, the full route catalog, the 10-mutations-per-minute mutation bucket, the 60-reads-per-minute read budget that this surface is exempt from.
  • cli-tags.md — the closest sibling CLI surface (multi-route library management for tag library). The contrast is instructive: cli-tags has an inbox-approval round-trip on library mutations and honors X-Client-Request-Id for idempotency; cli-quick-replies has neither, because Quick Replies are personal text and tag library changes carry blast radius across sessions.
  • cli-pending-actions.md — the inbox-approval surface used by every approval-gated CLI mutation. Quick Replies do not land here — every Quick Replies mutation skips the queue and applies inline.
  • omniscio-control quick-replies skill — the external-AI side of the contract: how an agent decides which route to call when the user says "add a quick reply for X" or "make a Greetings folder".
  • cli-quick-replies-contract.md — the test-locked invariants I1–I8 with citations into the routes and schemas tests. Read this before changing the surface.

Last verified 2026-09-29