@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.
@@ -5,10 +5,10 @@
5
5
  * @module src/serve/routes/api
6
6
  */
7
7
 
8
- // node:fs/promises structure walk has no Bun equivalent
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
  }
@@ -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);