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
labeland a bodytext(e.g. label"Hi", text"Hello — thanks for the message."). An optionalautoSubmit: trueflag means clicking the snippet inserts AND sends the message in one step. - divider — a visual section break with a
labelonly (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, andPOST /quick-replies/importall execute inline and return200/201the 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 theQUICK_REPLIES_CHANGEDpush. - No idempotency dedup. Unlike the cli-tags / cli-cron / cli-automations surfaces, which honor
X-Client-Request-Idto fold a network-hiccup retry into the same pending row, the Quick Replies create body accepts aclientRequestIdfield for backward compatibility but the routes do not consult it. A blind retry onPOST /quick-replies/createwill produce two rows. Treat each mutation as one-shot; if you need to know whether a row landed, callGET /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), andGET /quick-replies/export(the whole library as a bundle) require the bearer token inAuthorization: Bearer <token>and return401when 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 —/listpredates that budget, and/treeand/exportwere 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
401when the bearer is missing or invalid. Every route on this surface — reads and mutations alike — answers a missing/invalid bearer with401 unauthorized;403is 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
- Discover what's already there with
GET /quick-replies/treebefore 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/listinstead if you want a flat array (e.g. to grep all snippet bodies for a phrase without walking the tree). - Create a row with
POST /quick-replies/create. The body'stypefield discriminates the variant —snippet(withtext+ optionalautoSubmit),divider(label only), orfolder(label only). Each variant accepts an optionalparentId— omit (or sendnull) to land at the top level; pass a folder'sidto drop the new row inside it. Returns201with the full new row including its server-assignedid. The change is immediately live in every open Omniscio client (push fires on success). - 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 thesnippetcreate body accepts). The schema is.strict()— sendingparentId,displayOrder,type, oridreturns400. To reparent or reorder, usePOST /quick-replies/moveinstead. To convert a snippet into a folder, you cannot — delete and re-create. - Reparent or reorder with
POST /quick-replies/move. Body{ id, parentId, beforeId? }.parentId: nullmoves the row to the top level;parentId: "<folder-id>"moves it inside that folder.beforeId(optional, defaults tonull) controls position within the destination —nullappends 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) return409; an unknownidreturns404. - Delete a snippet or divider with
DELETE /quick-replies/:id. No body, no query string. The legacy path is idempotent — a missing id still returns200.?mode=is ignored on non-folder rows. - Delete a folder with
DELETE /quick-replies/:id?mode=cascadeor?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. Returns200.?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. Returns200.- Omitting
?mode=on a folder delete returns400with the error stringDeleting 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, not404. This matters: do not assume a200from 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/listfirst.
- 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 sequentialdisplayOrdermatching its position in the array. Use this when you have a known desired order across the whole library; usePOST /quick-replies/movewithbeforeIdwhen you only need to slot one row into place. - Export the whole library with
GET /quick-replies/export. No body; returns200with the bundle ({ kind, version, exportedAt, appVersion, items }). Plain JSON — save it, or pipe it straight intoPOST /quick-replies/importon another Omniscio machine.itemsis a flat array; each row names its ownparentIdand its prepend/append refs in the FILE's id space, so the tree round-trips.useCount/lastUsedAtare absent by design. - Merge a bundle with
POST /quick-replies/import. Body{ bundle: <the /export payload> }or{ text: "<the same JSON as a string>" }, plus an optionalsourceLabel(defaults to"command line"). The merge is add-only and repeatable: an incoming row whosetypeANDlabelANDtextalready 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)"; anumberKeysurvives 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. Returns200with{ added, skipped, renamed, tagsCreated, keysDropped }. A malformed / non-library / newer-version / over-cap payload returns400with 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 ontype. Thesnippetvariant requireslabel(1-50 chars, trimmed) +text(1-500,000 chars — the same cap the IPC create and REST update schemas use), with optionalautoSubmit: boolean(defaultfalse) and optionalparentId: string | null. Thedividervariant requireslabelonly, optionalparentId. Thefoldervariant requireslabelonly, optionalparentId.clientRequestIdis 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, andprepend/appendQuickReplyIds— at parity with the IPCupdateQuickReplySchema. Every other key (includingparentId,displayOrder,type,id) returns 400. Reparent viaPOST /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 theparentId.quickReplyRestMoveSchema(line 205) —.strict(). Strict{ id: string, parentId: string | null, beforeId?: string | null }. Maps 1:1 ontoqueries-quick-replies.moveItem(id, parentId, beforeId).quickReplyRestReorderSchema(line 196) —{ orderedIds: string[] }capped at 500 entries.quickReplyCliImportSchema(line 516) — the body forPOST /quick-replies/import: exactly one ofbundle(object) ortext(string), plus an optionalsourceLabel. The route hands the chosen form toimportQuickReplies(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) — andGET /quick-replies/exportreturnsbuildQuickReplyExport()from that same module. AQuickReplyImportPayloadErrormaps to400with its plain-English reason; anything else is a500. The import emits theQUICK_REPLIES_CHANGEDpush and theimported_clitelemetry verb, with no metadata (reply text can hold PII, so thequick_replyregistry 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):
gateAuthMutationat the dispatcher level — checks bearer (401on missing), enforces the global 10-mutations-per-minute bucket (429on bust). This is the same gate every other mutation on the CLI server uses.Body parse via the matching Zod schema —
400on validation failure with the Zod error message in theerrorfield.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) withmode: 'cascade' | 'promote',reorderQuickReplies(orderedIds)for reorder.
DELETE /quick-replies/:idlooks up the row first viagetQuickReply(id). If the row exists and hastype === 'folder', the?mode=gate runs anddeleteFolder()dispatches; otherwise the route falls through to the snippet/divider path (which is idempotent — a missing id still returns 200). Thattypecheck is load-bearing: a folder silently demoted to a snippet would orphan its children at delete time.moveItemerror mapping in cli-server-quick-reply-routes.ts:'Item not found'→404vianotFoundResponse(res).- Message containing
'descendant'OR'Cycle'→409 { ok: false, error: <message> }. - Any other thrown error rethrows to the outer
internalErrorResponse500 handler.
Emit push —
emitPush(IPC.QUICK_REPLIES_CHANGED, {})(zero payload — clients refetch on the signal).Track event —
trackEvent('quick_reply', '<verb>_cli', { source: 'cli' }). The verbs arecreated_cli,updated_cli,moved_cli,deleted_cli,folder_deleted_cli,reordered_cli.Respond —
201on create (with the new row),200on every other successful mutation.
Route pipeline (read routes — GET /list, GET /tree, GET /export):
authenticateRequest()— plain bearer-check. Returns401 { ok: false, error: 'unauthorized' }on missing or invalid token. NotgateAuthRead— the deliberate parity with the pre-existing/quick-replies/listis documented in the contract: both endpoints expose the same data, only in different shapes, and diverging gates between them would surprise.- Direct call into
listQuickReplies()(flat) orgetFolderTree()(line 453, nested). - 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, optionalparentIddefaulting tonull, folder-variant acceptance.tests/unit/cli-server-quick-replies-routes.test.ts— every route's auth tier (uniform401 unauthorizedon 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.
clientRequestIdis accepted in the create body for backward compatibility but ignored. A retry produces a duplicate row. Documented above; deliberate KISS choice. (POST /quick-replies/importis 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/importonly 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 withDELETE /quick-replies/:id. - No transactional bulk endpoint. There is no
POST /quick-replies/bulkthat creates a folder + its children atomically. Build the tree top-down: create the folder, read back itsid, then create each child with that id asparentId. If you discover a mistake mid-script, walk the changes back individually withPOST /quick-replies/moveandDELETE /quick-replies/:id. - No
typeconversion via PATCH. The update schema does not accepttype— 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/tagsroute 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-Idfor 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