@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
@@ -247,6 +247,7 @@ export function createProgram(): Command {
247
247
  // Wire command groups
248
248
  wireSearchCommands(program);
249
249
  wireOnboardingCommands(program);
250
+ wireCaptureCommand(program);
250
251
  wireManagementCommands(program);
251
252
  wirePublishCommand(program);
252
253
  wireVecCommands(program);
@@ -1009,6 +1010,69 @@ function wireOnboardingCommands(program: Command): void {
1009
1010
  });
1010
1011
  }
1011
1012
 
1013
+ function wireCaptureCommand(program: Command): void {
1014
+ program
1015
+ .command("capture [content...]")
1016
+ .description("Capture a note with provenance")
1017
+ .option("--stdin", "read capture content from stdin")
1018
+ .option("--file <path>", "read capture content from a text/markdown file")
1019
+ .option("-c, --collection <name>", "target collection")
1020
+ .option("--title <title>", "capture title")
1021
+ .option("--path <relPath>", "explicit relative path inside collection")
1022
+ .option("--folder <relPath>", "folder path inside collection")
1023
+ .option("--preset <id>", "note preset scaffold")
1024
+ .option("--tags <tags>", "comma-separated tags")
1025
+ .option(
1026
+ "--collision-policy <policy>",
1027
+ "error, open_existing, or create_with_suffix"
1028
+ )
1029
+ .option(
1030
+ "--source-kind <kind>",
1031
+ "direct, web, email, meeting, chat, file, api, or unknown"
1032
+ )
1033
+ .option("--source-url <url>", "source URL")
1034
+ .option("--source-title <title>", "source title")
1035
+ .option("--source-author <author>", "source author")
1036
+ .option("--source-date <date>", "source observed date/time")
1037
+ .option("--source-id <id>", "source external id")
1038
+ .option("--json", "JSON output")
1039
+ .action(
1040
+ async (contentParts: string[], cmdOpts: Record<string, unknown>) => {
1041
+ const format = getFormat(cmdOpts);
1042
+ assertFormatSupported(CMD.capture, format);
1043
+ const globals = getGlobals();
1044
+ const { capture, formatCaptureReceipt } =
1045
+ await import("./commands/capture");
1046
+ const receipt = await capture({
1047
+ configPath: globals.config,
1048
+ indexName: globals.index,
1049
+ inlineContent:
1050
+ contentParts.length > 0 ? contentParts.join(" ") : undefined,
1051
+ stdin: Boolean(cmdOpts.stdin),
1052
+ file: cmdOpts.file as string | undefined,
1053
+ collection: cmdOpts.collection as string | undefined,
1054
+ title: cmdOpts.title as string | undefined,
1055
+ path: cmdOpts.path as string | undefined,
1056
+ folder: cmdOpts.folder as string | undefined,
1057
+ preset: cmdOpts.preset as never,
1058
+ tags: cmdOpts.tags as string | undefined,
1059
+ collisionPolicy: cmdOpts.collisionPolicy as never,
1060
+ sourceKind: cmdOpts.sourceKind as never,
1061
+ sourceUrl: cmdOpts.sourceUrl as string | undefined,
1062
+ sourceTitle: cmdOpts.sourceTitle as string | undefined,
1063
+ sourceAuthor: cmdOpts.sourceAuthor as string | undefined,
1064
+ sourceDate: cmdOpts.sourceDate as string | undefined,
1065
+ sourceId: cmdOpts.sourceId as string | undefined,
1066
+ });
1067
+ const output = formatCaptureReceipt(receipt, {
1068
+ json: format === "json",
1069
+ quiet: globals.quiet,
1070
+ });
1071
+ await writeOutput(output, format);
1072
+ }
1073
+ );
1074
+ }
1075
+
1012
1076
  // ─────────────────────────────────────────────────────────────────────────────
1013
1077
  // Retrieval Commands (get, multi-get, ls)
