@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/mcp/tools/capture.ts
CHANGED
|
@@ -9,54 +9,39 @@ import { mkdir } from "node:fs/promises";
|
|
|
9
9
|
// node:path for path utils (no Bun path utils)
|
|
10
10
|
import { dirname, extname, join } from "node:path";
|
|
11
11
|
|
|
12
|
+
import type { NoteCollisionPolicy } from "../../core/note-creation";
|
|
13
|
+
import type { NotePresetId } from "../../core/note-presets";
|
|
12
14
|
import type { ToolContext } from "../server";
|
|
13
15
|
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
buildCaptureReceipt,
|
|
18
|
+
listCaptureDiskRelPaths,
|
|
19
|
+
planCapture,
|
|
20
|
+
type CaptureInput as SharedCaptureInput,
|
|
21
|
+
type CaptureReceipt,
|
|
22
|
+
} from "../../core/capture";
|
|
23
|
+
import { writeCapturePlanFile } from "../../core/capture-write";
|
|
15
24
|
import { MCP_ERRORS } from "../../core/errors";
|
|
16
25
|
import { withWriteLock } from "../../core/file-lock";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
resolveNoteCreatePlan,
|
|
20
|
-
type NoteCollisionPolicy,
|
|
21
|
-
} from "../../core/note-creation";
|
|
22
|
-
import {
|
|
23
|
-
getNotePreset,
|
|
24
|
-
resolveNotePreset,
|
|
25
|
-
type NotePresetId,
|
|
26
|
-
} from "../../core/note-presets";
|
|
27
|
-
import { normalizeTag, validateTag } from "../../core/tags";
|
|
28
|
-
import {
|
|
29
|
-
normalizeCollectionName,
|
|
30
|
-
validateRelPath,
|
|
31
|
-
} from "../../core/validation";
|
|
32
|
-
import { defaultSyncService } from "../../ingestion";
|
|
33
|
-
import { updateFrontmatterTags } from "../../ingestion/frontmatter";
|
|
34
|
-
import { extractTitle } from "../../pipeline/contextual";
|
|
26
|
+
import { normalizeCollectionName } from "../../core/validation";
|
|
27
|
+
import { defaultSyncService, withContentTypeRules } from "../../ingestion";
|
|
35
28
|
import { runTool, type ToolResult } from "./index";
|
|
36
29
|
|
|
37
|
-
interface CaptureInput
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
interface CaptureInput extends Omit<
|
|
31
|
+
SharedCaptureInput,
|
|
32
|
+
"relPath" | "collisionPolicy" | "presetId"
|
|
33
|
+
> {
|
|
41
34
|
path?: string;
|
|
42
|
-
overwrite?: boolean;
|
|
43
|
-
folderPath?: string;
|
|
44
35
|
collisionPolicy?: NoteCollisionPolicy;
|
|
45
36
|
presetId?: NotePresetId;
|
|
46
|
-
tags?: string[];
|
|
47
37
|
}
|
|
48
38
|
|
|
49
|
-
|
|
39
|
+
type McpCaptureResult = CaptureReceipt & {
|
|
50
40
|
docid: string;
|
|
51
|
-
uri: string;
|
|
52
41
|
absPath: string;
|
|
53
|
-
collection: string;
|
|
54
|
-
relPath: string;
|
|
55
|
-
created: boolean;
|
|
56
42
|
overwritten: boolean;
|
|
57
43
|
serverInstanceId: string;
|
|
58
|
-
|
|
59
|
-
}
|
|
44
|
+
};
|
|
60
45
|
|
|
61
46
|
const SENSITIVE_SUBPATHS = new Set([
|
|
62
47
|
".ssh",
|
|
@@ -67,51 +52,56 @@ const SENSITIVE_SUBPATHS = new Set([
|
|
|
67
52
|
"node_modules",
|
|
68
53
|
]);
|
|
69
54
|
|
|
70
|
-
function sanitizeFilename(title: string): string {
|
|
71
|
-
return title
|
|
72
|
-
.toLowerCase()
|
|
73
|
-
.trim()
|
|
74
|
-
.replaceAll(/[^\w\s-]/g, "")
|
|
75
|
-
.replaceAll(/\s+/g, "-")
|
|
76
|
-
.replaceAll(/-+/g, "-")
|
|
77
|
-
.replace(/^-|-$/g, "");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
55
|
function ensureMarkdownExtension(relPath: string): string {
|
|
81
56
|
return extname(relPath) ? relPath : `${relPath}.md`;
|
|
82
57
|
}
|
|
83
58
|
|
|
84
59
|
function assertNotSensitive(relPath: string): void {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
60
|
+
for (const segment of relPath.split(/[\\/]/)) {
|
|
61
|
+
if (segment && SENSITIVE_SUBPATHS.has(segment)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`${MCP_ERRORS.INVALID_PATH.code}: Cannot write to sensitive directory: ${segment}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
90
66
|
}
|
|
91
67
|
}
|
|
92
68
|
|
|
93
|
-
function
|
|
94
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
95
|
-
const fallback = `note-${timestamp}.md`;
|
|
96
|
-
const baseTitle = title?.trim() || extractTitle(content, fallback);
|
|
97
|
-
const slug = sanitizeFilename(baseTitle);
|
|
98
|
-
const safeSlug = slug || `note-${timestamp}`;
|
|
99
|
-
return `${safeSlug}.md`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function formatCaptureResult(result: CaptureResult): string {
|
|
69
|
+
function formatCaptureResult(result: McpCaptureResult): string {
|
|
103
70
|
const lines: string[] = [];
|
|
104
71
|
lines.push(`Doc: ${result.docid}`);
|
|
105
72
|
lines.push(`URI: ${result.uri}`);
|
|
106
73
|
lines.push(`Path: ${result.absPath}`);
|
|
107
74
|
lines.push(`Created: ${result.created ? "yes" : "no"}`);
|
|
75
|
+
lines.push(`Opened existing: ${result.openedExisting ? "yes" : "no"}`);
|
|
108
76
|
lines.push(`Overwritten: ${result.overwritten ? "yes" : "no"}`);
|
|
109
|
-
|
|
77
|
+
lines.push(`Collision: ${result.collisionPolicyResult}`);
|
|
78
|
+
lines.push(`Sync: ${result.sync.status}`);
|
|
79
|
+
lines.push(`Embed: ${result.embed.status}`);
|
|
80
|
+
lines.push(`Content hash: ${result.contentHash}`);
|
|
81
|
+
if (result.tags.length > 0) {
|
|
110
82
|
lines.push(`Tags: ${result.tags.join(", ")}`);
|
|
111
83
|
}
|
|
112
84
|
return lines.join("\n");
|
|
113
85
|
}
|
|
114
86
|
|
|
87
|
+
function buildSharedInput(
|
|
88
|
+
args: CaptureInput,
|
|
89
|
+
collectionName: string
|
|
90
|
+
): SharedCaptureInput {
|
|
91
|
+
return {
|
|
92
|
+
collection: collectionName,
|
|
93
|
+
content: args.content,
|
|
94
|
+
title: args.title,
|
|
95
|
+
relPath: args.path ? ensureMarkdownExtension(args.path) : undefined,
|
|
96
|
+
folderPath: args.folderPath,
|
|
97
|
+
collisionPolicy: args.collisionPolicy,
|
|
98
|
+
presetId: args.presetId,
|
|
99
|
+
tags: args.tags,
|
|
100
|
+
source: args.source,
|
|
101
|
+
overwrite: args.overwrite,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
115
105
|
export function handleCapture(
|
|
116
106
|
args: CaptureInput,
|
|
117
107
|
ctx: ToolContext
|
|
@@ -135,183 +125,139 @@ export function handleCapture(
|
|
|
135
125
|
);
|
|
136
126
|
}
|
|
137
127
|
|
|
138
|
-
if (args.presetId && !getNotePreset(args.presetId)) {
|
|
139
|
-
throw new Error(
|
|
140
|
-
`${MCP_ERRORS.INVALID_INPUT.code}: Unknown presetId: ${args.presetId}`
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
128
|
const existingDocs = await ctx.store.listDocuments(collectionName);
|
|
145
129
|
if (!existingDocs.ok) {
|
|
146
130
|
throw new Error(existingDocs.error.message);
|
|
147
131
|
}
|
|
148
132
|
|
|
149
|
-
let
|
|
133
|
+
let plan;
|
|
150
134
|
try {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
: undefined,
|
|
157
|
-
title:
|
|
158
|
-
args.title?.trim() ||
|
|
159
|
-
generateFilename(args.title, args.content ?? "").replace(
|
|
160
|
-
/\.md$/u,
|
|
161
|
-
""
|
|
162
|
-
),
|
|
163
|
-
folderPath: args.folderPath,
|
|
164
|
-
collisionPolicy: args.collisionPolicy,
|
|
165
|
-
},
|
|
166
|
-
existingDocs.value.map((doc) => doc.relPath)
|
|
167
|
-
);
|
|
168
|
-
if (plan.openedExisting) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
`${MCP_ERRORS.CONFLICT.code}: Existing note resolution is not supported through gno_capture yet`
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
relPath = plan.relPath;
|
|
135
|
+
plan = planCapture({
|
|
136
|
+
input: buildSharedInput(args, collection.name),
|
|
137
|
+
existingRelPaths: existingDocs.value.map((doc) => doc.relPath),
|
|
138
|
+
diskRelPaths: await listCaptureDiskRelPaths(collection.path),
|
|
139
|
+
});
|
|
174
140
|
} catch (error) {
|
|
175
141
|
const message =
|
|
176
142
|
error instanceof Error ? error.message : String(error);
|
|
177
|
-
throw new Error(`${MCP_ERRORS.
|
|
143
|
+
throw new Error(`${MCP_ERRORS.INVALID_INPUT.code}: ${message}`);
|
|
178
144
|
}
|
|
179
145
|
|
|
180
|
-
assertNotSensitive(relPath);
|
|
146
|
+
assertNotSensitive(plan.relPath);
|
|
181
147
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
for (const tag of args.tags) {
|
|
186
|
-
const normalized = normalizeTag(tag);
|
|
187
|
-
if (!validateTag(normalized)) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
`${MCP_ERRORS.INVALID_INPUT.code}: Invalid tag "${tag}". Tags must be lowercase, alphanumeric with hyphens/dots/slashes.`
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
normalizedTags.push(normalized);
|
|
193
|
-
}
|
|
194
|
-
// Dedupe
|
|
195
|
-
normalizedTags = [...new Set(normalizedTags)];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const absPath = join(collection.path, relPath);
|
|
199
|
-
const file = Bun.file(absPath);
|
|
200
|
-
const exists = await file.exists();
|
|
201
|
-
if (exists && !args.overwrite) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
`${MCP_ERRORS.CONFLICT.code}: File exists: ${relPath}`
|
|
204
|
-
);
|
|
205
|
-
}
|
|
148
|
+
const absPath = join(collection.path, plan.relPath);
|
|
149
|
+
const existingFile = Bun.file(absPath);
|
|
150
|
+
const exists = await existingFile.exists();
|
|
206
151
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
args.title?.trim() ||
|
|
212
|
-
relPath
|
|
213
|
-
.split("/")
|
|
214
|
-
.pop()
|
|
215
|
-
?.replace(/\.[^.]+$/u, "") ||
|
|
216
|
-
"Untitled",
|
|
217
|
-
tags: normalizedTags,
|
|
218
|
-
body: args.content,
|
|
219
|
-
});
|
|
220
|
-
let contentToWrite =
|
|
221
|
-
resolvedPreset?.content ??
|
|
222
|
-
args.content ??
|
|
223
|
-
`# ${args.title?.trim() || "Untitled"}\n`;
|
|
224
|
-
const isMarkdown =
|
|
225
|
-
relPath.endsWith(".md") || relPath.endsWith(".markdown");
|
|
226
|
-
if (isMarkdown && normalizedTags.length > 0) {
|
|
227
|
-
// Use shared utility that handles all frontmatter edge cases
|
|
228
|
-
contentToWrite = updateFrontmatterTags(
|
|
229
|
-
contentToWrite,
|
|
230
|
-
normalizedTags
|
|
152
|
+
if (plan.openedExisting) {
|
|
153
|
+
const docResult = await ctx.store.getDocument(
|
|
154
|
+
collectionName,
|
|
155
|
+
plan.relPath
|
|
231
156
|
);
|
|
157
|
+
const existingDoc = docResult.ok ? docResult.value : undefined;
|
|
158
|
+
return buildCaptureReceipt({
|
|
159
|
+
plan,
|
|
160
|
+
absPath,
|
|
161
|
+
docid: existingDoc?.docid ?? "",
|
|
162
|
+
sync: {
|
|
163
|
+
status: existingDoc ? "completed" : "skipped",
|
|
164
|
+
reason: existingDoc
|
|
165
|
+
? "Existing capture already indexed."
|
|
166
|
+
: "Existing capture opened from disk but is not indexed yet.",
|
|
167
|
+
},
|
|
168
|
+
serverInstanceId: ctx.serverInstanceId,
|
|
169
|
+
}) as McpCaptureResult;
|
|
232
170
|
}
|
|
233
171
|
|
|
234
172
|
await mkdir(dirname(absPath), { recursive: true });
|
|
235
|
-
await
|
|
173
|
+
await writeCapturePlanFile(plan, absPath);
|
|
236
174
|
|
|
237
175
|
const results = await defaultSyncService.syncFiles(
|
|
238
176
|
collection,
|
|
239
177
|
ctx.store,
|
|
240
|
-
[relPath],
|
|
241
|
-
|
|
178
|
+
[plan.relPath],
|
|
179
|
+
withContentTypeRules(
|
|
180
|
+
{ runUpdateCmd: false, gitPull: false },
|
|
181
|
+
ctx.config
|
|
182
|
+
)
|
|
242
183
|
);
|
|
243
184
|
const syncResult = results[0];
|
|
244
185
|
if (!syncResult) {
|
|
245
|
-
|
|
186
|
+
return buildCaptureReceipt({
|
|
187
|
+
plan,
|
|
188
|
+
absPath,
|
|
189
|
+
docid: "",
|
|
190
|
+
sync: {
|
|
191
|
+
status: "failed",
|
|
192
|
+
error: "RUNTIME: Sync result missing",
|
|
193
|
+
},
|
|
194
|
+
overwritten: exists && args.overwrite === true,
|
|
195
|
+
serverInstanceId: ctx.serverInstanceId,
|
|
196
|
+
}) as McpCaptureResult;
|
|
246
197
|
}
|
|
247
198
|
if (syncResult.status === "error") {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
199
|
+
return buildCaptureReceipt({
|
|
200
|
+
plan,
|
|
201
|
+
absPath,
|
|
202
|
+
docid: "",
|
|
203
|
+
sync: {
|
|
204
|
+
status: "failed",
|
|
205
|
+
error: `INGEST_ERROR: ${syncResult.errorCode ?? "ERROR"} - ${
|
|
206
|
+
syncResult.errorMessage ?? "Unknown error"
|
|
207
|
+
}`,
|
|
208
|
+
},
|
|
209
|
+
overwritten: exists && args.overwrite === true,
|
|
210
|
+
serverInstanceId: ctx.serverInstanceId,
|
|
211
|
+
}) as McpCaptureResult;
|
|
253
212
|
}
|
|
254
213
|
|
|
255
214
|
let docid = syncResult.docid;
|
|
256
215
|
let documentId: number | undefined;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
throw new Error(docResult.error.message);
|
|
264
|
-
}
|
|
265
|
-
if (!docResult.value) {
|
|
266
|
-
throw new Error("RUNTIME: Document missing after sync");
|
|
267
|
-
}
|
|
268
|
-
docid = docResult.value.docid;
|
|
216
|
+
const docResult = await ctx.store.getDocument(
|
|
217
|
+
collectionName,
|
|
218
|
+
plan.relPath
|
|
219
|
+
);
|
|
220
|
+
if (docResult.ok && docResult.value) {
|
|
221
|
+
docid = docid ?? docResult.value.docid;
|
|
269
222
|
documentId = docResult.value.id;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
223
|
+
}
|
|
224
|
+
if (!docid) {
|
|
225
|
+
return buildCaptureReceipt({
|
|
226
|
+
plan,
|
|
227
|
+
absPath,
|
|
228
|
+
docid: "",
|
|
229
|
+
sync: {
|
|
230
|
+
status: "failed",
|
|
231
|
+
error: "RUNTIME: Document missing after sync",
|
|
232
|
+
},
|
|
233
|
+
overwritten: exists && args.overwrite === true,
|
|
234
|
+
serverInstanceId: ctx.serverInstanceId,
|
|
235
|
+
}) as McpCaptureResult;
|
|
279
236
|
}
|
|
280
237
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (!isMarkdown && normalizedTags.length > 0 && documentId) {
|
|
238
|
+
const isMarkdown =
|
|
239
|
+
plan.relPath.endsWith(".md") || plan.relPath.endsWith(".markdown");
|
|
240
|
+
if (!isMarkdown && plan.tags.length > 0 && documentId) {
|
|
285
241
|
const tagResult = await ctx.store.setDocTags(
|
|
286
242
|
documentId,
|
|
287
|
-
|
|
243
|
+
plan.tags,
|
|
288
244
|
"user"
|
|
289
245
|
);
|
|
290
|
-
if (tagResult.ok) {
|
|
291
|
-
tagsStored = true;
|
|
292
|
-
} else {
|
|
293
|
-
// Log warning - document created but tags not stored
|
|
246
|
+
if (!tagResult.ok) {
|
|
294
247
|
console.error(
|
|
295
248
|
`[MCP] Warning: Document created but tags not stored: ${tagResult.error.message}`
|
|
296
249
|
);
|
|
297
250
|
}
|
|
298
251
|
}
|
|
299
252
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
tagsStored && normalizedTags.length > 0 ? normalizedTags : undefined;
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
docid,
|
|
306
|
-
uri: buildUri(collectionName, relPath),
|
|
253
|
+
return buildCaptureReceipt({
|
|
254
|
+
plan,
|
|
307
255
|
absPath,
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
overwritten: exists,
|
|
256
|
+
docid,
|
|
257
|
+
sync: { status: "completed" },
|
|
258
|
+
overwritten: exists && args.overwrite === true,
|
|
312
259
|
serverInstanceId: ctx.serverInstanceId,
|
|
313
|
-
|
|
314
|
-
};
|
|
260
|
+
}) as McpCaptureResult;
|
|
315
261
|
});
|
|
316
262
|
},
|
|
317
263
|
formatCaptureResult
|
|
@@ -12,7 +12,7 @@ import { acquireWriteLock, type WriteLockHandle } from "../../core/file-lock";
|
|
|
12
12
|
import { JobError } from "../../core/job-manager";
|
|
13
13
|
import { normalizeCollectionName } from "../../core/validation";
|
|
14
14
|
import { embedBacklog } from "../../embed";
|
|
15
|
-
import { defaultSyncService } from "../../ingestion";
|
|
15
|
+
import { defaultSyncService, withContentTypeRules } from "../../ingestion";
|
|
16
16
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
17
17
|
import { resolveModelUri } from "../../llm/registry";
|
|
18
18
|
import {
|
|
@@ -95,11 +95,14 @@ export function handleIndex(
|
|
|
95
95
|
? [collection.name]
|
|
96
96
|
: ctx.collections.map((entry) => entry.name);
|
|
97
97
|
|
|
98
|
-
const options =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
const options = withContentTypeRules(
|
|
99
|
+
{
|
|
100
|
+
gitPull: args.gitPull ?? false,
|
|
101
|
+
// Security: MCP never runs updateCmd by default
|
|
102
|
+
runUpdateCmd: false,
|
|
103
|
+
},
|
|
104
|
+
ctx.config
|
|
105
|
+
);
|
|
103
106
|
|
|
104
107
|
const modelUri = resolveModelUri(
|
|
105
108
|
ctx.config,
|
|
@@ -203,7 +206,10 @@ export function handleIndex(
|
|
|
203
206
|
collections,
|
|
204
207
|
status: "started",
|
|
205
208
|
phases: ["sync", "embed"],
|
|
206
|
-
options
|
|
209
|
+
options: {
|
|
210
|
+
gitPull: options.gitPull ?? false,
|
|
211
|
+
runUpdateCmd: options.runUpdateCmd ?? false,
|
|
212
|
+
},
|
|
207
213
|
};
|
|
208
214
|
|
|
209
215
|
return result;
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { z } from "zod";
|
|
|
10
10
|
|
|
11
11
|
import type { ToolContext } from "../server";
|
|
12
12
|
|
|
13
|
+
import { CAPTURE_MAX_TEXT_BYTES } from "../../core/capture";
|
|
14
|
+
import { NOTE_PRESETS, type NotePresetId } from "../../core/note-presets";
|
|
13
15
|
import { normalizeTag } from "../../core/tags";
|
|
14
16
|
import { handleAddCollection } from "./add-collection";
|
|
15
17
|
import { handleCapture } from "./capture";
|
|
@@ -141,13 +143,19 @@ const searchInputSchema = z.object({
|
|
|
141
143
|
.describe("Require ANY of these tags (OR filter)"),
|
|
142
144
|
});
|
|
143
145
|
|
|
144
|
-
const
|
|
146
|
+
const notePresetIds = NOTE_PRESETS.map((preset) => preset.id) as [
|
|
147
|
+
NotePresetId,
|
|
148
|
+
...NotePresetId[],
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
export const captureInputSchema = z.object({
|
|
145
152
|
collection: z
|
|
146
153
|
.string()
|
|
147
154
|
.min(1, "Collection cannot be empty")
|
|
148
155
|
.describe("Target collection name (must already exist)"),
|
|
149
156
|
content: z
|
|
150
157
|
.string()
|
|
158
|
+
.max(CAPTURE_MAX_TEXT_BYTES)
|
|
151
159
|
.optional()
|
|
152
160
|
.describe(
|
|
153
161
|
"Document content (markdown or plain text). Optional when presetId provides a scaffold."
|
|
@@ -171,14 +179,7 @@ const captureInputSchema = z.object({
|
|
|
171
179
|
.optional()
|
|
172
180
|
.describe("How to handle name collisions"),
|
|
173
181
|
presetId: z
|
|
174
|
-
.enum(
|
|
175
|
-
"blank",
|
|
176
|
-
"project-note",
|
|
177
|
-
"research-note",
|
|
178
|
-
"decision-note",
|
|
179
|
-
"prompt-pattern",
|
|
180
|
-
"source-summary",
|
|
181
|
-
])
|
|
182
|
+
.enum(notePresetIds)
|
|
182
183
|
.optional()
|
|
183
184
|
.describe("Optional note preset scaffold"),
|
|
184
185
|
overwrite: z
|
|
@@ -189,6 +190,39 @@ const captureInputSchema = z.object({
|
|
|
189
190
|
.array(z.string())
|
|
190
191
|
.optional()
|
|
191
192
|
.describe("Tags to apply to the new document"),
|
|
193
|
+
source: z
|
|
194
|
+
.object({
|
|
195
|
+
kind: z
|
|
196
|
+
.enum([
|
|
197
|
+
"direct",
|
|
198
|
+
"web",
|
|
199
|
+
"email",
|
|
200
|
+
"meeting",
|
|
201
|
+
"chat",
|
|
202
|
+
"file",
|
|
203
|
+
"api",
|
|
204
|
+
"unknown",
|
|
205
|
+
])
|
|
206
|
+
.optional()
|
|
207
|
+
.describe("Capture source kind"),
|
|
208
|
+
title: z.string().optional().describe("Human source title"),
|
|
209
|
+
url: z.string().optional().describe("Source URL"),
|
|
210
|
+
uri: z.string().optional().describe("Source URI"),
|
|
211
|
+
docid: z.string().optional().describe("Source GNO doc ID"),
|
|
212
|
+
mime: z.string().optional().describe("Source MIME type"),
|
|
213
|
+
ext: z.string().optional().describe("Source file extension"),
|
|
214
|
+
author: z.string().optional().describe("Source author"),
|
|
215
|
+
observedAt: z
|
|
216
|
+
.string()
|
|
217
|
+
.optional()
|
|
218
|
+
.describe("When the source was observed"),
|
|
219
|
+
capturedAt: z.string().optional().describe("Capture timestamp override"),
|
|
220
|
+
externalId: z.string().optional().describe("External system/source ID"),
|
|
221
|
+
})
|
|
222
|
+
.optional()
|
|
223
|
+
.describe(
|
|
224
|
+
"Structured provenance metadata written under source frontmatter"
|
|
225
|
+
),
|
|
192
226
|
});
|
|
193
227
|
|
|
194
228
|
const addCollectionInputSchema = z.object({
|
package/src/mcp/tools/sync.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { MCP_ERRORS } from "../../core/errors";
|
|
|
12
12
|
import { acquireWriteLock, type WriteLockHandle } from "../../core/file-lock";
|
|
13
13
|
import { JobError } from "../../core/job-manager";
|
|
14
14
|
import { normalizeCollectionName } from "../../core/validation";
|
|
15
|
-
import { defaultSyncService } from "../../ingestion";
|
|
15
|
+
import { defaultSyncService, withContentTypeRules } from "../../ingestion";
|
|
16
16
|
import { runTool, type ToolResult } from "./index";
|
|
17
17
|
|
|
18
18
|
interface SyncInput {
|
|
@@ -101,10 +101,13 @@ export function handleSync(
|
|
|
101
101
|
? [collection.name]
|
|
102
102
|
: ctx.collections.map((entry) => entry.name);
|
|
103
103
|
|
|
104
|
-
const options =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
const options = withContentTypeRules(
|
|
105
|
+
{
|
|
106
|
+
gitPull: args.gitPull ?? false,
|
|
107
|
+
runUpdateCmd: args.runUpdateCmd ?? false,
|
|
108
|
+
},
|
|
109
|
+
ctx.config
|
|
110
|
+
);
|
|
108
111
|
|
|
109
112
|
const jobId = await ctx.jobManager.startJobWithLock(
|
|
110
113
|
"sync",
|
|
@@ -133,7 +136,10 @@ export function handleSync(
|
|
|
133
136
|
jobId,
|
|
134
137
|
collections,
|
|
135
138
|
status: "started",
|
|
136
|
-
options
|
|
139
|
+
options: {
|
|
140
|
+
gitPull: options.gitPull ?? false,
|
|
141
|
+
runUpdateCmd: options.runUpdateCmd ?? false,
|
|
142
|
+
},
|
|
137
143
|
};
|
|
138
144
|
|
|
139
145
|
return result;
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
planMoveRefactor,
|
|
28
28
|
planRenameRefactor,
|
|
29
29
|
} from "../../core/file-refactors";
|
|
30
|
-
import { defaultSyncService } from "../../ingestion";
|
|
30
|
+
import { defaultSyncService, withContentTypeRules } from "../../ingestion";
|
|
31
31
|
import { runTool, type ToolResult } from "./index";
|
|
32
32
|
|
|
33
33
|
interface CreateFolderInput {
|
|
@@ -210,9 +210,11 @@ export function handleRenameNote(
|
|
|
210
210
|
const currentPath = join(collection.path, doc.relPath);
|
|
211
211
|
const nextPath = join(collection.path, plan.nextRelPath);
|
|
212
212
|
await renameFilePath(currentPath, nextPath);
|
|
213
|
-
await defaultSyncService.syncCollection(
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
await defaultSyncService.syncCollection(
|
|
214
|
+
collection,
|
|
215
|
+
ctx.store,
|
|
216
|
+
withContentTypeRules({ runUpdateCmd: false }, ctx.config)
|
|
217
|
+
);
|
|
216
218
|
return {
|
|
217
219
|
uri: plan.nextUri,
|
|
218
220
|
relPath: plan.nextRelPath,
|
|
@@ -253,9 +255,11 @@ export function handleMoveNote(
|
|
|
253
255
|
const nextPath = join(collection.path, plan.nextRelPath);
|
|
254
256
|
await mkdir(dirname(nextPath), { recursive: true });
|
|
255
257
|
await renameFilePath(currentPath, nextPath);
|
|
256
|
-
await defaultSyncService.syncCollection(
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
await defaultSyncService.syncCollection(
|
|
259
|
+
collection,
|
|
260
|
+
ctx.store,
|
|
261
|
+
withContentTypeRules({ runUpdateCmd: false }, ctx.config)
|
|
262
|
+
);
|
|
259
263
|
return {
|
|
260
264
|
uri: plan.nextUri,
|
|
261
265
|
relPath: plan.nextRelPath,
|
|
@@ -304,9 +308,11 @@ export function handleDuplicateNote(
|
|
|
304
308
|
const nextPath = join(collection.path, plan.nextRelPath);
|
|
305
309
|
await mkdir(dirname(nextPath), { recursive: true });
|
|
306
310
|
await copyFilePath(currentPath, nextPath);
|
|
307
|
-
await defaultSyncService.syncCollection(
|
|
308
|
-
|
|
309
|
-
|
|
311
|
+
await defaultSyncService.syncCollection(
|
|
312
|
+
collection,
|
|
313
|
+
ctx.store,
|
|
314
|
+
withContentTypeRules({ runUpdateCmd: false }, ctx.config)
|
|
315
|
+
);
|
|
310
316
|
return {
|
|
311
317
|
uri: plan.nextUri,
|
|
312
318
|
relPath: plan.nextRelPath,
|
package/src/pipeline/hybrid.ts
CHANGED
|
@@ -841,6 +841,8 @@ export async function searchHybrid(
|
|
|
841
841
|
score: candidate.blendedScore,
|
|
842
842
|
uri: doc.uri,
|
|
843
843
|
title: doc.title ?? undefined,
|
|
844
|
+
contentType: doc.contentType ?? undefined,
|
|
845
|
+
categories: doc.categories ?? undefined,
|
|
844
846
|
line: snippetChunk.startLine,
|
|
845
847
|
snippet,
|
|
846
848
|
snippetLanguage: chunk.language ?? undefined,
|
package/src/pipeline/search.ts
CHANGED
|
@@ -117,6 +117,8 @@ function buildSearchResult(ctx: BuildResultContext): SearchResult {
|
|
|
117
117
|
score: fts.score, // Raw score, normalized later as batch
|
|
118
118
|
uri: fts.uri ?? "",
|
|
119
119
|
title: fts.title,
|
|
120
|
+
contentType: fts.contentType,
|
|
121
|
+
categories: fts.categories,
|
|
120
122
|
line: chunk?.startLine,
|
|
121
123
|
snippet,
|
|
122
124
|
snippetLanguage: chunk?.language ?? undefined,
|
package/src/pipeline/types.ts
CHANGED