@gmickel/gno 1.7.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +10 -1
  2. package/assets/skill/SKILL.md +41 -0
  3. package/assets/skill/cli-reference.md +34 -0
  4. package/assets/skill/examples.md +37 -0
  5. package/assets/skill/mcp-reference.md +21 -0
  6. package/package.json +1 -1
  7. package/src/cli/commands/capture.ts +282 -0
  8. package/src/cli/commands/index-cmd.ts +17 -6
  9. package/src/cli/commands/shared.ts +17 -1
  10. package/src/cli/commands/update.ts +17 -6
  11. package/src/cli/options.ts +2 -0
  12. package/src/cli/program.ts +64 -0
  13. package/src/config/content-types.ts +140 -0
  14. package/src/config/defaults.ts +1 -0
  15. package/src/config/index.ts +14 -0
  16. package/src/config/loader.ts +11 -2
  17. package/src/config/types.ts +37 -1
  18. package/src/core/capture-write.ts +38 -0
  19. package/src/core/capture.ts +746 -0
  20. package/src/core/config-mutation.ts +14 -2
  21. package/src/core/file-ops.ts +21 -2
  22. package/src/core/note-presets.ts +61 -5
  23. package/src/ingestion/frontmatter.ts +77 -2
  24. package/src/ingestion/index.ts +2 -0
  25. package/src/ingestion/sync-options.ts +29 -0
  26. package/src/ingestion/sync.ts +104 -16
  27. package/src/ingestion/types.ts +14 -0
  28. package/src/mcp/tools/add-collection.ts +8 -5
  29. package/src/mcp/tools/capture.ts +137 -191
  30. package/src/mcp/tools/index-cmd.ts +13 -7
  31. package/src/mcp/tools/index.ts +43 -9
  32. package/src/mcp/tools/sync.ts +12 -6
  33. package/src/mcp/tools/workspace-write.ts +16 -10
  34. package/src/pipeline/hybrid.ts +2 -0
  35. package/src/pipeline/search.ts +2 -0
  36. package/src/pipeline/types.ts +2 -0
  37. package/src/pipeline/vsearch.ts +4 -0
  38. package/src/sdk/client.ts +156 -20
  39. package/src/sdk/index.ts +2 -0
  40. package/src/sdk/types.ts +6 -0
  41. package/src/serve/background-runtime.ts +18 -4
  42. package/src/serve/config-sync.ts +9 -2
  43. package/src/serve/public/components/CaptureModal.tsx +248 -10
  44. package/src/serve/public/globals.built.css +1 -1
  45. package/src/serve/routes/api.ts +238 -26
  46. package/src/serve/server.ts +12 -0
  47. package/src/serve/watch-service.ts +12 -2
  48. package/src/store/migrations/009-content-type-rule-fingerprint.ts +36 -0
  49. package/src/store/migrations/index.ts +12 -1
  50. package/src/store/sqlite/adapter.ts +29 -14
  51. package/src/store/types.ts +6 -0
package/README.md CHANGED
@@ -92,9 +92,16 @@ gno daemon --detach # headless continuous indexing (background; --status / --st
92
92
 
93
93
  ## What's New
94
94
 