1014
1078
  // ─────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Content type config normalization.
3
+ *
4
+ * @module src/config/content-types
5
+ */
6
+
7
+ import type { NotePresetId } from "../core/note-presets";
8
+ import type { Config, ContentTypeConfig } from "./types";
9
+
10
+ import { NOTE_PRESETS } from "../core/note-presets";
11
+
12
+ export type ConfigWarningCode =
13
+ | "UNKNOWN_CONTENT_TYPE_PRESET"
14
+ | "DUPLICATE_CONTENT_TYPE_PREFIX";
15
+
16
+ export interface ConfigWarning {
17
+ code: ConfigWarningCode;
18
+ message: string;
19
+ path: string;
20
+ }
21
+
22
+ export interface NormalizedContentTypeRule extends ContentTypeConfig {
23
+ preset: NotePresetId;
24
+ }
25
+
26
+ export interface ContentTypeNormalizationResult {
27
+ rules: NormalizedContentTypeRule[];
28
+ warnings: ConfigWarning[];
29
+ }
30
+
31
+ export interface ConfigNormalizationResult {
32
+ config: Config;
33
+ warnings: ConfigWarning[];
34
+ }
35
+
36
+ const NOTE_PRESET_IDS = new Set<NotePresetId>(
37
+ NOTE_PRESETS.map((preset) => preset.id)
38
+ );
39
+
40
+ function isNotePresetId(value: string): value is NotePresetId {
41
+ return NOTE_PRESET_IDS.has(value as NotePresetId);
42
+ }
43
+
44
+ export function normalizeContentTypes(
45
+ contentTypes: ContentTypeConfig[]
46
+ ): ContentTypeNormalizationResult {
47
+ const warnings: ConfigWarning[] = [];
48
+ const seenPrefixes = new Set<string>();
49
+ const rules: NormalizedContentTypeRule[] = [];
50
+
51
+ for (const [typeIndex, contentType] of contentTypes.entries()) {
52
+ const path = `contentTypes[${typeIndex}]`;
53
+ if (!isNotePresetId(contentType.preset)) {
54
+ warnings.push({
55
+ code: "UNKNOWN_CONTENT_TYPE_PRESET",
56
+ path: `${path}.preset`,
57
+ message: `Dropped content type "${contentType.id}" because preset "${contentType.preset}" is not a known note preset.`,
58
+ });
59
+ continue;
60
+ }
61
+
62
+ const prefixes: string[] = [];
63
+ for (const [prefixIndex, prefix] of contentType.prefixes.entries()) {
64
+ if (seenPrefixes.has(prefix)) {
65
+ warnings.push({
66
+ code: "DUPLICATE_CONTENT_TYPE_PREFIX",
67
+ path: `${path}.prefixes[${prefixIndex}]`,
68
+ message: `Dropped duplicate content type prefix "${prefix}" from "${contentType.id}".`,
69
+ });
70
+ continue;
71
+ }
72
+
73
+ seenPrefixes.add(prefix);
74
+ prefixes.push(prefix);
75
+ }
76
+
77
+ if (prefixes.length === 0) {
78
+ rules.push({
79
+ ...contentType,
80
+ preset: contentType.preset,
81
+ prefixes,
82
+ });
83
+ continue;
84
+ }
85
+
86
+ rules.push({
87
+ ...contentType,
88
+ preset: contentType.preset,
89
+ prefixes: [...prefixes].sort((a, b) => b.length - a.length),
90
+ });
91
+ }
92
+
93
+ rules.sort((a, b) => {
94
+ const aLongest = a.prefixes[0]?.length ?? 0;
95
+ const bLongest = b.prefixes[0]?.length ?? 0;
96
+ return bLongest - aLongest;
97
+ });
98
+
99
+ return { rules, warnings };
100
+ }
101
+
102
+ export function normalizeConfigContentTypes(
103
+ config: Config
104
+ ): ConfigNormalizationResult {
105
+ const normalized = normalizeContentTypes(config.contentTypes ?? []);
106
+ return {
107
+ config: {
108
+ ...config,
109
+ contentTypes: normalized.rules,
110
+ },
111
+ warnings: normalized.warnings,
112
+ };
113
+ }
114
+
115
+ export function fingerprintContentTypeRules(
116
+ rules: NormalizedContentTypeRule[]
117
+ ): string {
118
+ const canonical = rules.map((rule) => ({
119
+ id: rule.id,
120
+ preset: rule.preset,
121
+ prefixes: rule.prefixes,
122
+ }));
123
+ const hasher = new Bun.CryptoHasher("sha256");
124
+ hasher.update(JSON.stringify(canonical));
125
+ return hasher.digest("hex");
126
+ }
127
+
128
+ export function formatConfigWarning(warning: ConfigWarning): string {
129
+ return `[config] ${warning.path}: ${warning.message}`;
130
+ }
131
+
132
+ export function formatConfigWarnings(warnings?: ConfigWarning[]): string[] {
133
+ return (warnings ?? []).map(formatConfigWarning);
134
+ }
135
+
136
+ export function writeConfigWarningsToStderr(warnings?: ConfigWarning[]): void {
137
+ for (const warning of formatConfigWarnings(warnings)) {
138
+ process.stderr.write(`${warning}\n`);
139
+ }
140
+ }
@@ -16,5 +16,6 @@ export function createDefaultConfig(): Config {
16
16
  ftsTokenizer: DEFAULT_FTS_TOKENIZER,
17
17
  collections: [],
18
18
  contexts: [],
19
+ contentTypes: [],
19
20
  };
