@gmickel/gno 1.7.0 → 1.8.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 +6 -1
- package/assets/skill/SKILL.md +29 -1
- package/assets/skill/cli-reference.md +28 -2
- package/assets/skill/examples.md +16 -0
- package/assets/skill/mcp-reference.md +17 -3
- package/package.json +1 -1
- package/src/cli/commands/capture.ts +279 -0
- package/src/cli/options.ts +2 -0
- package/src/cli/program.ts +67 -1
- package/src/core/capture-write.ts +38 -0
- package/src/core/capture.ts +746 -0
- package/src/core/file-ops.ts +21 -2
- package/src/mcp/tools/capture.ts +132 -189
- package/src/mcp/tools/index.ts +40 -1
- package/src/mcp/tools/query.ts +2 -0
- package/src/pipeline/hybrid.ts +1 -1
- package/src/pipeline/types.ts +3 -1
- package/src/sdk/client.ts +109 -0
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +6 -0
- package/src/serve/public/components/CaptureModal.tsx +248 -10
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/routes/api.ts +191 -3
- package/src/serve/server.ts +12 -0
package/src/core/file-ops.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* @module src/core/file-ops
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
// node:fs/promises for rename/unlink (no Bun equivalent for structure ops)
|
|
8
|
-
import { copyFile, mkdir, rename, unlink } from "node:fs/promises";
|
|
7
|
+
// node:fs/promises for rename/unlink/link (no Bun equivalent for structure ops or exclusive hard-link create)
|
|
8
|
+
import { copyFile, link, mkdir, rename, unlink } from "node:fs/promises";
|
|
9
9
|
// node:os platform/homedir/tmpdir: no Bun equivalent
|
|
10
10
|
import { homedir, platform as getPlatform, tmpdir } from "node:os";
|
|
11
11
|
// node:path dirname/join/parse: no Bun equivalent
|
|
@@ -27,6 +27,25 @@ export async function atomicWrite(
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export async function atomicCreate(
|
|
31
|
+
path: string,
|
|
32
|
+
content: string
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
const tempPath = `${path}.tmp.${crypto.randomUUID()}`;
|
|
35
|
+
await Bun.write(tempPath, content);
|
|
36
|
+
try {
|
|
37
|
+
await link(tempPath, path);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
await unlink(tempPath).catch(() => {
|
|
40
|
+
/* ignore cleanup errors */
|
|
41
|
+
});
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
await unlink(tempPath).catch(() => {
|
|
45
|
+
/* ignore cleanup errors */
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
async function runCommand(cmd: string[]): Promise<void> {
|
|
31
50
|
const proc = Bun.spawn({
|
|
32
51
|
cmd,
|
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";
|
|
26
|
+
import { normalizeCollectionName } from "../../core/validation";
|
|
32
27
|
import { defaultSyncService } from "../../ingestion";
|
|
33
|
-
import { updateFrontmatterTags } from "../../ingestion/frontmatter";
|
|
34
|
-
import { extractTitle } from "../../pipeline/contextual";
|
|
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,136 @@ 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],
|
|
178
|
+
[plan.relPath],
|
|
241
179
|
{ runUpdateCmd: false, gitPull: false }
|
|
242
180
|
);
|
|
243
181
|
const syncResult = results[0];
|
|
244
182
|
if (!syncResult) {
|
|
245
|
-
|
|
183
|
+
return buildCaptureReceipt({
|
|
184
|
+
plan,
|
|
185
|
+
absPath,
|
|
186
|
+
docid: "",
|
|
187
|
+
sync: {
|
|
188
|
+
status: "failed",
|
|
189
|
+
error: "RUNTIME: Sync result missing",
|
|
190
|
+
},
|
|
191
|
+
overwritten: exists && args.overwrite === true,
|
|
192
|
+
serverInstanceId: ctx.serverInstanceId,
|
|
193
|
+
}) as McpCaptureResult;
|
|
246
194
|
}
|
|
247
195
|
if (syncResult.status === "error") {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
196
|
+
return buildCaptureReceipt({
|
|
197
|
+
plan,
|
|
198
|
+
absPath,
|
|
199
|
+
docid: "",
|
|
200
|
+
sync: {
|
|
201
|
+
status: "failed",
|
|
202
|
+
error: `INGEST_ERROR: ${syncResult.errorCode ?? "ERROR"} - ${
|
|
203
|
+
syncResult.errorMessage ?? "Unknown error"
|
|
204
|
+
}`,
|
|
205
|
+
},
|
|
206
|
+
overwritten: exists && args.overwrite === true,
|
|
207
|
+
serverInstanceId: ctx.serverInstanceId,
|
|
208
|
+
}) as McpCaptureResult;
|
|
253
209
|
}
|
|
254
210
|
|
|
255
211
|
let docid = syncResult.docid;
|
|
256
212
|
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;
|
|
213
|
+
const docResult = await ctx.store.getDocument(
|
|
214
|
+
collectionName,
|
|
215
|
+
plan.relPath
|
|
216
|
+
);
|
|
217
|
+
if (docResult.ok && docResult.value) {
|
|
218
|
+
docid = docid ?? docResult.value.docid;
|
|
269
219
|
documentId = docResult.value.id;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
220
|
+
}
|
|
221
|
+
if (!docid) {
|
|
222
|
+
return buildCaptureReceipt({
|
|
223
|
+
plan,
|
|
224
|
+
absPath,
|
|
225
|
+
docid: "",
|
|
226
|
+
sync: {
|
|
227
|
+
status: "failed",
|
|
228
|
+
error: "RUNTIME: Document missing after sync",
|
|
229
|
+
},
|
|
230
|
+
overwritten: exists && args.overwrite === true,
|
|
231
|
+
serverInstanceId: ctx.serverInstanceId,
|
|
232
|
+
}) as McpCaptureResult;
|
|
279
233
|
}
|
|
280
234
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (!isMarkdown && normalizedTags.length > 0 && documentId) {
|
|
235
|
+
const isMarkdown =
|
|
236
|
+
plan.relPath.endsWith(".md") || plan.relPath.endsWith(".markdown");
|
|
237
|
+
if (!isMarkdown && plan.tags.length > 0 && documentId) {
|
|
285
238
|
const tagResult = await ctx.store.setDocTags(
|
|
286
239
|
documentId,
|
|
287
|
-
|
|
240
|
+
plan.tags,
|
|
288
241
|
"user"
|
|
289
242
|
);
|
|
290
|
-
if (tagResult.ok) {
|
|
291
|
-
tagsStored = true;
|
|
292
|
-
} else {
|
|
293
|
-
// Log warning - document created but tags not stored
|
|
243
|
+
if (!tagResult.ok) {
|
|
294
244
|
console.error(
|
|
295
245
|
`[MCP] Warning: Document created but tags not stored: ${tagResult.error.message}`
|
|
296
246
|
);
|
|
297
247
|
}
|
|
298
248
|
}
|
|
299
249
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
tagsStored && normalizedTags.length > 0 ? normalizedTags : undefined;
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
docid,
|
|
306
|
-
uri: buildUri(collectionName, relPath),
|
|
250
|
+
return buildCaptureReceipt({
|
|
251
|
+
plan,
|
|
307
252
|
absPath,
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
overwritten: exists,
|
|
253
|
+
docid,
|
|
254
|
+
sync: { status: "completed" },
|
|
255
|
+
overwritten: exists && args.overwrite === true,
|
|
312
256
|
serverInstanceId: ctx.serverInstanceId,
|
|
313
|
-
|
|
314
|
-
};
|
|
257
|
+
}) as McpCaptureResult;
|
|
315
258
|
});
|
|
316
259
|
},
|
|
317
260
|
formatCaptureResult
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
|
|
11
11
|
import type { ToolContext } from "../server";
|
|
12
12
|
|
|
13
|
+
import { CAPTURE_MAX_TEXT_BYTES } from "../../core/capture";
|
|
13
14
|
import { normalizeTag } from "../../core/tags";
|
|
14
15
|
import { handleAddCollection } from "./add-collection";
|
|
15
16
|
import { handleCapture } from "./capture";
|
|
@@ -148,6 +149,7 @@ const captureInputSchema = z.object({
|
|
|
148
149
|
.describe("Target collection name (must already exist)"),
|
|
149
150
|
content: z
|
|
150
151
|
.string()
|
|
152
|
+
.max(CAPTURE_MAX_TEXT_BYTES)
|
|
151
153
|
.optional()
|
|
152
154
|
.describe(
|
|
153
155
|
"Document content (markdown or plain text). Optional when presetId provides a scaffold."
|
|
@@ -189,6 +191,39 @@ const captureInputSchema = z.object({
|
|
|
189
191
|
.array(z.string())
|
|
190
192
|
.optional()
|
|
191
193
|
.describe("Tags to apply to the new document"),
|
|
194
|
+
source: z
|
|
195
|
+
.object({
|
|
196
|
+
kind: z
|
|
197
|
+
.enum([
|
|
198
|
+
"direct",
|
|
199
|
+
"web",
|
|
200
|
+
"email",
|
|
201
|
+
"meeting",
|
|
202
|
+
"chat",
|
|
203
|
+
"file",
|
|
204
|
+
"api",
|
|
205
|
+
"unknown",
|
|
206
|
+
])
|
|
207
|
+
.optional()
|
|
208
|
+
.describe("Capture source kind"),
|
|
209
|
+
title: z.string().optional().describe("Human source title"),
|
|
210
|
+
url: z.string().optional().describe("Source URL"),
|
|
211
|
+
uri: z.string().optional().describe("Source URI"),
|
|
212
|
+
docid: z.string().optional().describe("Source GNO doc ID"),
|
|
213
|
+
mime: z.string().optional().describe("Source MIME type"),
|
|
214
|
+
ext: z.string().optional().describe("Source file extension"),
|
|
215
|
+
author: z.string().optional().describe("Source author"),
|
|
216
|
+
observedAt: z
|
|
217
|
+
.string()
|
|
218
|
+
.optional()
|
|
219
|
+
.describe("When the source was observed"),
|
|
220
|
+
capturedAt: z.string().optional().describe("Capture timestamp override"),
|
|
221
|
+
externalId: z.string().optional().describe("External system/source ID"),
|
|
222
|
+
})
|
|
223
|
+
.optional()
|
|
224
|
+
.describe(
|
|
225
|
+
"Structured provenance metadata written under source frontmatter"
|
|
226
|
+
),
|
|
192
227
|
});
|
|
193
228
|
|
|
194
229
|
const addCollectionInputSchema = z.object({
|
|
@@ -456,7 +491,11 @@ export const queryInputSchema = z.object({
|
|
|
456
491
|
noGraph: z
|
|
457
492
|
.boolean()
|
|
458
493
|
.optional()
|
|
459
|
-
.describe("
|
|
494
|
+
.describe("Compatibility no-op unless graph is also true"),
|
|
495
|
+
graph: z
|
|
496
|
+
.boolean()
|
|
497
|
+
.optional()
|
|
498
|
+
.describe("Enable bounded one-hop graph neighbor expansion"),
|
|
460
499
|
tagsAll: z.array(z.string()).optional().describe("Require ALL of these tags"),
|
|
461
500
|
tagsAny: z.array(z.string()).optional().describe("Require ANY of these tags"),
|
|
462
501
|
});
|
package/src/mcp/tools/query.ts
CHANGED
|
@@ -51,6 +51,7 @@ interface QueryInput {
|
|
|
51
51
|
expand?: boolean;
|
|
52
52
|
rerank?: boolean;
|
|
53
53
|
noGraph?: boolean;
|
|
54
|
+
graph?: boolean;
|
|
54
55
|
tagsAll?: string[];
|
|
55
56
|
tagsAny?: string[];
|
|
56
57
|
}
|
|
@@ -272,6 +273,7 @@ export function handleQuery(
|
|
|
272
273
|
author: args.author,
|
|
273
274
|
noExpand,
|
|
274
275
|
noRerank,
|
|
276
|
+
graph: args.graph === true,
|
|
275
277
|
noGraph: args.noGraph || args.fast,
|
|
276
278
|
queryModes,
|
|
277
279
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
package/src/pipeline/hybrid.ts
CHANGED
|
@@ -554,7 +554,7 @@ export async function searchHybrid(
|
|
|
554
554
|
includeSimilar: vectorAvailable,
|
|
555
555
|
limit,
|
|
556
556
|
candidateLimit,
|
|
557
|
-
disabled: options.noGraph,
|
|
557
|
+
disabled: !options.graph || options.noGraph,
|
|
558
558
|
lang: options.lang,
|
|
559
559
|
tagsAll: options.tagsAll,
|
|
560
560
|
tagsAny: options.tagsAny,
|
package/src/pipeline/types.ts
CHANGED
|
@@ -174,7 +174,9 @@ export type HybridSearchOptions = SearchOptions & {
|
|
|
174
174
|
candidateLimit?: number;
|
|
175
175
|
/** Enable explain output */
|
|
176
176
|
explain?: boolean;
|
|
177
|
-
/**
|
|
177
|
+
/** Enable bounded one-hop graph candidate expansion */
|
|
178
|
+
graph?: boolean;
|
|
179
|
+
/** Compatibility no-op unless graph is also true */
|
|
178
180
|
noGraph?: boolean;
|
|
179
181
|
/** Language hint for prompt selection (does NOT filter retrieval, only affects expansion prompts) */
|
|
180
182
|
queryLanguageHint?: string;
|