@gmickel/gno 1.7.1 → 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 +28 -0
- package/assets/skill/cli-reference.md +25 -0
- package/assets/skill/examples.md +16 -0
- package/assets/skill/mcp-reference.md +15 -0
- 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 +64 -0
- 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 +35 -0
- 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 +189 -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,
|
|
@@ -292,6 +300,8 @@ export interface CreateDocRequestBody {
|
|
|
292
300
|
tags?: string[];
|
|
293
301
|
}
|
|
294
302
|
|
|
303
|
+
export interface CreateCaptureRequestBody extends PublicCaptureInput {}
|
|
304
|
+
|
|
295
305
|
export interface RenameDocRequestBody {
|
|
296
306
|
name: string;
|
|
297
307
|
uri?: string;
|
|
@@ -2795,6 +2805,157 @@ export async function handleCreateEditableCopy(
|
|
|
2795
2805
|
return handleCreateDoc(ctxHolder, store, createReq);
|
|
2796
2806
|
}
|
|
2797
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
|
+
|
|
2798
2959
|
/**
|
|
2799
2960
|
* POST /api/docs
|
|
2800
2961
|
* Create a new document in a collection.
|
|
@@ -3989,6 +4150,31 @@ export async function routeApi(
|
|
|
3989
4150
|
return handlePublishExport(config, store, req);
|
|
3990
4151
|
}
|
|
3991
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
|
+
|
|
3992
4178
|
if (path === "/api/docs") {
|
|
3993
4179
|
return handleDocs(store, url);
|
|
3994
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);
|