@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/serve/routes/api.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* @module src/serve/routes/api
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
// node:fs/promises structure
|
|
9
|
-
import { readdir } from "node:fs/promises";
|
|
8
|
+
// node:fs/promises structure ops have no Bun equivalent
|
|
9
|
+
import { mkdir, readdir } from "node:fs/promises";
|
|
10
10
|
// node:path has no Bun equivalent
|
|
11
|
-
import { posix as pathPosix } from "node:path";
|
|
11
|
+
import { dirname, join as pathJoin, posix as pathPosix } from "node:path";
|
|
12
12
|
|
|
13
13
|
import type {
|
|
14
14
|
Collection,
|
|
@@ -35,6 +35,14 @@ import {
|
|
|
35
35
|
removeCollection,
|
|
36
36
|
updateCollection,
|
|
37
37
|
} from "../../collection";
|
|
38
|
+
import {
|
|
39
|
+
buildCaptureReceipt,
|
|
40
|
+
listCaptureDiskRelPaths,
|
|
41
|
+
planCapture,
|
|
42
|
+
type CapturePlan,
|
|
43
|
+
type PublicCaptureInput,
|
|
44
|
+
} from "../../core/capture";
|
|
45
|
+
import { writeCapturePlanFile } from "../../core/capture-write";
|
|
38
46
|
import {
|
|
39
47
|
buildEditableCopyContent,
|
|
40
48
|
deriveEditableCopyRelPath,
|
|
@@ -173,6 +181,7 @@ export interface QueryRequestBody {
|
|
|
173
181
|
noExpand?: boolean;
|
|
174
182
|
noRerank?: boolean;
|
|
175
183
|
noGraph?: boolean;
|
|
184
|
+
graph?: boolean;
|
|
176
185
|
/** Comma-separated tags - filter to docs having ALL (AND) */
|
|
177
186
|
tagsAll?: string;
|
|
178
187
|
/** Comma-separated tags - filter to docs having ANY (OR) */
|
|
@@ -291,6 +300,8 @@ export interface CreateDocRequestBody {
|
|
|
291
300
|
tags?: string[];
|
|
292
301
|
}
|
|
293
302
|
|
|
303
|
+
export interface CreateCaptureRequestBody extends PublicCaptureInput {}
|
|
304
|
+
|
|
294
305
|
export interface RenameDocRequestBody {
|
|
295
306
|
name: string;
|
|
296
307
|
uri?: string;
|
|
@@ -2794,6 +2805,157 @@ export async function handleCreateEditableCopy(
|
|
|
2794
2805
|
return handleCreateDoc(ctxHolder, store, createReq);
|
|
2795
2806
|
}
|
|
2796
2807
|
|
|
2808
|
+
/**
|
|
2809
|
+
* POST /api/capture
|
|
2810
|
+
* Capture a note with structured provenance.
|
|
2811
|
+
*/
|
|
2812
|
+
export async function handleCreateCapture(
|
|
2813
|
+
ctxHolder: ContextHolder,
|
|
2814
|
+
store: SqliteAdapter,
|
|
2815
|
+
req: Request
|
|
2816
|
+
): Promise<Response> {
|
|
2817
|
+
let body: CreateCaptureRequestBody;
|
|
2818
|
+
try {
|
|
2819
|
+
body = (await req.json()) as CreateCaptureRequestBody;
|
|
2820
|
+
} catch {
|
|
2821
|
+
return errorResponse("VALIDATION", "Invalid JSON body");
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
if (!body.collection || typeof body.collection !== "string") {
|
|
2825
|
+
return errorResponse("VALIDATION", "Missing or invalid collection");
|
|
2826
|
+
}
|
|
2827
|
+
if (body.content !== undefined && typeof body.content !== "string") {
|
|
2828
|
+
return errorResponse("VALIDATION", "content must be a string");
|
|
2829
|
+
}
|
|
2830
|
+
if (body.title !== undefined && typeof body.title !== "string") {
|
|
2831
|
+
return errorResponse("VALIDATION", "title must be a string");
|
|
2832
|
+
}
|
|
2833
|
+
if (body.relPath !== undefined && typeof body.relPath !== "string") {
|
|
2834
|
+
return errorResponse("VALIDATION", "relPath must be a string");
|
|
2835
|
+
}
|
|
2836
|
+
if (body.folderPath !== undefined && typeof body.folderPath !== "string") {
|
|
2837
|
+
return errorResponse("VALIDATION", "folderPath must be a string");
|
|
2838
|
+
}
|
|
2839
|
+
if ("overwrite" in body) {
|
|
2840
|
+
return errorResponse(
|
|
2841
|
+
"VALIDATION",
|
|
2842
|
+
"overwrite is not supported by /api/capture; use collisionPolicy instead"
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
const collectionName = body.collection.toLowerCase();
|
|
2847
|
+
const collection = ctxHolder.config.collections.find(
|
|
2848
|
+
(candidate) => candidate.name.toLowerCase() === collectionName
|
|
2849
|
+
);
|
|
2850
|
+
if (!collection) {
|
|
2851
|
+
return errorResponse(
|
|
2852
|
+
"NOT_FOUND",
|
|
2853
|
+
`Collection not found: ${body.collection}`,
|
|
2854
|
+
404
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
let plan: CapturePlan;
|
|
2859
|
+
try {
|
|
2860
|
+
plan = planCapture({
|
|
2861
|
+
input: {
|
|
2862
|
+
...body,
|
|
2863
|
+
collection: collection.name,
|
|
2864
|
+
},
|
|
2865
|
+
existingRelPaths: await listCollectionRelPaths(store, collection.name),
|
|
2866
|
+
diskRelPaths: await listCaptureDiskRelPaths(collection.path),
|
|
2867
|
+
});
|
|
2868
|
+
} catch (error) {
|
|
2869
|
+
return errorResponse(
|
|
2870
|
+
"VALIDATION",
|
|
2871
|
+
error instanceof Error ? error.message : String(error),
|
|
2872
|
+
409
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
const fullPath = pathJoin(collection.path, plan.relPath);
|
|
2877
|
+
if (plan.openedExisting) {
|
|
2878
|
+
const existingDoc = await store.getDocument(collection.name, plan.relPath);
|
|
2879
|
+
if (!existingDoc.ok) {
|
|
2880
|
+
return errorResponse("RUNTIME", existingDoc.error.message, 500);
|
|
2881
|
+
}
|
|
2882
|
+
return jsonResponse(
|
|
2883
|
+
buildCaptureReceipt({
|
|
2884
|
+
plan,
|
|
2885
|
+
absPath: fullPath,
|
|
2886
|
+
docid: existingDoc.value?.docid,
|
|
2887
|
+
sync: existingDoc.value
|
|
2888
|
+
? { status: "completed" }
|
|
2889
|
+
: {
|
|
2890
|
+
status: "skipped",
|
|
2891
|
+
reason: "Existing file is not indexed yet.",
|
|
2892
|
+
},
|
|
2893
|
+
})
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
try {
|
|
2898
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
2899
|
+
ctxHolder.watchService?.suppress(fullPath);
|
|
2900
|
+
await writeCapturePlanFile(plan, fullPath);
|
|
2901
|
+
|
|
2902
|
+
const gnoUri = `gno://${collection.name}/${plan.relPath}`;
|
|
2903
|
+
const jobResult = startJob("sync", async (): Promise<SyncResult> => {
|
|
2904
|
+
const result = await defaultSyncService.syncCollection(
|
|
2905
|
+
collection,
|
|
2906
|
+
store,
|
|
2907
|
+
{ runUpdateCmd: false }
|
|
2908
|
+
);
|
|
2909
|
+
ctxHolder.scheduler?.notifySyncComplete([plan.relPath]);
|
|
2910
|
+
ctxHolder.eventBus?.emit({
|
|
2911
|
+
type: "document-changed",
|
|
2912
|
+
uri: gnoUri,
|
|
2913
|
+
collection: collection.name,
|
|
2914
|
+
relPath: plan.relPath,
|
|
2915
|
+
origin: "create",
|
|
2916
|
+
changedAt: new Date().toISOString(),
|
|
2917
|
+
});
|
|
2918
|
+
return {
|
|
2919
|
+
collections: [result],
|
|
2920
|
+
totalDurationMs: result.durationMs,
|
|
2921
|
+
totalFilesProcessed: result.filesProcessed,
|
|
2922
|
+
totalFilesAdded: result.filesAdded,
|
|
2923
|
+
totalFilesUpdated: result.filesUpdated,
|
|
2924
|
+
totalFilesErrored: result.filesErrored,
|
|
2925
|
+
totalFilesSkipped: result.filesSkipped,
|
|
2926
|
+
};
|
|
2927
|
+
});
|
|
2928
|
+
|
|
2929
|
+
return jsonResponse(
|
|
2930
|
+
buildCaptureReceipt({
|
|
2931
|
+
plan,
|
|
2932
|
+
absPath: fullPath,
|
|
2933
|
+
sync: jobResult.ok
|
|
2934
|
+
? {
|
|
2935
|
+
status: "pending",
|
|
2936
|
+
jobId: jobResult.jobId,
|
|
2937
|
+
reason: "Sync job started; poll /api/jobs/:id for status.",
|
|
2938
|
+
}
|
|
2939
|
+
: {
|
|
2940
|
+
status: "skipped",
|
|
2941
|
+
jobId: jobResult.activeJobId,
|
|
2942
|
+
reason: "Sync skipped because another job is running.",
|
|
2943
|
+
error: jobResult.error,
|
|
2944
|
+
},
|
|
2945
|
+
}),
|
|
2946
|
+
202
|
|
2947
|
+
);
|
|
2948
|
+
} catch (error) {
|
|
2949
|
+
return errorResponse(
|
|
2950
|
+
"RUNTIME",
|
|
2951
|
+
`Failed to capture document: ${
|
|
2952
|
+
error instanceof Error ? error.message : String(error)
|
|
2953
|
+
}`,
|
|
2954
|
+
500
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2797
2959
|
/**
|
|
2798
2960
|
* POST /api/docs
|
|
2799
2961
|
* Create a new document in a collection.
|
|
@@ -3311,6 +3473,7 @@ export async function handleQuery(
|
|
|
3311
3473
|
queryModes: normalizedQueryModes,
|
|
3312
3474
|
noExpand: body.noExpand,
|
|
3313
3475
|
noRerank: body.noRerank,
|
|
3476
|
+
graph: body.graph === true,
|
|
3314
3477
|
noGraph: body.noGraph,
|
|
3315
3478
|
tagsAll,
|
|
3316
3479
|
tagsAny,
|
|
@@ -3987,6 +4150,31 @@ export async function routeApi(
|
|
|
3987
4150
|
return handlePublishExport(config, store, req);
|
|
3988
4151
|
}
|
|
3989
4152
|
|
|
4153
|
+
if (path === "/api/capture" && req.method === "POST") {
|
|
4154
|
+
const ctxHolder: ContextHolder = {
|
|
4155
|
+
current: {
|
|
4156
|
+
config,
|
|
4157
|
+
store,
|
|
4158
|
+
vectorIndex: null,
|
|
4159
|
+
embedPort: null,
|
|
4160
|
+
expandPort: null,
|
|
4161
|
+
answerPort: null,
|
|
4162
|
+
rerankPort: null,
|
|
4163
|
+
capabilities: {
|
|
4164
|
+
bm25: true,
|
|
4165
|
+
vector: false,
|
|
4166
|
+
hybrid: false,
|
|
4167
|
+
answer: false,
|
|
4168
|
+
},
|
|
4169
|
+
},
|
|
4170
|
+
config,
|
|
4171
|
+
scheduler: null,
|
|
4172
|
+
eventBus: null,
|
|
4173
|
+
watchService: null,
|
|
4174
|
+
};
|
|
4175
|
+
return handleCreateCapture(ctxHolder, store, req);
|
|
4176
|
+
}
|
|
4177
|
+
|
|
3990
4178
|
if (path === "/api/docs") {
|
|
3991
4179
|
return handleDocs(store, url);
|
|
3992
4180
|
}
|
package/src/serve/server.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
handleCreateFolder,
|
|
24
24
|
handleCreateCollection,
|
|
25
25
|
handleCreateEditableCopy,
|
|
26
|
+
handleCreateCapture,
|
|
26
27
|
handleCreateDoc,
|
|
27
28
|
handleDeactivateDoc,
|
|
28
29
|
handleDeleteCollection,
|
|
@@ -256,6 +257,17 @@ export async function startServer(
|
|
|
256
257
|
);
|
|
257
258
|
},
|
|
258
259
|
},
|
|
260
|
+
"/api/capture": {
|
|
261
|
+
POST: async (req: Request) => {
|
|
262
|
+
if (!isRequestAllowed(req, port)) {
|
|
263
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
264
|
+
}
|
|
265
|
+
return withSecurityHeaders(
|
|
266
|
+
await handleCreateCapture(ctxHolder, store, req),
|
|
267
|
+
isDev
|
|
268
|
+
);
|
|
269
|
+
},
|
|
270
|
+
},
|
|
259
271
|
"/api/docs": {
|
|
260
272
|
GET: async (req: Request) => {
|
|
261
273
|
const url = new URL(req.url);
|