95
- > Latest release: [v1.1.0](./CHANGELOG.md#110---2026-04-21)
95
+ > Latest release: [v1.8.0](./CHANGELOG.md#180---2026-06-05)
96
96
  > Full release history: [CHANGELOG.md](./CHANGELOG.md)
97
97
 
98
+ - **Second-brain capture**: `gno capture`, REST `/api/capture`, SDK
99
+ `client.capture()`, MCP `gno_capture`, and Web UI Quick Capture write
100
+ provenance-rich notes from text, stdin, or files, including typed presets for
101
+ ideas, people, company/projects, and meetings
102
+ - **Schema-lite content types**: optional `contentTypes` rules map configured
103
+ frontmatter `type` values or path prefixes to canonical `contentType` metadata
104
+ in JSON search/query results
98
105
  - **Publish to [gno.sh](https://gno.sh/publish)**: new `gno publish export` CLI and Web UI action produce a self-contained artifact you upload to the hosted reader — public, secret, invite-only, or locally encrypted before upload
99
106
  - **Retrieval Quality Upgrade**: stronger BM25 lexical handling, code-aware chunking, terminal result hyperlinks, and per-collection model overrides
100
107
  - **Code Embedding Benchmarks**: new benchmark workflow across canonical, real-GNO, and pinned OSS slices for comparing alternate embedding models
@@ -543,6 +550,8 @@ Open `http://localhost:3000` to:
543
550
  - **Browse**: Cross-collection tree workspace with folder detail panes and per-tab browse context
544
551
  - **Edit**: Create, edit, and delete documents with live preview
545
552
  - **Create in place**: New notes in the current folder/collection with presets and command-palette flows
553
+ - **Capture with provenance**: `gno capture` and Web UI Quick Capture write quick notes to an editable collection with structured `source:` metadata, typed preset scaffolds, and a receipt that separates write, sync, and embed state
554
+ - **Same capture contract everywhere**: CLI, MCP `gno_capture`, REST `/api/capture`, SDK `client.capture()`, and Web UI Quick Capture return the same provenance receipt shape
546
555
  - **Ask**: AI-powered Q&A with citations
547
556
  - **Manage Collections**: Add, remove, and re-index collections
548
557
  - **Connect agents**: Install core Skill/MCP integrations from the app
@@ -207,6 +207,47 @@ gno embed --collection travel
207
207
 
208
208
  MCP `gno.sync` and `gno.capture` do NOT auto-embed. Use CLI for embedding.
209
209
 
210
+ ## Capture Notes
211
+
212
+ Use `gno capture` for quick second-brain writes into an editable collection:
213
+
214
+ ```bash
215
+ gno capture "thought to remember"
216
+ gno capture --file ./clip.md --source-url https://example.com --source-kind web --json
217
+ gno capture --preset person --title "Jane Doe" --folder people/
218
+ gno capture --preset meeting --title "Weekly sync" --folder meetings/
219
+ ```
220
+
221
+ Preset IDs: `blank`, `project-note`, `research-note`, `decision-note`,
222
+ `prompt-pattern`, `source-summary`, `idea-original`, `person`,
223
+ `company-project`, `meeting`.
224
+
225
+ For second-brain pages, prefer the typed presets:
226
+
227
+ - `idea-original`: exact idea phrasing, context, related concepts, publish potential.
228
+ - `person`: current state, relationship, assessment, open threads, timeline.
229
+ - `company-project`: state, changes, decisions, people, timeline.
230
+ - `meeting`: synthesis/action analysis above `## Timeline`; raw notes below.
231
+
232
+ The JSON receipt reports write, sync, and embed status separately. Generated
233
+ captures land under `inbox/YYYY-MM-DD/capture-<body-hash>.md` unless `--path`,
234
+ `--folder`, or `--title` overrides the path. Capture does not imply embedding
235
+ unless `embed.status` is `completed`. Capture inputs must be text; binary-like
236
+ file/stdin content is rejected before writing. CLI, REST, SDK, and Web capture
237
+ writes fail instead of replacing late-arriving files; legacy `overwrite` is
238
+ MCP-only.
239
+
240
+ Programmatic capture uses the same receipt contract:
241
+
242
+ - MCP: `gno_capture` (requires `gno mcp --enable-write`)
243
+ - REST: `POST /api/capture`
244
+ - SDK: `client.capture({ collection, content, source, tags })`
245
+
246
+ MCP capture writes structured `source:` frontmatter, runs under the MCP write
247
+ lock, syncs the file for FTS, and preserves legacy MCP fields (`docid`,
248
+ `absPath`, `overwritten`, `serverInstanceId`) alongside the shared receipt. It
249
+ does not auto-embed.
250
+
210
251
  ## Collection-specific embedding models
211
252
 
212
253
  Collections can override the global embedding model with `models.embed`.
@@ -111,6 +111,40 @@ Generate embeddings only.
111
111
  gno embed [--force] [--model <uri>] [--batch-size <n>] [--dry-run]
112
112
  ```
113
113
 
114
+ ## Capture
115
+
116
+ ### gno capture
117
+
118
+ Capture a note into an editable collection with provenance.
119
+
120
+ ```bash
121
+ gno capture "thought to remember"
122
+ gno capture --stdin --collection notes --preset source-summary --tags inbox,gno
123
+ gno capture --file ./clip.md --source-url https://example.com --source-kind web --json
124
+ gno capture "meeting note" --quiet
125
+ ```
126
+
127
+ Preset IDs: `blank`, `project-note`, `research-note`, `decision-note`,
128
+ `prompt-pattern`, `source-summary`, `idea-original`, `person`,
129
+ `company-project`, `meeting`.
130
+
131
+ Second-brain presets keep current synthesis above `## Timeline` and dated
132
+ evidence below it. Use `idea-original` for exact idea wording, `person` for
133
+ relationship/current-state notes, `company-project` for organizations or active
134
+ workstreams, and `meeting` for analysis above transcript/raw notes/action items.
135
+
136
+ Important behavior:
137
+
138
+ - Inline content, `--stdin`, and `--file` are mutually exclusive.
139
+ - Capture accepts text only; binary-like file/stdin content is rejected before
140
+ writing.
141
+ - Without `--path`, `--folder`, or `--title`, captures use
142
+ `inbox/YYYY-MM-DD/capture-<body-hash>.md` in UTC.
143
+ - Capture writes fail instead of replacing a late-arriving file.
144
+ - `--json` returns a capture receipt with separate write, sync, and embed status.
145
+ - Capture syncs the file into FTS but does not imply embedding unless
146
+ `embed.status` is `completed`.
147
+
114
148
  ## Search Commands
115
149
 
116
150
  ### gno search
@@ -349,3 +349,40 @@ gno search "common term" --min-score 0.7
349
349
  # Get full content
350
350
  gno search "term" --full
351
351
  ```
352
+
353
+ ## Capture Notes
354
+
355
+ ### CLI capture with provenance
356
+
357
+ ```bash
358
+ gno capture "thought to remember"
359
+ gno capture --file ./clip.md --source-url https://example.com --source-kind web --json
360
+ ```
361
+
362
+ ### Typed second-brain presets
363
+
364
+ ```bash
365
+ # Original idea: preserve exact phrasing and related concepts
366
+ gno capture --preset idea-original --title "Local-first inbox triage" --folder ideas/
367
+
368
+ # Person page: current synthesis plus relationship/open threads/timeline
369
+ gno capture --preset person --title "Jane Doe" --folder people/
370
+
371
+ # Company/project page: state, decisions, people, and timeline
372
+ gno capture --preset company-project --title "Acme renewal" --folder projects/
373
+
374
+ # Meeting page: analysis above raw notes, transcript, and action items
375
+ gno capture --preset meeting --title "Weekly sync" --folder meetings/
376
+ ```
377
+
378
+ For these presets, keep current synthesis above `## Timeline`; put dated evidence
379
+ and raw chronology below it. If `contentTypes` are configured in `index.yml`,
380
+ matching frontmatter `type` or path prefixes become `contentType` in JSON search
381
+ results.
382
+
383
+ ### Web UI quick capture
384
+
385
+ Press **N** in `gno serve`, write the note, and open **Source** only when you
386
+ need provenance fields such as URL, author, observed date, or external id. The
387
+ success view reports the write result, FTS sync state, and embed state
388
+ separately.
@@ -68,6 +68,27 @@ community summaries,
68
68
  Graph edges include confidence/audit metadata; prefer `explicit` edges when
69
69
  answers depend on link certainty.
70
70
 
71
+ ## Capture
72
+
73
+ `gno_capture` is available only when MCP starts with `--enable-write` or
74
+ `GNO_MCP_ENABLE_WRITE=1`. It writes quick notes with structured `source:`
75
+ frontmatter and returns the same provenance receipt shape as CLI, REST, and SDK
76
+ capture, plus legacy MCP fields (`docid`, `absPath`, `overwritten`,
77
+ `serverInstanceId`).
78
+
79
+ `presetId` accepts `blank`, `project-note`, `research-note`, `decision-note`,
80
+ `prompt-pattern`, `source-summary`, `idea-original`, `person`,
81
+ `company-project`, or `meeting`. The typed second-brain presets use flat
82
+ frontmatter (`type`, `category`, `tags`) and a synthesis/timeline page pattern;
83
+ provenance still comes from the capture `source` fields, not the preset.
84
+
85
+ Use `collisionPolicy: "open_existing"` to return an existing note without
86
+ rewriting, `create_with_suffix` to create the next available path, or legacy
87
+ `overwrite: true` to replace the target path. Capture content must be text, and
88
+ non-overwrite captures fail instead of replacing a late-arriving file. MCP
89
+ capture syncs the file for FTS but does not auto-embed; run `gno_embed` or
90
+ `gno_index` afterward when vector search should include it.
91
+
71
92
  ## Uninstall
72
93
 
73
94
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.7.1",
3
+ "version": "1.9.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
@@ -0,0 +1,282 @@
1
+ /**
2
+ * gno capture command implementation.
3
+ *
4
+ * @module src/cli/commands/capture
5
+ */
6
+
7
+ // node:fs/promises for mkdir (no Bun equivalent for recursive dir creation)
8
+ import { mkdir } from "node:fs/promises";
9
+ // node:path has no Bun path utilities
10
+ import { dirname, join } from "node:path";
11
+
12
+ import type {
13
+ CapturePlan,
14
+ CaptureReceipt,
15
+ CaptureSourceKind,
16
+ } from "../../core/capture";
17
+ import type { NoteCollisionPolicy } from "../../core/note-creation";
18
+ import type { NotePresetId } from "../../core/note-presets";
19
+
20
+ import { getIndexDbPath } from "../../app/constants";
21
+ import {
22
+ buildCaptureReceipt,
23
+ listCaptureDiskRelPaths,
24
+ planCapture,
25
+ serializeCaptureReceipt,
26
+ } from "../../core/capture";
27
+ import { writeCapturePlanFile } from "../../core/capture-write";
28
+ import { withWriteLock } from "../../core/file-lock";
29
+ import { defaultSyncService, withContentTypeRules } from "../../ingestion";
30
+ import { CliError } from "../errors";
31
+ import { initStore } from "./shared";
32
+
33
+ export interface CaptureCliOptions {
34
+ configPath?: string;
35
+ indexName?: string;
36
+ inlineContent?: string;
37
+ stdin?: boolean;
38
+ file?: string;
39
+ collection?: string;
40
+ title?: string;
41
+ path?: string;
42
+ folder?: string;
43
+ preset?: NotePresetId;
44
+ tags?: string;
45
+ collisionPolicy?: NoteCollisionPolicy;
46
+ sourceKind?: CaptureSourceKind;
47
+ sourceUrl?: string;
48
+ sourceTitle?: string;
49
+ sourceAuthor?: string;
50
+ sourceDate?: string;
51
+ sourceId?: string;
52
+ }
53
+
54
+ const SOURCE_KINDS = new Set<CaptureSourceKind>([
55
+ "direct",
56
+ "web",
57
+ "email",
58
+ "meeting",
59
+ "chat",
60
+ "file",
61
+ "api",
62
+ "unknown",
63
+ ]);
64
+ const COLLISION_POLICIES = new Set<NoteCollisionPolicy>([
65
+ "error",
66
+ "open_existing",
67
+ "create_with_suffix",
68
+ ]);
69
+
70
+ function parseTags(raw: string | undefined): string[] {
71
+ return (
72
+ raw
73
+ ?.split(",")
74
+ .map((tag) => tag.trim())
75
+ .filter((tag) => tag.length > 0) ?? []
76
+ );
77
+ }
78
+
79
+ async function readContent(
80
+ options: CaptureCliOptions
81
+ ): Promise<string | undefined> {
82
+ const sources = [
83
+ options.inlineContent?.trim().length ? "inline" : null,
84
+ options.stdin ? "stdin" : null,
85
+ options.file ? "file" : null,
86
+ ].filter((source) => source !== null);
87
+ if (sources.length > 1) {
88
+ throw new CliError(
89
+ "VALIDATION",
90
+ "Use only one content source: inline, --stdin, or --file."
91
+ );
92
+ }
93
+
94
+ if (options.stdin) {
95
+ return await Bun.stdin.text();
96
+ }
97
+ if (options.file) {
98
+ return await Bun.file(options.file).text();
99
+ }
100
+ return options.inlineContent;
101
+ }
102
+
103
+ function buildSource(options: CaptureCliOptions) {
104
+ if (options.sourceKind && !SOURCE_KINDS.has(options.sourceKind)) {
105
+ throw new CliError(
106
+ "VALIDATION",
107
+ "--source-kind must be one of: direct, web, email, meeting, chat, file, api, unknown"
108
+ );
109
+ }
110
+ return {
111
+ kind: options.sourceKind,
112
+ url: options.sourceUrl,
113
+ title: options.sourceTitle,
114
+ author: options.sourceAuthor,
115
+ observedAt: options.sourceDate,
116
+ externalId: options.sourceId,
117
+ };
118
+ }
119
+
120
+ function validateCollisionPolicy(
121
+ policy: NoteCollisionPolicy | undefined
122
+ ): void {
123
+ if (policy && !COLLISION_POLICIES.has(policy)) {
124
+ throw new CliError(
125
+ "VALIDATION",
126
+ "--collision-policy must be one of: error, open_existing, create_with_suffix"
127
+ );
128
+ }
129
+ }
130
+
131
+ export async function capture(
132
+ options: CaptureCliOptions
133
+ ): Promise<CaptureReceipt> {
134
+ const storeInit = await initStore({
135
+ configPath: options.configPath,
136
+ indexName: options.indexName,
137
+ syncConfig: true,
138
+ });
139
+ if (!storeInit.ok) {
140
+ throw new CliError("VALIDATION", storeInit.error);
141
+ }
142
+
143
+ const { store, collections, config } = storeInit;
144
+ try {
145
+ const collectionName = options.collection?.trim();
146
+ const collection = collectionName
147
+ ? collections.find(
148
+ (candidate) =>
149
+ candidate.name.toLowerCase() === collectionName.toLowerCase()
150
+ )
151
+ : collections[0];
152
+ if (!collection) {
153
+ throw new CliError(
154
+ "VALIDATION",
155
+ collectionName
156
+ ? `Collection not found: ${collectionName}`
157
+ : "No editable collection configured."
158
+ );
159
+ }
160
+
161
+ validateCollisionPolicy(options.collisionPolicy);
162
+ const content = await readContent(options);
163
+ const existingDocs = await store.listDocuments(collection.name);
164
+ if (!existingDocs.ok) {
165
+ throw new Error(existingDocs.error.message);
166
+ }
167
+ const diskRelPaths = await listCaptureDiskRelPaths(collection.path);
168
+ let plan: CapturePlan;
169
+ try {
170
+ plan = planCapture({
171
+ input: {
172
+ collection: collection.name,
173
+ content,
174
+ title: options.title,
175
+ relPath: options.path,
176
+ folderPath: options.folder,
177
+ collisionPolicy: options.collisionPolicy,
178
+ presetId: options.preset,
179
+ tags: parseTags(options.tags),
180
+ source: buildSource(options),
181
+ },
182
+ existingRelPaths: existingDocs.value.map((doc) => doc.relPath),
183
+ diskRelPaths,
184
+ });
185
+ } catch (error) {
186
+ throw new CliError(
187
+ "VALIDATION",
188
+ error instanceof Error ? error.message : String(error)
189
+ );
190
+ }
191
+ const absPath = join(collection.path, plan.relPath);
192
+
193
+ const lockPath = join(
194
+ dirname(getIndexDbPath(options.indexName)),
195
+ ".mcp-write.lock"
196
+ );
197
+ return await withWriteLock(lockPath, async () => {
198
+ if (plan.openedExisting) {
199
+ const existingDoc = await store.getDocument(
200
+ collection.name,
201
+ plan.relPath
202
+ );
203
+ if (!existingDoc.ok) {
204
+ throw new Error(existingDoc.error.message);
205
+ }
206
+ return buildCaptureReceipt({
207
+ plan,
208
+ absPath,
209
+ docid: existingDoc.value?.docid,
210
+ sync: existingDoc.value
211
+ ? { status: "completed" }
212
+ : {
213
+ status: "skipped",
214
+ reason: "Existing file is not indexed yet.",
215
+ },
216
+ });
217
+ }
218
+
219
+ await mkdir(dirname(absPath), { recursive: true });
220
+ await writeCapturePlanFile(plan, absPath);
221
+ const syncResults = await defaultSyncService.syncFiles(
222
+ collection,
223
+ store,
224
+ [plan.relPath],
225
+ withContentTypeRules(
226
+ {
227
+ runUpdateCmd: false,
228
+ gitPull: false,
229
+ },
230
+ config
231
+ )
232
+ );
233
+ const syncResult = syncResults[0];
234
+ const docResult = await store.getDocument(collection.name, plan.relPath);
235
+ const docid = docResult.ok ? docResult.value?.docid : undefined;
236
+ return buildCaptureReceipt({
237
+ plan,
238
+ absPath,
239
+ docid: syncResult?.docid ?? docid,
240
+ sync:
241
+ syncResult?.status === "error"
242
+ ? {
243
+ status: "failed",
244
+ error:
245
+ syncResult.errorMessage ??
246
+ syncResult.errorCode ??
247
+ "Unknown sync error",
248
+ }
249
+ : { status: "completed" },
250
+ });
251
+ });
252
+ } finally {
253
+ await store.close();
254
+ }
255
+ }
256
+
257
+ export function formatCaptureReceipt(
258
+ receipt: CaptureReceipt,
259
+ options: { json?: boolean; quiet?: boolean } = {}
260
+ ): string {
261
+ if (options.json) {
262
+ return serializeCaptureReceipt(receipt);
263
+ }
264
+ if (options.quiet) {
265
+ return receipt.uri;
266
+ }
267
+
268
+ const lines = [
269
+ receipt.openedExisting ? "Opened existing capture." : "Captured note.",
270
+ `URI: ${receipt.uri}`,
271
+ `Path: ${receipt.absPath ?? receipt.relPath}`,
272
+ `Sync: ${receipt.sync.status}`,
273
+ `Embed: ${receipt.embed.status}`,
274
+ ];
275
+ if (receipt.tags.length > 0) {
276
+ lines.push(`Tags: ${receipt.tags.join(", ")}`);
277
+ }
278
+ if (receipt.source.url) {
279
+ lines.push(`Source: ${receipt.source.url}`);
280
+ }
281
+ return lines.join("\n");
282
+ }
@@ -5,7 +5,11 @@
5
5
  * @module src/cli/commands/indexCmd
6
6
  */
7
7
 
8
- import { defaultSyncService, type SyncResult } from "../../ingestion";
8
+ import {
9
+ defaultSyncService,
10
+ type SyncResult,
11
+ withContentTypeRules,
12
+ } from "../../ingestion";
9
13
  import { formatSyncResultLines, initStore } from "./shared";
10
14
 
11
15
  /**
@@ -55,14 +59,21 @@ export async function index(options: IndexOptions = {}): Promise<IndexResult> {
55
59
  return { success: false, error: initResult.error };
56
60
  }
57
61
 
58
- const { store, collections } = initResult;
62
+ const { store, collections, config } = initResult;
59
63
 
60
64
  try {
61
65
  // Run sync service (update phase)
62
- const syncResult = await defaultSyncService.syncAll(collections, store, {
63
- gitPull: options.gitPull,
64
- runUpdateCmd: true,
65
- });
66
+ const syncResult = await defaultSyncService.syncAll(
67
+ collections,
68
+ store,
69
+ withContentTypeRules(
70
+ {
71
+ gitPull: options.gitPull,
72
+ runUpdateCmd: true,
73
+ },
74
+ config
75
+ )
76
+ );
66
77
 
67
78
  // Embedding phase
68
79
  const embedSkipped = options.noEmbed ?? false;
@@ -10,7 +10,12 @@ import type { SyncResult } from "../../ingestion";
10
10
  import type { SearchResults } from "../../pipeline/types";
11
11
 
12
12
  import { decorateUriForIndex, getIndexDbPath } from "../../app/constants";
13
- import { getConfigPaths, isInitialized, loadConfig } from "../../config";
13
+ import {
14
+ getConfigPaths,
15
+ isInitialized,
16
+ loadConfig,
17
+ writeConfigWarningsToStderr,
18
+ } from "../../config";
14
19
  import { SqliteAdapter } from "../../store/sqlite/adapter";
15
20
 
16
21
  /**
@@ -61,6 +66,7 @@ export async function initStore(
61
66
  if (!configResult.ok) {
62
67
  return { ok: false, error: configResult.error.message };
63
68
  }
69
+ writeConfigWarningsToStderr(configResult.warnings);
64
70
  const config = configResult.value;
65
71
 
66
72
  // Filter to single collection if specified
@@ -165,6 +171,16 @@ export function formatSyncResultLines(
165
171
  lines.push(` [${err.code}] ${err.relPath}: ${err.message}`);
166
172
  }
167
173
  }
174
+ if (options.verbose && c.files?.length) {
175
+ for (const file of c.files) {
176
+ if (!file.contentType || !file.contentTypeSource) {
177
+ continue;
178
+ }
179
+ lines.push(
180
+ ` [${file.status}] ${file.relPath}: contentType=${file.contentType} (${file.contentTypeSource})`
181
+ );
182
+ }
183
+ }
168
184
  }
169
185
 
170
186
  lines.push("");
@@ -5,7 +5,11 @@
5
5
  * @module src/cli/commands/update
6
6
  */
7
7
 
8
- import { defaultSyncService, type SyncResult } from "../../ingestion";
8
+ import {
9
+ defaultSyncService,
10
+ type SyncResult,
11
+ withContentTypeRules,
12
+ } from "../../ingestion";
9
13
  import { formatSyncResultLines, initStore } from "./shared";
10
14
 
11
15
  /**
@@ -43,14 +47,21 @@ export async function update(
43
47
  return { success: false, error: initResult.error };
44
48
  }
45
49
 
46
- const { store, collections } = initResult;
50
+ const { store, collections, config } = initResult;
47
51
 
48
52
  try {
49
53
  // Run sync service
50
- const result = await defaultSyncService.syncAll(collections, store, {
51
- gitPull: options.gitPull,
52
- runUpdateCmd: true,
53
- });
54
+ const result = await defaultSyncService.syncAll(
55
+ collections,
56
+ store,
57
+ withContentTypeRules(
58
+ {
59
+ gitPull: options.gitPull,
60
+ runUpdateCmd: true,
61
+ },
62
+ config
63
+ )
64
+ );
54
65
 
55
66
  return { success: true, result };
56
67
  } finally {
@@ -38,6 +38,7 @@ export const CMD = {
38
38
  backlinks: "backlinks",
39
39
  similar: "similar",
40
40
  graph: "graph",
41
+ capture: "capture",
41
42
  } as const;
42
43
 
43
44
  export type CommandId = (typeof CMD)[keyof typeof CMD];
@@ -63,6 +64,7 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
63
64
  [CMD.similar]: ["terminal", "json", "md"],
64
65
  // graph uses custom --dot/--mermaid flags (not OutputFormat) and writes via terminal output
65
66
  [CMD.graph]: ["json", "terminal"],
67
+ [CMD.capture]: ["terminal", "json"],
66
68
  };
67
69
 
68
70
  // ─────────────────────────────────────────────────────────────────────────────