20
21
  }
@@ -5,6 +5,16 @@
5
5
  */
6
6
 
7
7
  export { createDefaultConfig } from "./defaults";
8
+ export {
9
+ type ConfigWarning,
10
+ fingerprintContentTypeRules,
11
+ formatConfigWarning,
12
+ formatConfigWarnings,
13
+ normalizeConfigContentTypes,
14
+ normalizeContentTypes,
15
+ type NormalizedContentTypeRule,
16
+ writeConfigWarningsToStderr,
17
+ } from "./content-types";
8
18
  // Loading
9
19
  export {
10
20
  isInitialized,
@@ -40,6 +50,10 @@ export {
40
50
  CollectionSchema,
41
51
  type Config,
42
52
  ConfigSchema,
53
+ CONTENT_TYPE_GRAPH_HINTS,
54
+ type ContentTypeConfig,
55
+ type ContentTypeGraphHint,
56
+ ContentTypeSchema,
43
57
  type Context,
44
58
  ContextSchema,
45
59
  DEFAULT_EXCLUDES,
@@ -7,6 +7,10 @@
7
7
 
8
8
  import type { ZodError } from "zod";
9
9
 
10
+ import {
11
+ normalizeConfigContentTypes,
12
+ type ConfigWarning,
13
+ } from "./content-types";
10
14
  import { configExists, expandPath, getConfigPaths } from "./paths";
11
15
  import { CONFIG_VERSION, type Config, ConfigSchema } from "./types";
12
16
 
@@ -15,7 +19,7 @@ import { CONFIG_VERSION, type Config, ConfigSchema } from "./types";
15
19
  // ─────────────────────────────────────────────────────────────────────────────
16
20
 
17
21
  export type LoadResult<T> =
18
- | { ok: true; value: T }
22
+ | { ok: true; value: T; warnings: ConfigWarning[] }
19
23
  | { ok: false; error: LoadError };
20
24
 
21
25
  export type LoadError =
@@ -128,7 +132,12 @@ export async function loadConfigFromPath(
128
132
  };
129
133
  }
130
134
 
131
- return { ok: true, value: result.data };
135
+ const normalized = normalizeConfigContentTypes(result.data);
136
+ return {
137
+ ok: true,
138
+ value: normalized.config,
139
+ warnings: normalized.warnings,
140
+ };
132
141
  }
