---
title: "CLI knowledge workflows: exits, JSON contracts, and final states"
description: Check current CLI commands and JSON shapes; reject false success with synthetic process and per-ID state checks before scheduling and recovery.
image: "https://zengliangyi.com/og/blog/en/cli-knowledge-workflow.png"
---

A CLI can connect import, summarization, queries, and checks in scheduled work. A reliable pipeline records each exit code, response shape, and final state. A queued message alone is not a reason to publish a report. Commands below are checked against the pinned commit; acceptance exercises use synthetic responses without a real service or personal histories.

## Fix the connection and noninteractive output

[server/src/cli/index.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/index.ts) defines global –base-url, –token, –json, and –no-interactive. [server/src/cli/connection.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/connection.ts) selects explicit arguments, environment, saved connection, then the implicit local default. Automatic startup applies only to an unavailable implicit local default. User-configured loopback targets also face cloudMode checks. Scheduled jobs should fix their target and credential source so connection changes do not send writes to another instance. Use HTTPS remotely; the current client’s transport check does not replace deployment protection.

These are existing command forms. Only the final example requests summary enqueueing. Check mode and data scope before running them in a reviewed connection environment:



```bash
crystal   --json   status  
  crystal   --json   conversations   --status   imported   --limit   20  
  crystal   --json   notes   list   --tag   typescript   --limit   20  
  crystal   --json   search   "transaction rollback"   --limit   5  
  crystal   --json   tags  
  crystal   --json   summarize   --all
```



## Check JSON shapes and capabilities separately

| Command | Current JSON result | What automation must not assume |

| --- | --- | --- |

| status | Object with stats | No queue; missing fields cannot mean an empty queue |

| conversations / notes list | items, total, and pagination fields | The first page is not all results |

| search / tags | Arrays | Search is not a results wrapper object |

| summarize –all | queued, total, and queue | Enqueueing is not successful summaries and embeddings |

| config get / set | Config result; set accepts llm or embedding dotted keys | No sources.paths config command |

[server/src/cli/commands/status.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/commands/status.ts) and [server/src/cli/commands/search.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/commands/search.ts) provide the individual contracts. Serve uses `crystal serve --daemon --port 3721`, `crystal serve stop`, and `crystal serve status`, rather than a start subcommand. The current serve status error-display path does not explicitly set a failing exit code; config test may also print a connection failure and finish normally. Inspect business fields or the expected response instead of treating either process exit alone as readiness.

[server/src/cli/commands/import.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/commands/import.ts) uses a local scan stream, or a panel under TTY; the local branches do not forward –source. Only remote import applies that filter, and JSON or noninteractive remote use requires –yes. That flag approves source-conversation transmission; it does not enable summaries. Do not report source-filtered acceptance when the actual entry point scanned all enabled sources.

[server/src/cli/commands/summarize.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/commands/summarize.ts) currently calls summarize twice in its single-ID JSON branch, so that form cannot promise exactly one request. Targeted automation can use the batch-ids and status-ids APIs in [server/src/routes/notes.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/routes/notes.ts) with a recorded ID set. Check authentication, HTTP status, success, data, and schema. unknown/error must fail; imported/summarizing remain pending; summarized/filtered are distinct terminal states. Filtered does not mean a note was created, and a summary terminal state does not ensure embedding_status=done.

The remaining command contracts are in [notes](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/commands/notes.ts), [tags](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/commands/tags.ts), [config](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/commands/config.ts), [serve](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/commands/serve.ts), and the [HTTP client](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/client.ts). Recheck the pinned implementation’s display and request boundaries before deployment; a newer help page does not establish this commit’s behavior.

## An offline pipeline that rejects false success

Save this as workflow.mjs under Node 24 and run `node workflow.mjs`. It creates real child-process exits, stdout, and stderr, but those children only produce synthetic command responses. They never execute crystal. ID checks and polling bounds are teaching fixtures without HTTP, SSE, models, or a production database.



