@gmickel/gno 1.30.7 → 1.32.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.
Files changed (71) hide show
  1. package/README.md +6 -5
  2. package/assets/skill/SKILL.md +25 -0
  3. package/assets/skill/mcp-reference.md +6 -0
  4. package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/package.json +1 -1
  8. package/spec/cli.md +19 -0
  9. package/spec/db/schema.sql +55 -0
  10. package/spec/mcp.md +176 -11
  11. package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
  12. package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
  13. package/spec/output-schemas/section-target-create-result.schema.json +20 -0
  14. package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
  15. package/spec/output-schemas/section-target.schema.json +118 -0
  16. package/spec/output-schemas/section.schema.json +113 -0
  17. package/src/core/document-capabilities.ts +13 -0
  18. package/src/core/file-ops.ts +129 -1
  19. package/src/core/file-refactor-adapter.ts +329 -0
  20. package/src/core/file-refactor-apply-edits.ts +61 -0
  21. package/src/core/file-refactor-apply-fs.ts +512 -0
  22. package/src/core/file-refactor-apply-safety.ts +340 -0
  23. package/src/core/file-refactor-apply-validate.ts +401 -0
  24. package/src/core/file-refactor-contract.ts +486 -0
  25. package/src/core/file-refactor-destination.ts +123 -0
  26. package/src/core/file-refactor-from-snapshot.ts +148 -0
  27. package/src/core/file-refactor-journal-port.ts +150 -0
  28. package/src/core/file-refactor-journal.ts +347 -0
  29. package/src/core/file-refactor-paths.ts +60 -0
  30. package/src/core/file-refactor-plan-classify.ts +208 -0
  31. package/src/core/file-refactor-plan-validate.ts +169 -0
  32. package/src/core/file-refactor-planner-types.ts +62 -0
  33. package/src/core/file-refactor-planner.ts +423 -0
  34. package/src/core/file-refactor-resolve.ts +280 -0
  35. package/src/core/file-refactor-service.ts +468 -0
  36. package/src/core/file-refactors.ts +84 -56
  37. package/src/core/link-destination-parse.ts +275 -0
  38. package/src/core/link-inventory-markdown.ts +454 -0
  39. package/src/core/link-inventory-opaque.ts +244 -0
  40. package/src/core/link-inventory-types.ts +47 -0
  41. package/src/core/link-inventory.ts +182 -0
  42. package/src/core/link-relevance.ts +150 -0
  43. package/src/core/section-parse.ts +187 -0
  44. package/src/core/section-target-link.ts +154 -0
  45. package/src/core/section-target-resolve.ts +351 -0
  46. package/src/core/section-target-transport.ts +519 -0
  47. package/src/core/section-target.ts +263 -0
  48. package/src/core/sections.ts +60 -115
  49. package/src/mcp/AGENTS.md +1 -0
  50. package/src/mcp/CLAUDE.md +1 -0
  51. package/src/mcp/http-egress.ts +1 -0
  52. package/src/mcp/tools/index.ts +37 -17
  53. package/src/mcp/tools/sections.ts +512 -0
  54. package/src/mcp/tools/workspace-write.ts +215 -97
  55. package/src/sdk/client.ts +238 -116
  56. package/src/sdk/index.ts +12 -0
  57. package/src/sdk/types.ts +61 -3
  58. package/src/serve/file-refactor-http.ts +239 -0
  59. package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
  60. package/src/serve/public/globals.built.css +1 -1
  61. package/src/serve/public/lib/section-links.ts +189 -0
  62. package/src/serve/public/pages/DocView.tsx +395 -77
  63. package/src/serve/routes/api.ts +191 -104
  64. package/src/serve/routes/section-targets.ts +221 -0
  65. package/src/serve/server.ts +34 -0
  66. package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
  67. package/src/store/migrations/index.ts +2 -0
  68. package/src/store/sqlite/adapter.ts +452 -0
  69. package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
  70. package/src/store/types.ts +84 -0
  71. package/browser-extension/artifacts/gno-browser-clipper-v1.30.7.zip.sha256 +0 -1
