@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
@@ -227,6 +227,8 @@ export async function searchVectorWithEmbedding(
227
227
  score,
228
228
  uri: doc.uri,
229
229
  title: doc.title ?? undefined,
230
+ contentType: doc.contentType ?? undefined,
231
+ categories: doc.categories ?? undefined,
230
232
  line: chunk.startLine,
231
233
  snippet: chunk.text,
232
234
  snippetLanguage: chunk.language ?? undefined,
@@ -281,6 +283,8 @@ export async function searchVectorWithEmbedding(
281
283
  score,
282
284
  uri: doc.uri,
283
285
  title: doc.title ?? undefined,
286
+ contentType: doc.contentType ?? undefined,
287
+ categories: doc.categories ?? undefined,
284
288
  line: chunk.startLine,
285
289
  snippet: fullContent ?? chunk.text,
286
290
  snippetLanguage: chunk.language ?? undefined,
package/src/sdk/client.ts CHANGED
@@ -15,6 +15,8 @@ import type { IndexStatus, StoreResult } from "../store/types";
15
15
  import type { VectorIndexPort } from "../store/vector";
16
16
  import type {
17
17
  GnoAskOptions,
18
+ GnoCaptureOptions,
19
+ GnoCaptureResult,
18
20
  GnoClient,
19
21
  GnoCreateFolderOptions,
20
22
  GnoCreateFolderResult,
@@ -42,7 +44,18 @@ import {
42
44
  getIndexDbPath,
43
45
  parseUri,
44
46
  } from "../app/constants";
45
- import { ConfigSchema, loadConfig } from "../config";
47
+ import {
48
+ ConfigSchema,
49
+ loadConfig,
50
+ normalizeConfigContentTypes,
51
+ } from "../config";
52
+ import {
53
+ buildCaptureReceipt,
54
+ type CapturePlan,
55
+ listCaptureDiskRelPaths,
56
+ planCapture,
57
+ } from "../core/capture";
58
+ import { writeCapturePlanFile } from "../core/capture-write";
46
59
  import {
47
60
  atomicWrite,
48
61
  copyFilePath,
@@ -61,7 +74,11 @@ import { resolveNotePreset } from "../core/note-presets";
61
74
  import { extractSections } from "../core/sections";
62
75
  import { normalizeStructuredQueryInput } from "../core/structured-query";
63
76
  import { parseAndValidateTagFilter } from "../core/tags";
64
- import { defaultSyncService, type SyncResult } from "../ingestion";
77
+ import {
78
+ defaultSyncService,
79
+ type SyncResult,
80
+ withContentTypeRules,
81
+ } from "../ingestion";
65
82
  import { updateFrontmatterTags } from "../ingestion/frontmatter";
66
83
  import { LlmAdapter } from "../llm/nodeLlamaCpp/adapter";
67
84
  import { resolveDownloadPolicy } from "../llm/policy";
@@ -132,7 +149,7 @@ async function resolveClientState(
132
149
  parsed.error.issues[0]?.message ?? "Invalid config"
133
150
  );
134
151
  }
135
- config = parsed.data;
152
+ config = normalizeConfigContentTypes(parsed.data).config;
136
153
  configPath = null;
137
154
  configSource = "inline";
138
155
  } else {
@@ -686,10 +703,17 @@ class GnoClientImpl implements GnoClient {
686
703
  async update(options: GnoUpdateOptions = {}): Promise<SyncResult> {
687
704
  this.assertOpen();
688
705
  const collections = this.getCollections(options.collection);
689
- return defaultSyncService.syncAll(collections, this.store, {
690
- gitPull: options.gitPull,
691
- runUpdateCmd: true,
692
- });
706
+ return defaultSyncService.syncAll(
707
+ collections,
708
+ this.store,
709
+ withContentTypeRules(
710
+ {
711
+ gitPull: options.gitPull,
712
+ runUpdateCmd: true,
713
+ },
714
+ this.config
715
+ )
716
+ );
693
717
  }
694
718
 
695
719
  async embed(options: GnoEmbedOptions = {}): Promise<GnoEmbedResult> {
@@ -799,10 +823,13 @@ class GnoClientImpl implements GnoClient {
799
823
  collection,
800
824
  this.store,
801
825
  [plan.relPath],
802
- {
803
- runUpdateCmd: false,
804
- gitPull: false,
805
- }
826
+ withContentTypeRules(
827
+ {
828
+ runUpdateCmd: false,
829
+ gitPull: false,
830
+ },
831
+ this.config
832
+ )
806
833
  );
807
834
  const syncResult = syncResults[0];
808
835
  if (!syncResult || syncResult.status === "error") {
@@ -822,6 +849,109 @@ class GnoClientImpl implements GnoClient {
822
849
  };
823
850
  }
824
851
 
852
+ async capture(options: GnoCaptureOptions): Promise<GnoCaptureResult> {
853
+ this.assertOpen();
854
+ const collection = this.getCollections(options.collection)[0];
855
+ if (!collection) {
856
+ throw sdkError(
857
+ "VALIDATION",
858
+ `Collection not found: ${options.collection}`
859
+ );
860
+ }
861
+
862
+ const existingList = await this.store.listDocuments(collection.name);
863
+ if (!existingList.ok) {
864
+ throw sdkError("STORE", existingList.error.message, {
865
+ cause: existingList.error.cause,
866
+ });
867
+ }
868
+ const { overwrite: _unsupportedOverwrite, ...captureOptions } =
869
+ options as GnoCaptureOptions & { overwrite?: unknown };
870
+ if (_unsupportedOverwrite !== undefined) {
871
+ throw sdkError(
872
+ "VALIDATION",
873
+ "overwrite is not supported by client.capture(); use collisionPolicy instead"
874
+ );
875
+ }
876
+
877
+ let plan: CapturePlan;
878
+ try {
879
+ plan = planCapture({
880
+ input: {
881
+ ...captureOptions,
882
+ collection: collection.name,
883
+ },
884
+ existingRelPaths: existingList.value.map((doc) => doc.relPath),
885
+ diskRelPaths: await listCaptureDiskRelPaths(collection.path),
886
+ });
887
+ } catch (error) {
888
+ throw sdkError(
889
+ "VALIDATION",
890
+ error instanceof Error ? error.message : String(error)
891
+ );
892
+ }
893
+
894
+ const fullPath = `${collection.path}/${plan.relPath}`;
895
+ if (plan.openedExisting) {
896
+ const existingDoc = await this.store.getDocument(
897
+ collection.name,
898
+ plan.relPath
899
+ );
900
+ if (!existingDoc.ok) {
901
+ throw sdkError("STORE", existingDoc.error.message, {
902
+ cause: existingDoc.error.cause,
903
+ });
904
+ }
905
+ return buildCaptureReceipt({
906
+ plan,
907
+ absPath: fullPath,
908
+ docid: existingDoc.value?.docid,
909
+ sync: existingDoc.value
910
+ ? { status: "completed" }
911
+ : {
912
+ status: "skipped",
913
+ reason: "Existing file is not indexed yet.",
914
+ },
915
+ });
916
+ }
917
+
918
+ await mkdir(dirname(fullPath), { recursive: true });
919
+ await writeCapturePlanFile(plan, fullPath);
920
+ const syncResults = await defaultSyncService.syncFiles(
921
+ collection,
922
+ this.store,
923
+ [plan.relPath],
924
+ withContentTypeRules(
925
+ {
926
+ runUpdateCmd: false,
927
+ gitPull: false,
928
+ },
929
+ this.config
930
+ )
931
+ );
932
+ const syncResult = syncResults[0];
933
+ const docResult = await this.store.getDocument(
934
+ collection.name,
935
+ plan.relPath
936
+ );
937
+ const docid = docResult.ok ? docResult.value?.docid : undefined;
938
+ return buildCaptureReceipt({
939
+ plan,
940
+ absPath: fullPath,
941
+ docid: syncResult?.docid ?? docid,
942
+ sync:
943
+ syncResult?.status === "error"
944
+ ? {
945
+ status: "failed",
946
+ error:
947
+ syncResult.errorMessage ??
948
+ syncResult.errorCode ??
949
+ "Unknown sync error",
950
+ }
951
+ : { status: "completed" },
952
+ });
953
+ }
954
+
825
955
  async createFolder(
826
956
  options: GnoCreateFolderOptions
827
957
  ): Promise<GnoCreateFolderResult> {
@@ -878,9 +1008,11 @@ class GnoClientImpl implements GnoClient {
878
1008
  const currentPath = `${collection.path}/${storedDoc.relPath}`;
879
1009
  const nextPath = `${collection.path}/${plan.nextRelPath}`;
880
1010
  await renameFilePath(currentPath, nextPath);
881
- await defaultSyncService.syncCollection(collection, this.store, {
882
- runUpdateCmd: false,
883
- });
1011
+ await defaultSyncService.syncCollection(
1012
+ collection,
1013
+ this.store,
1014
+ withContentTypeRules({ runUpdateCmd: false }, this.config)
1015
+ );
884
1016
  const linksResult = await this.store.getLinksForDoc(storedDoc.id);
885
1017
  const backlinksResult = await this.store.getBacklinksForDoc(storedDoc.id);
886
1018
  if (!linksResult.ok || !backlinksResult.ok) {
@@ -935,9 +1067,11 @@ class GnoClientImpl implements GnoClient {
935
1067
  const nextPath = `${collection.path}/${plan.nextRelPath}`;
936
1068
  await mkdir(dirname(nextPath), { recursive: true });
937
1069
  await renameFilePath(currentPath, nextPath);
938
- await defaultSyncService.syncCollection(collection, this.store, {
939
- runUpdateCmd: false,
940
- });
1070
+ await defaultSyncService.syncCollection(
1071
+ collection,
1072
+ this.store,
1073
+ withContentTypeRules({ runUpdateCmd: false }, this.config)
1074
+ );
941
1075
  const linksResult = await this.store.getLinksForDoc(storedDoc.id);
942
1076
  const backlinksResult = await this.store.getBacklinksForDoc(storedDoc.id);
943
1077
  if (!linksResult.ok || !backlinksResult.ok) {
@@ -1004,9 +1138,11 @@ class GnoClientImpl implements GnoClient {
1004
1138
  const nextPath = `${collection.path}/${plan.nextRelPath}`;
1005
1139
  await mkdir(dirname(nextPath), { recursive: true });
1006
1140
  await copyFilePath(currentPath, nextPath);
1007
- await defaultSyncService.syncCollection(collection, this.store, {
1008
- runUpdateCmd: false,
1009
- });
1141
+ await defaultSyncService.syncCollection(
1142
+ collection,
1143
+ this.store,
1144
+ withContentTypeRules({ runUpdateCmd: false }, this.config)
1145
+ );
1010
1146
  const linksResult = await this.store.getLinksForDoc(storedDoc.id);
1011
1147
  const backlinksResult = await this.store.getBacklinksForDoc(storedDoc.id);
1012
1148
  if (!linksResult.ok || !backlinksResult.ok) {
package/src/sdk/index.ts CHANGED
@@ -29,6 +29,8 @@ export { GnoSdkError, sdkError } from "./errors";
29
29
  export { createGnoClient } from "./client";
30
30
  export type {
31
31
  GnoAskOptions,
32
+ GnoCaptureOptions,
33
+ GnoCaptureResult,
32
34
  GnoClient,
33
35
  GnoClientInitOptions,
34
36
  GnoEmbedOptions,
package/src/sdk/types.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Config } from "../config/types";
8
+ import type { CaptureInput, CaptureReceipt } from "../core/capture";
8
9
  import type { NoteCollisionPolicy } from "../core/note-creation";
9
10
  import type { NotePresetId } from "../core/note-presets";
10
11
  import type { DocumentSection } from "../core/sections";
@@ -130,6 +131,10 @@ export interface GnoCreateNoteResult {
130
131
  createdWithSuffix?: boolean;
131
132
  }
132
133
 
134
+ export interface GnoCaptureOptions extends Omit<CaptureInput, "overwrite"> {}
135
+
136
+ export type GnoCaptureResult = CaptureReceipt;
137
+
133
138
  export interface GnoCreateFolderOptions {
134
139
  collection: string;
135
140
  name: string;
@@ -194,6 +199,7 @@ export interface GnoClient {
194
199
  update(options?: GnoUpdateOptions): Promise<SyncResult>;
195
200
  embed(options?: GnoEmbedOptions): Promise<GnoEmbedResult>;
196
201
  index(options?: GnoIndexOptions): Promise<GnoIndexResult>;
202
+ capture(options: GnoCaptureOptions): Promise<GnoCaptureResult>;
197
203
  createNote(options: GnoCreateNoteOptions): Promise<GnoCreateNoteResult>;
198
204
  createFolder(options: GnoCreateFolderOptions): Promise<GnoCreateFolderResult>;
199
205
  renameNote(options: GnoRenameNoteOptions): Promise<GnoRefactorNoteResult>;
@@ -11,11 +11,13 @@ import type {
11
11
  import { getIndexDbPath } from "../app/constants";
12
12
  import {
13
13
  ensureDirectories,
14
+ formatConfigWarnings,
14
15
  getConfigPaths,
15
16
  isInitialized,
16
17
  loadConfig,
17
18
  } from "../config";
18
19
  import { defaultSyncService } from "../ingestion";
20
+ import { withContentTypeRules } from "../ingestion";
19
21
  import { getActivePreset } from "../llm/registry";
20
22
  import { SqliteAdapter } from "../store/sqlite/adapter";
21
23
  import {
@@ -79,6 +81,7 @@ type BackgroundRuntimeDeps = {
79
81
  scheduler: EmbedScheduler | null;
80
82
  eventBus?: DocumentEventBus | null;
81
83
  callbacks?: CollectionWatchCallbacks;
84
+ syncOptions?: Parameters<typeof withContentTypeRules>[0];
82
85
  }) => CollectionWatchService;
83
86
  };
84
87
 
@@ -103,6 +106,9 @@ export async function startBackgroundRuntime(
103
106
  if (!configResult.ok) {
104
107
  return { success: false, error: configResult.error.message };
105
108
  }
109
+ for (const warning of formatConfigWarnings(configResult.warnings)) {
110
+ console.warn(warning);
111
+ }
106
112
  const config = configResult.value;
107
113
 
108
114
  if (options.requireCollections && config.collections.length === 0) {
@@ -171,6 +177,7 @@ export async function startBackgroundRuntime(
171
177
  scheduler,
172
178
  eventBus: options.eventBus ?? null,
173
179
  callbacks: options.watchCallbacks,
180
+ syncOptions: withContentTypeRules({}, config),
174
181
  });
175
182
  watchService.start();
176
183
  ctxHolder.watchService = watchService;
@@ -189,10 +196,17 @@ export async function startBackgroundRuntime(
189
196
  eventBus: options.eventBus ?? null,
190
197
  watchService,
191
198
  async syncAll(syncOptions = {}) {
192
- const syncResult = await syncAllService(config.collections, store, {
193
- gitPull: syncOptions.gitPull,
194
- runUpdateCmd: syncOptions.runUpdateCmd,
195
- });
199
+ const syncResult = await syncAllService(
200
+ config.collections,
201
+ store,
202
+ withContentTypeRules(
203
+ {
204
+ gitPull: syncOptions.gitPull,
205
+ runUpdateCmd: syncOptions.runUpdateCmd,
206
+ },
207
+ config
208
+ )
209
+ );
196
210
 
197
211
  let embedResult: EmbedResult | null = null;
198
212
  if (syncOptions.triggerEmbed !== false) {
@@ -14,6 +14,7 @@ import {
14
14
  type ApplyConfigResult,
15
15
  type MutationResult,
16
16
  } from "../core/config-mutation";
17
+ import { withContentTypeRules } from "../ingestion";
17
18
 
18
19
  /**
19
20
  * Apply a config mutation atomically with serialization.
@@ -53,7 +54,10 @@ export async function applyConfigChange(
53
54
  );
54
55
 
55
56
  if (result.ok) {
56
- ctxHolder.watchService?.updateCollections(ctxHolder.config.collections);
57
+ ctxHolder.watchService?.updateCollections(
58
+ ctxHolder.config.collections,
59
+ withContentTypeRules({}, ctxHolder.config)
60
+ );
57
61
  }
58
62
 
59
63
  return result;
@@ -78,7 +82,10 @@ export async function applyConfigChangeTyped<T>(
78
82
  );
79
83
 
80
84
  if (result.ok) {
81
- ctxHolder.watchService?.updateCollections(ctxHolder.config.collections);
85
+ ctxHolder.watchService?.updateCollections(
86
+ ctxHolder.config.collections,
87
+ withContentTypeRules({}, ctxHolder.config)
88
+ );
82
89
  }
83
90
 
84
91
  return result;