```js
import   assert   from   'node:assert/strict'  ;  
  import   { spawnSync }   from   'node:child_process'  ;  
  
  // Synthetic executable responses, never a real crystal process.  
  function   commandFixture  (  stdout  ,   status   =   0  ) {  
    const   code   =  
      'process.stderr.write("fixture diagnostic  \\  n");process.stdout.write(process.argv[1]);process.exit(Number(process.argv[2]));'  ;  
    const   result   =   spawnSync  (process.execPath, [  '-e'  , code, stdout,   String  (status)], {  
      encoding:   'utf8'  ,  
      timeout:   1000  ,  
    });  
    if   (result.error   ||   result.status   !==   0  )   throw   new   Error  (  'command failed'  );  
    return   JSON  .  parse  (result.stdout);  
  }  
  function   requireStatus  (  value  ) {  
    assert.  equal  (  typeof   value.stats?.totalConversations,   'number'  );  
    return   value;  
  }  
  function   requireImport  (  value  ) {  
    assert.  ok  (Array.  isArray  (value.summarizationCandidateIds));  
    assert.  equal  (value.errors,   0  );  
    return   value.summarizationCandidateIds;  
  }  
  function   inspectIds  (  value  ,   expected  ) {  
    assert.  ok  (Array.  isArray  (value.items));  
    const   byId   =   new   Map  (value.items.  map  ((  item  )   =>   [item.id, item.status]));  
    assert.  equal  (byId.size, expected.  length  );  
    let   pending   =   false  ;  
    for   (  const   id   of   expected) {  
      const   status   =   byId.  get  (id);  
      if   (  !  status   ||   [  'error'  ,   'unknown'  ].  includes  (status))   throw   new   Error  (  `failed ${  id  }`  );  
      if   ([  'imported'  ,   'summarizing'  ].  includes  (status)) pending   =   true  ;  
      else   if   (  !  [  'summarized'  ,   'filtered'  ].  includes  (status))   throw   new   Error  (  'unknown state'  );  
    }  
    return   pending   ?   'pending'   :   'finished'  ;  
  }  
  async   function   waitForIds  (  read  ,   ids  ,   maxPolls   =   3  ) {  
    for   (  let   i   =   0  ; i   <   maxPolls; i  ++  ) {  
      if   (  inspectIds  (  await   read  (), ids)   ===   'finished'  )   return  ;  
    }  
    throw   new   Error  (  'deadline exceeded'  );  
  }  
  
  const   status   =   requireStatus  (  commandFixture  (  JSON  .  stringify  ({ stats: { totalConversations:   2   } })));  
  assert.  equal  (status.queue,   undefined  );  
  assert.  throws  (()   =>   commandFixture  (  '{}'  ,   7  ),   /  command failed  /  );  
  assert.  throws  (()   =>   commandFixture  (  'not JSON'  ), SyntaxError);  
  assert.  throws  (()   =>   requireStatus  (  commandFixture  (  '{}'  )));  
  const   ids   =   requireImport  (  
    commandFixture  (  JSON  .  stringify  ({ errors:   0  , summarizationCandidateIds: [  'a'  ,   'b'  ] }))  
  );  
  assert.  throws  (()   =>   requireImport  ({ errors:   1  , summarizationCandidateIds: ids }));  
  const   receipt   =   commandFixture  (  '{"queued":2,"total":2}'  );  
  assert.  equal  (receipt.queued,   2  );   // Receipt alone never proves completion.  
  let   polls   =   0  ;  
  await   waitForIds  (  
    async   ()   =>   ({  
      items: ids.  map  ((  id  )   =>   ({ id, status:   ++  polls   <=   2   ?   'imported'   :   'summarized'   })),  
    }),  
    ids  
  );  
  assert.  ok  (polls   >   2  );  
  await   assert.  rejects  (  waitForIds  (  async   ()   =>   ({ items: [] }), ids));  
  await   assert.  rejects  (  
    waitForIds  (  async   ()   =>   ({ items: ids.  map  ((  id  )   =>   ({ id, status:   'error'   })) }), ids)  
  );  
  await   assert.  rejects  (  
    waitForIds  (  async   ()   =>   ({ items: ids.  map  ((  id  )   =>   ({ id, status:   'imported'   })) }), ids),  
    /  deadline  /  
  );  
  assert.  equal  (  
    inspectIds  (  
      {  
        items: [  
          { id:   'a'  , status:   'filtered'   },  
          { id:   'b'  , status:   'summarized'   },  
        ],  
      },  
      ids  
    ),  
    'finished'  
  );  
  console.  log  (  'exit, JSON shape, per-ID terminal state, and deadlines pass on synthetic fixtures'  );
```