133
142
 
134
143
  /**
@@ -245,6 +245,37 @@ export const ModelConfigSchema = z.object({
245
245
 
246
246
  export type ModelConfig = z.infer<typeof ModelConfigSchema>;
247
247
 
248
+ // ─────────────────────────────────────────────────────────────────────────────
249
+ // Content Type Schema
250
+ // ─────────────────────────────────────────────────────────────────────────────
251
+
252
+ export const CONTENT_TYPE_GRAPH_HINTS = [
253
+ "mentions",
254
+ "works_at",
255
+ "attended",
256
+ "decided",
257
+ "related_to",
258
+ ] as const;
259
+
260
+ export type ContentTypeGraphHint = (typeof CONTENT_TYPE_GRAPH_HINTS)[number];
261
+
262
+ export const ContentTypeSchema = z.object({
263
+ /** Stable content type identifier */
264
+ id: z.string().min(1),
265
+ /** Relative path prefixes that map to this content type */
266
+ prefixes: z.array(z.string().min(1)),
267
+ /** Note preset ID, resolved post-parse so unknown refs warn-and-drop */
268
+ preset: z.string().min(1),
269
+ /** Reserved for fn-84 typed graph hints; accepted but no-op in fn-83 */
270
+ graphHints: z.array(z.string().min(1)).optional(),
271
+ /** Reserved for future ranking; accepted but no-op in fn-83 */
272
+ searchBoost: z.number().finite().optional(),
273
+ /** Marks time-oriented content types; accepted but no-op in fn-83 */
274
+ temporal: z.boolean().optional(),
275
+ });
276
+
277
+ export type ContentTypeConfig = z.infer<typeof ContentTypeSchema>;
278
+
248
279
  // ─────────────────────────────────────────────────────────────────────────────
249
280
  // Config Schema (root)
250
281
  // ─────────────────────────────────────────────────────────────────────────────
@@ -265,11 +296,16 @@ export const ConfigSchema = z.object({
265
296
  /** Context metadata */
266
297
  contexts: z.array(ContextSchema).default([]),
267
298
 
299
+ /** Opt-in schema-lite content type rules */
300
+ contentTypes: z.array(ContentTypeSchema).default([]),
301
+
268
302
  /** Model configuration */
269
303
  models: ModelConfigSchema.optional(),
270
304
  });
271
305
 
272
- export type Config = z.infer<typeof ConfigSchema>;
306
+ export type Config = Omit<z.infer<typeof ConfigSchema>, "contentTypes"> & {
307
+ contentTypes?: ContentTypeConfig[];
308
+ };
273
309
 
274
310
  // ─────────────────────────────────────────────────────────────────────────────
275
311
  // Scope Utilities
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared capture write semantics.
3
+ *
4
+ * @module src/core/capture-write
5
+ */
6
+
7
+ import type { CapturePlan } from "./capture";
8
+
9
+ import { atomicCreate, atomicWrite } from "./file-ops";
10
+
11
+ function isFileExistsError(error: unknown): boolean {
12
+ return (
13
+ error instanceof Error &&
14
+ "code" in error &&
15
+ typeof error.code === "string" &&
16
+ error.code === "EEXIST"
17
+ );
18
+ }
19
+
20
+ export async function writeCapturePlanFile(
21
+ plan: CapturePlan,
22
+ absPath: string
23
+ ): Promise<void> {
24
+ try {
25
+ if (plan.overwrite) {
26
+ await atomicWrite(absPath, plan.content);
27
+ return;
28
+ }
29
+ await atomicCreate(absPath, plan.content);
30
+ } catch (error) {
31
+ if (isFileExistsError(error)) {
32
+ throw new Error(
33
+ "File already exists. Use open_existing, create_with_suffix, or overwrite."
34
+ );
35
+ }
36
+ throw error;
37
+ }
38
+ }