@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.
- package/README.md +10 -1
- package/assets/skill/SKILL.md +41 -0
- package/assets/skill/cli-reference.md +34 -0
- package/assets/skill/examples.md +37 -0
- package/assets/skill/mcp-reference.md +21 -0
- package/package.json +1 -1
- package/src/cli/commands/capture.ts +282 -0
- package/src/cli/commands/index-cmd.ts +17 -6
- package/src/cli/commands/shared.ts +17 -1
- package/src/cli/commands/update.ts +17 -6
- package/src/cli/options.ts +2 -0
- package/src/cli/program.ts +64 -0
- package/src/config/content-types.ts +140 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +14 -0
- package/src/config/loader.ts +11 -2
- package/src/config/types.ts +37 -1
- package/src/core/capture-write.ts +38 -0
- package/src/core/capture.ts +746 -0
- package/src/core/config-mutation.ts +14 -2
- package/src/core/file-ops.ts +21 -2
- package/src/core/note-presets.ts +61 -5
- package/src/ingestion/frontmatter.ts +77 -2
- package/src/ingestion/index.ts +2 -0
- package/src/ingestion/sync-options.ts +29 -0
- package/src/ingestion/sync.ts +104 -16
- package/src/ingestion/types.ts +14 -0
- package/src/mcp/tools/add-collection.ts +8 -5
- package/src/mcp/tools/capture.ts +137 -191
- package/src/mcp/tools/index-cmd.ts +13 -7
- package/src/mcp/tools/index.ts +43 -9
- package/src/mcp/tools/sync.ts +12 -6
- package/src/mcp/tools/workspace-write.ts +16 -10
- package/src/pipeline/hybrid.ts +2 -0
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +2 -0
- package/src/pipeline/vsearch.ts +4 -0
- package/src/sdk/client.ts +156 -20
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +6 -0
- package/src/serve/background-runtime.ts +18 -4
- package/src/serve/config-sync.ts +9 -2
- package/src/serve/public/components/CaptureModal.tsx +248 -10
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/routes/api.ts +238 -26
- package/src/serve/server.ts +12 -0
- package/src/serve/watch-service.ts +12 -2
- package/src/store/migrations/009-content-type-rule-fingerprint.ts +36 -0
- package/src/store/migrations/index.ts +12 -1
- package/src/store/sqlite/adapter.ts +29 -14
- package/src/store/types.ts +6 -0
package/src/cli/program.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/config/defaults.ts
CHANGED
package/src/config/index.ts
CHANGED
|
@@ -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,
|
package/src/config/loader.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/src/config/types.ts
CHANGED
|
@@ -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
|
+
}
|