## Faults, scheduling, and backup

Remove the nonzero-exit guard: the exit-7 child reaches parsing and the rejection assertion must fail. Return finished for an empty items array: the missing-ID rejection assertion must fail. Restore and rerun. Keep stderr separate from JSON stdout instead of merging with `2>&1`. Timeouts, HTTP errors, invalid JSON, missing fields, unknown IDs, and persistent pending states must fail the job. Poll count is a deterministic exercise cutoff; real jobs need wall-clock deadlines, per-request timeouts, and backoff.

The tracker in [server/src/queue/tracker.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/queue/tracker.ts) can clear five seconds after becoming idle. A late empty queue cannot prove the previous batch completed. Retain target IDs and query persistent conversation state, then inspect notes, embeddings, and relations required by the job. Add a single-job lock, fixed PATH and working directory, log retention, and failure reporting at the scheduler layer. These are surrounding job design, not new CLI features implemented here.

Use [server/src/runtime/paths.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/runtime/paths.ts) and [server/src/services/vector-index.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/vector-index.ts) to locate chatcrystal.db, config.json, vectra-index, and related deployment files. Include required authentication files in a controlled recovery plan. Stop every writer or use a snapshot with validated consistency before copying the agreed dataset; copying active files individually is not a cross-file transaction. Restore to an isolated instance and check conversation/note counts, target IDs, and retrieval, rather than merely checking that an archive exists. See [desktop lifecycle](/en/blog/fastify-electron-service-lifecycle/) for connection and shutdown boundaries, and [import performance acceptance](/en/blog/bulk-import-performance-plan/) for batch costs.

Share:

##  Related Posts

[##  Google Indexing in Astro: Incremental Cache-Driven Submissions

How to fix Google indexing deadlocks in static Astro sites using incremental cache-driven API submissions, Git SHA hashing, and sitemap lastmod timestamps.          曾亮懿       October 13, 2026      4 min read

#Astro

#SEO

#Google

#Automation](/en/blog/google-indexing-incremental-cache)    [##  AI Conversation Formats: JSONL Events vs SQLite Snapshots

Compare 5 AI adapter formats: separate JSONL events from SQLite keys, and inspect indexed, orphan, and unknown-version chat transcripts in memory stores.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#SQLite](/en/blog/ai-conversation-format-divergence)    [##  How Claude Code, Cursor, Copilot, and Trae Store Chats on Disk

Compare Claude Code, Cursor, Copilot, and Trae storage paths. Distinguish raw copies from exports, and verify migration retention with test manifests.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#SQLite](/en/blog/ai-conversation-management-options)

##  From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "CLI knowledge workflows: exits, JSON contracts, and final states",
  "description": "Check current CLI commands and JSON shapes; reject false success with synthetic process and per-ID state checks before scheduling and recovery.",
  "url": "https://zengliangyi.com/en/blog/cli-knowledge-workflow/",
  "image": "https://zengliangyi.com/images/blog/cli-knowledge-workflow.svg",
  "datePublished": "2026-10-07T00:00:00.000Z",
  "dateModified": "2026-10-07T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/cli-knowledge-workflow/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "CLI knowledge workflows: exits, JSON contracts, and final states",
      "item": "https://zengliangyi.com/en/blog/cli-knowledge-workflow/"
    }
  ]
}
```