@@ -8,6 +8,7 @@
8
8
  import { mkdir } from "node:fs/promises";
9
9
  // node:path for dirname/join (no Bun path utils)
10
10
  import { dirname, join } from "node:path";
11
+ import { z } from "zod";
11
12
 
12
13
  import type { Collection } from "../../config/types";
13
14
  import type { ToolContext } from "../server";
@@ -15,17 +16,22 @@ import type { ToolContext } from "../server";
15
16
  import { getDocumentCapabilities } from "../../core/document-capabilities";
16
17
  import { MCP_ERRORS } from "../../core/errors";
17
18
  import { withWriteLock } from "../../core/file-lock";
19
+ import { copyFilePath, createFolderPath } from "../../core/file-ops";
18
20
  import {
19
- copyFilePath,
20
- createFolderPath,
21
- renameFilePath,
22
- } from "../../core/file-ops";
23
- import {
21
+ applyCanonicalFileRefactor,
22
+ assertFileRefactorSyncConverged,
23
+ buildCanonicalRefactorPlan,
24
+ buildDurableFileRefactorApplyDeps,
24
25
  buildRefactorWarnings,
26
+ FILE_REFACTOR_APPLY_CONFIRMATION,
27
+ FILE_REFACTOR_SCHEMA_VERSION,
28
+ parseRefactorApplyConfirmation,
25
29
  planCreateFolder,
26
30
  planDuplicateRefactor,
27
- planMoveRefactor,
28
- planRenameRefactor,
31
+ resolveMoveTarget,
32
+ resolveRenameTarget,
33
+ type FileRefactorApplyResult,
34
+ type FileRefactorPreviewPlan,
29
35
  } from "../../core/file-refactors";
30
36
  import { recordContentMutation } from "../../core/mutation-generations";
31
37
  import { defaultSyncService, withContentTypeRules } from "../../ingestion";
@@ -37,23 +43,72 @@ interface CreateFolderInput {
37
43
  parentPath?: string;
38
44
  }
39
45
 
40
- interface RenameNoteInput {
41
- ref: string;
42
- name: string;
43
- }
44
-
45
- interface MoveNoteInput {
46
- ref: string;
47
- folderPath: string;
48
- name?: string;
49
- }
50
-
51
46
  interface DuplicateNoteInput {
52
47
  ref: string;
53
48
  folderPath?: string;
54
49
  name?: string;
55
50
  }
56
51
 
52
+ const refactorPreviewActionSchema = z.literal("preview");
53
+ const refactorApplyActionSchema = z.literal("apply");
54
+
55
+ export const renameNoteInputSchema = z.discriminatedUnion("action", [
56
+ z.object({
57
+ action: refactorPreviewActionSchema,
58
+ ref: z.string().min(1, "ref cannot be empty"),
59
+ name: z.string().min(1, "name cannot be empty"),
60
+ }),
61
+ z.object({
62
+ action: refactorApplyActionSchema,
63
+ ref: z.string().min(1, "ref cannot be empty"),
64
+ name: z.string().min(1, "name cannot be empty"),
65
+ schemaVersion: z.literal(FILE_REFACTOR_SCHEMA_VERSION),
66
+ planDigest: z
67
+ .string()
68
+ .regex(/^[a-f0-9]{64}$/, "planDigest must be a lowercase SHA-256"),
69
+ confirmation: z.literal(FILE_REFACTOR_APPLY_CONFIRMATION),
70
+ confirm: z.literal(true),
71
+ }),
72
+ ]);
73
+
74
+ export const moveNoteInputSchema = z.discriminatedUnion("action", [
75
+ z.object({
76
+ action: refactorPreviewActionSchema,
77
+ ref: z.string().min(1, "ref cannot be empty"),
78
+ folderPath: z.string().min(1, "folderPath cannot be empty"),
79
+ name: z.string().optional(),
80
+ }),
81
+ z.object({
82
+ action: refactorApplyActionSchema,
83
+ ref: z.string().min(1, "ref cannot be empty"),
84
+ folderPath: z.string().min(1, "folderPath cannot be empty"),
85
+ name: z.string().optional(),
86
+ schemaVersion: z.literal(FILE_REFACTOR_SCHEMA_VERSION),
87
+ planDigest: z
88
+ .string()
89
+ .regex(/^[a-f0-9]{64}$/, "planDigest must be a lowercase SHA-256"),
90
+ confirmation: z.literal(FILE_REFACTOR_APPLY_CONFIRMATION),
91
+ confirm: z.literal(true),
92
+ }),
93
+ ]);
94
+
95
+ export type RenameNoteInput = z.infer<typeof renameNoteInputSchema>;
96
+ export type MoveNoteInput = z.infer<typeof moveNoteInputSchema>;
97
+
98
+ export const RENAME_NOTE_MCP_ANNOTATIONS = {
99
+ readOnlyHint: false,
100
+ destructiveHint: true,
101
+ idempotentHint: false,
102
+ openWorldHint: false,
103
+ } as const;
104
+
105
+ export const MOVE_NOTE_MCP_ANNOTATIONS = {
106
+ readOnlyHint: false,
107
+ destructiveHint: true,
108
+ idempotentHint: false,
109
+ openWorldHint: false,
110
+ } as const;
111
+
57
112
  function resolveCollection(ctx: ToolContext, name: string): Collection {
58
113
  const normalized = name.trim().toLowerCase();
59
114
  const collection = ctx.collections.find((entry) => entry.name === normalized);
@@ -156,6 +211,136 @@ function ensureEditable(doc: {
156
211
  }`
157
212
  );
158
213
  }
214
+ return capabilities;
215
+ }
216
+
217
+ function requireWriteEnabled(ctx: ToolContext): void {
218
+ if (!ctx.enableWrite) {
219
+ throw new Error("Write tools disabled. Start MCP with --enable-write.");
220
+ }
221
+ }
222
+
223
+ function requireApplyConfirmation(args: {
224
+ schemaVersion: unknown;
225
+ planDigest: unknown;
226
+ confirmation: unknown;
227
+ confirm: unknown;
228
+ }) {
229
+ if (args.confirm !== true) {
230
+ throw new Error(
231
+ `${MCP_ERRORS.INVALID_INPUT.code}: confirm must be true to apply`
232
+ );
233
+ }
234
+ const parsed = parseRefactorApplyConfirmation(args);
235
+ if ("error" in parsed) {
236
+ throw new Error(`${parsed.error}: ${parsed.message}`);
237
+ }
238
+ return parsed;
239
+ }
240
+
241
+ function formatPreviewSummary(plan: FileRefactorPreviewPlan): string {
242
+ return `Preview ${plan.operation}: ${plan.source.relPath} → ${plan.target.relPath} (digest ${plan.planDigest.slice(0, 12)}…, canApply=${plan.canApply})`;
243
+ }
244
+
245
+ function formatApplySummary(result: FileRefactorApplyResult): string {
246
+ return `${result.operation} ${result.status}: ${result.source.relPath} → ${result.target.relPath}`;
247
+ }
248
+
249
+ async function previewOrApplyRename(
250
+ args: RenameNoteInput,
251
+ ctx: ToolContext
252
+ ): Promise<FileRefactorPreviewPlan | FileRefactorApplyResult> {
253
+ requireWriteEnabled(ctx);
254
+ const doc = await resolveDocByRef(ctx, args.ref);
255
+ const capabilities = ensureEditable(doc);
256
+ const collection = resolveCollection(ctx, doc.collection);
257
+ const target = resolveRenameTarget({
258
+ collection: collection.name,
259
+ currentRelPath: doc.relPath,
260
+ nextName: args.name,
261
+ });
262
+ const plan = await buildCanonicalRefactorPlan({
263
+ operation: "rename",
264
+ doc,
265
+ collection,
266
+ sourceFullPath: join(collection.path, doc.relPath),
267
+ target,
268
+ store: ctx.store,
269
+ sourceEditable: capabilities.editable,
270
+ });
271
+
272
+ if (args.action === "preview") {
273
+ return plan;
274
+ }
275
+
276
+ const confirmation = requireApplyConfirmation(args);
277
+ // Collection-scoped lock lives inside applyFileRefactor — do not wrap with
278
+ // ctx.writeLockPath (avoids double-locking incompatible with REST/SDK).
279
+ return applyCanonicalFileRefactor({
280
+ plan,
281
+ confirmation,
282
+ deps: buildDurableFileRefactorApplyDeps({
283
+ collection,
284
+ store: ctx.store,
285
+ syncAfterCommit: async () => {
286
+ const syncResult = await defaultSyncService.syncCollection(
287
+ collection,
288
+ ctx.store,
289
+ withContentTypeRules({ runUpdateCmd: false }, ctx.config)
290
+ );
291
+ assertFileRefactorSyncConverged(syncResult);
292
+ recordContentMutation(syncResult, ctx.markContentMutation);
293
+ },
294
+ }),
295
+ });
296
+ }
297
+
298
+ async function previewOrApplyMove(
299
+ args: MoveNoteInput,
300
+ ctx: ToolContext
301
+ ): Promise<FileRefactorPreviewPlan | FileRefactorApplyResult> {
302
+ requireWriteEnabled(ctx);
303
+ const doc = await resolveDocByRef(ctx, args.ref);
304
+ const capabilities = ensureEditable(doc);
305
+ const collection = resolveCollection(ctx, doc.collection);
306
+ const target = resolveMoveTarget({
307
+ collection: collection.name,
308
+ currentRelPath: doc.relPath,
309
+ folderPath: args.folderPath,
310
+ nextName: args.name,
311
+ });
312
+ const plan = await buildCanonicalRefactorPlan({
313
+ operation: "move",
314
+ doc,
315
+ collection,
316
+ sourceFullPath: join(collection.path, doc.relPath),
317
+ target,
318
+ store: ctx.store,
319
+ sourceEditable: capabilities.editable,
320
+ });
321
+
322
+ if (args.action === "preview") {
323
+ return plan;
324
+ }
325
+
326
+ const confirmation = requireApplyConfirmation(args);
327
+ return applyCanonicalFileRefactor({
328
+ plan,
329
+ confirmation,
330
+ deps: buildDurableFileRefactorApplyDeps({
331
+ collection,
332
+ store: ctx.store,
333
+ syncAfterCommit: async () => {
334
+ const syncResult = await defaultSyncService.syncCollection(
335
+ collection,
336
+ ctx.store,
337
+ withContentTypeRules({ runUpdateCmd: false }, ctx.config)
338
+ );
339
+ assertFileRefactorSyncConverged(syncResult);
340
+ recordContentMutation(syncResult, ctx.markContentMutation);
341
+ },
342
+ }),
343
+ });
159
344
  }
160
345
 
161
346
  export function handleCreateFolder(
@@ -166,9 +351,7 @@ export function handleCreateFolder(
166
351
  ctx,
167
352
  "gno_create_folder",
168
353
  async () => {
169
- if (!ctx.enableWrite) {
170
- throw new Error("Write tools disabled. Start MCP with --enable-write.");
171
- }
354
+ requireWriteEnabled(ctx);
172
355
 
173
356
  return withWriteLock(ctx.writeLockPath, async () => {
174
357
  const collection = resolveCollection(ctx, args.collection);
@@ -196,40 +379,11 @@ export function handleRenameNote(
196
379
  return runTool(
197
380
  ctx,
198
381
  "gno_rename_note",
199
- async () => {
200
- if (!ctx.enableWrite) {
201
- throw new Error("Write tools disabled. Start MCP with --enable-write.");
202
- }
203
-
204
- return withWriteLock(ctx.writeLockPath, async () => {
205
- const doc = await resolveDocByRef(ctx, args.ref);
206
- ensureEditable(doc);
207
- const collection = resolveCollection(ctx, doc.collection);
208
- const plan = planRenameRefactor({
209
- collection: collection.name,
210
- currentRelPath: doc.relPath,
211
- nextName: args.name,
212
- });
213
- const currentPath = join(collection.path, doc.relPath);
214
- const nextPath = join(collection.path, plan.nextRelPath);
215
- await renameFilePath(currentPath, nextPath);
216
- const syncResult = await defaultSyncService.syncCollection(
217
- collection,
218
- ctx.store,
219
- withContentTypeRules({ runUpdateCmd: false }, ctx.config)
220
- );
221
- recordContentMutation(syncResult, ctx.markContentMutation);
222
- return {
223
- uri: plan.nextUri,
224
- relPath: plan.nextRelPath,
225
- warnings: buildRefactorWarnings(
226
- await getRefactorSnapshot(ctx, doc.id),
227
- { filenameChanged: true }
228
- ).warnings,
229
- };
230
- });
231
- },
232
- (data) => `Renamed note to ${data.relPath}`
382
+ async () => previewOrApplyRename(args, ctx),
383
+ (data) =>
384
+ "planDigest" in data && "canApply" in data
385
+ ? formatPreviewSummary(data)
386
+ : formatApplySummary(data)
233
387
  );
234
388
  }
235
389
 
@@ -240,45 +394,11 @@ export function handleMoveNote(
240
394
  return runTool(
241
395
  ctx,
242
396
  "gno_move_note",
243
- async () => {
244
- if (!ctx.enableWrite) {
245
- throw new Error("Write tools disabled. Start MCP with --enable-write.");
246
- }
247
-
248
- return withWriteLock(ctx.writeLockPath, async () => {
249
- const doc = await resolveDocByRef(ctx, args.ref);
250
- ensureEditable(doc);
251
- const collection = resolveCollection(ctx, doc.collection);
252
- const plan = planMoveRefactor({
253
- collection: collection.name,
254
- currentRelPath: doc.relPath,
255
- folderPath: args.folderPath,
256
- nextName: args.name,
257
- });
258
- const currentPath = join(collection.path, doc.relPath);
259
- const nextPath = join(collection.path, plan.nextRelPath);
260
- await mkdir(dirname(nextPath), { recursive: true });
261
- await renameFilePath(currentPath, nextPath);
262
- const syncResult = await defaultSyncService.syncCollection(
263
- collection,
264
- ctx.store,
265
- withContentTypeRules({ runUpdateCmd: false }, ctx.config)
266
- );
267
- recordContentMutation(syncResult, ctx.markContentMutation);
268
- return {
269
- uri: plan.nextUri,
270
- relPath: plan.nextRelPath,
271
- warnings: buildRefactorWarnings(
272
- await getRefactorSnapshot(ctx, doc.id),
273
- {
274
- folderChanged: true,
275
- filenameChanged: Boolean(args.name),
276
- }
277
- ).warnings,
278
- };
279
- });
280
- },
281
- (data) => `Moved note to ${data.relPath}`
397
+ async () => previewOrApplyMove(args, ctx),
398
+ (data) =>
399
+ "planDigest" in data && "canApply" in data
400
+ ? formatPreviewSummary(data)
401
+ : formatApplySummary(data)
282
402
  );
283
403
  }
284
404
 
@@ -290,9 +410,7 @@ export function handleDuplicateNote(
290
410
  ctx,
291
411
  "gno_duplicate_note",
292
412
  async () => {
293
- if (!ctx.enableWrite) {
294
- throw new Error("Write tools disabled. Start MCP with --enable-write.");
295
- }
413
+ requireWriteEnabled(ctx);
296
414
 
297
415
  return withWriteLock(ctx.writeLockPath, async () => {
298
416
  const doc = await resolveDocByRef(ctx, args.ref);