@gmickel/gno 1.31.0 → 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 (53) hide show
  1. package/README.md +5 -4
  2. package/assets/skill/SKILL.md +23 -0
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.31.0.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +1 -1
  7. package/spec/cli.md +19 -0
  8. package/spec/db/schema.sql +55 -0
  9. package/spec/mcp.md +114 -11
  10. package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
  11. package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
  12. package/src/core/document-capabilities.ts +13 -0
  13. package/src/core/file-ops.ts +129 -1
  14. package/src/core/file-refactor-adapter.ts +329 -0
  15. package/src/core/file-refactor-apply-edits.ts +61 -0
  16. package/src/core/file-refactor-apply-fs.ts +512 -0
  17. package/src/core/file-refactor-apply-safety.ts +340 -0
  18. package/src/core/file-refactor-apply-validate.ts +401 -0
  19. package/src/core/file-refactor-contract.ts +486 -0
  20. package/src/core/file-refactor-destination.ts +123 -0
  21. package/src/core/file-refactor-from-snapshot.ts +148 -0
  22. package/src/core/file-refactor-journal-port.ts +150 -0
  23. package/src/core/file-refactor-journal.ts +347 -0
  24. package/src/core/file-refactor-paths.ts +60 -0
  25. package/src/core/file-refactor-plan-classify.ts +208 -0
  26. package/src/core/file-refactor-plan-validate.ts +169 -0
  27. package/src/core/file-refactor-planner-types.ts +62 -0
  28. package/src/core/file-refactor-planner.ts +423 -0
  29. package/src/core/file-refactor-resolve.ts +280 -0
  30. package/src/core/file-refactor-service.ts +468 -0
  31. package/src/core/file-refactors.ts +84 -56
  32. package/src/core/link-destination-parse.ts +275 -0
  33. package/src/core/link-inventory-markdown.ts +454 -0
  34. package/src/core/link-inventory-opaque.ts +244 -0
  35. package/src/core/link-inventory-types.ts +47 -0
  36. package/src/core/link-inventory.ts +182 -0
  37. package/src/core/link-relevance.ts +150 -0
  38. package/src/mcp/tools/index.ts +18 -17
  39. package/src/mcp/tools/workspace-write.ts +215 -97
  40. package/src/sdk/client.ts +167 -115
  41. package/src/sdk/index.ts +7 -0
  42. package/src/sdk/types.ts +32 -2
  43. package/src/serve/file-refactor-http.ts +239 -0
  44. package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
  45. package/src/serve/public/globals.built.css +1 -1
  46. package/src/serve/public/pages/DocView.tsx +176 -41
  47. package/src/serve/routes/api.ts +191 -104
  48. package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
  49. package/src/store/migrations/index.ts +2 -0
  50. package/src/store/sqlite/adapter.ts +452 -0
  51. package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
  52. package/src/store/types.ts +84 -0
  53. package/browser-extension/artifacts/gno-browser-clipper-v1.31.0.zip.sha256 +0 -1
@@ -31,6 +31,10 @@ import {
31
31
 
32
32
  import type { PdfFallbackReason } from "../lib/pdf";
33
33
 
34
+ import {
35
+ FILE_REFACTOR_APPLY_CONFIRMATION,
36
+ type FileRefactorPreviewPlan,
37
+ } from "../../../core/file-refactor-contract";
34
38
  import { extractSections } from "../../../core/sections";
35
39
  import {
36
40
  CodeBlock,
@@ -47,6 +51,7 @@ import {
47
51
  OutgoingLinksPanel,
48
52
  type OutgoingLink,
49
53
  } from "../components/OutgoingLinksPanel";
54
+ import { RefactorImpactPreview } from "../components/RefactorImpactPreview";
50
55
  import { RelatedNotesSidebar } from "../components/RelatedNotesSidebar";
51
56
  import { TagInput } from "../components/TagInput";
52
57
  import { Badge } from "../components/ui/badge";
@@ -199,6 +204,9 @@ interface RenameDocResponse {
199
204
  uri: string;
200
205
  path: string;
201
206
  relPath: string;
207
+ planDigest?: string;
208
+ status?: string;
209
+ warning?: string;
202
210
  refactorWarnings?: {
203
211
  warnings: string[];
204
212
  };
@@ -209,6 +217,9 @@ interface MoveDocResponse {
209
217
  uri: string;
210
218
  path: string;
211
219
  relPath: string;
220
+ planDigest?: string;
221
+ status?: string;
222
+ warning?: string;
212
223
  refactorWarnings?: {
213
224
  warnings: string[];
214
225
  };
@@ -359,13 +370,23 @@ export default function DocView({ navigate }: PageProps) {
359
370
  const [renaming, setRenaming] = useState(false);
360
371
  const [renameError, setRenameError] = useState<string | null>(null);
361
372
  const [renameValue, setRenameValue] = useState("");
362
- const [renameWarnings, setRenameWarnings] = useState<string[]>([]);
373
+ const [renamePlan, setRenamePlan] = useState<FileRefactorPreviewPlan | null>(
374
+ null
375
+ );
376
+ const [renamePlanLoading, setRenamePlanLoading] = useState(false);
377
+ const [renameConfirmed, setRenameConfirmed] = useState(false);
378
+ const [renameOutcome, setRenameOutcome] = useState<string | null>(null);
363
379
  const [moveDialogOpen, setMoveDialogOpen] = useState(false);
364
380
  const [moving, setMoving] = useState(false);
365
381
  const [moveError, setMoveError] = useState<string | null>(null);
366
382
  const [moveFolderPath, setMoveFolderPath] = useState("");
367
383
  const [moveName, setMoveName] = useState("");
368
- const [moveWarnings, setMoveWarnings] = useState<string[]>([]);
384
+ const [movePlan, setMovePlan] = useState<FileRefactorPreviewPlan | null>(
385
+ null
386
+ );
387
+ const [movePlanLoading, setMovePlanLoading] = useState(false);
388
+ const [moveConfirmed, setMoveConfirmed] = useState(false);
389
+ const [moveOutcome, setMoveOutcome] = useState<string | null>(null);
369
390
  const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
370
391
  const [duplicating, setDuplicating] = useState(false);
371
392
  const [duplicateError, setDuplicateError] = useState<string | null>(null);
@@ -875,7 +896,9 @@ export default function DocView({ navigate }: PageProps) {
875
896
  const filename = doc.relPath.split("/").pop() ?? doc.relPath;
876
897
  setRenameValue(filename);
877
898
  setRenameError(null);
878
- setRenameWarnings([]);
899
+ setRenamePlan(null);
900
+ setRenameConfirmed(false);
901
+ setRenameOutcome(null);
879
902
  setRenameDialogOpen(true);
880
903
  }, [doc]);
881
904
 
@@ -883,7 +906,11 @@ export default function DocView({ navigate }: PageProps) {
883
906
  if (!renameDialogOpen || !doc || !renameValue.trim()) {
884
907
  return;
885
908
  }
886
- void apiFetch<{ refactorWarnings?: { warnings: string[] } }>(
909
+ let cancelled = false;
910
+ setRenamePlanLoading(true);
911
+ setRenamePlan(null);
912
+ setRenameConfirmed(false);
913
+ void apiFetch<FileRefactorPreviewPlan>(
887
914
  `/api/docs/${encodeURIComponent(doc.docid)}/refactor-plan`,
888
915
  {
889
916
  method: "POST",
@@ -893,22 +920,42 @@ export default function DocView({ navigate }: PageProps) {
893
920
  uri: doc.uri,
894
921
  }),
895
922
  }
896
- ).then(({ data }) => {
897
- setRenameWarnings(data?.refactorWarnings?.warnings ?? []);
923
+ ).then(({ data, error: err }) => {
924
+ if (cancelled) {
925
+ return;
926
+ }
927
+ setRenamePlanLoading(false);
928
+ if (err) {
929
+ setRenameError(err);
930
+ setRenamePlan(null);
931
+ return;
932
+ }
933
+ setRenameError(null);
934
+ setRenamePlan(data);
898
935
  });
936
+ return () => {
937
+ cancelled = true;
938
+ };
899
939
  }, [doc, renameDialogOpen, renameValue]);
900
940
 
901
941
  const handleRename = useCallback(async () => {
902
- if (!doc) {
942
+ if (!doc || !renamePlan?.canApply || !renameConfirmed) {
903
943
  return;
904
944
  }
905
945
  setRenaming(true);
906
946
  setRenameError(null);
947
+ setRenameOutcome(null);
907
948
  const { data, error: err } = await apiFetch<RenameDocResponse>(
908
949
  `/api/docs/${encodeURIComponent(doc.docid)}/rename`,
909
950
  {
910
951
  method: "POST",
911
- body: JSON.stringify({ name: renameValue, uri: doc.uri }),
952
+ body: JSON.stringify({
953
+ name: renameValue,
954
+ uri: doc.uri,
955
+ planDigest: renamePlan.planDigest,
956
+ confirmation: FILE_REFACTOR_APPLY_CONFIRMATION,
957
+ schemaVersion: renamePlan.schemaVersion,
958
+ }),
912
959
  }
913
960
  );
914
961
  setRenaming(false);
@@ -918,11 +965,19 @@ export default function DocView({ navigate }: PageProps) {
918
965
  return;
919
966
  }
920
967
 
968
+ if (data?.status === "applied_with_sync_pending" || data?.warning) {
969
+ setRenameOutcome(
970
+ data.warning ??
971
+ "Files renamed, but index sync is still pending. Run Update All to finish."
972
+ );
973
+ return;
974
+ }
975
+
921
976
  setRenameDialogOpen(false);
922
977
  if (data?.uri) {
923
978
  navigate(`/doc?uri=${encodeURIComponent(data.uri)}`);
924
979
  }
925
- }, [doc, navigate, renameValue]);
980
+ }, [doc, navigate, renameConfirmed, renamePlan, renameValue]);
926
981
 
927
982
  const handleStartMove = useCallback(() => {
928
983
  if (!doc) {
@@ -931,7 +986,9 @@ export default function DocView({ navigate }: PageProps) {
931
986
  setMoveFolderPath(getParentPath(doc.relPath));
932
987
  setMoveName(doc.relPath.split("/").pop() ?? doc.relPath);
933
988
  setMoveError(null);
934
- setMoveWarnings([]);
989
+ setMovePlan(null);
990
+ setMoveConfirmed(false);
991
+ setMoveOutcome(null);
935
992
  setMoveDialogOpen(true);
936
993
  }, [doc]);
937
994
 
@@ -939,7 +996,11 @@ export default function DocView({ navigate }: PageProps) {
939
996
  if (!moveDialogOpen || !doc || !moveFolderPath.trim()) {
940
997
  return;
941
998
  }
942
- void apiFetch<{ refactorWarnings?: { warnings: string[] } }>(
999
+ let cancelled = false;
1000
+ setMovePlanLoading(true);
1001
+ setMovePlan(null);
1002
+ setMoveConfirmed(false);
1003
+ void apiFetch<FileRefactorPreviewPlan>(
943
1004
  `/api/docs/${encodeURIComponent(doc.docid)}/refactor-plan`,
944
1005
  {
945
1006
  method: "POST",
@@ -950,17 +1011,31 @@ export default function DocView({ navigate }: PageProps) {
950
1011
  uri: doc.uri,
951
1012
  }),
952
1013
  }
953
- ).then(({ data }) => {
954
- setMoveWarnings(data?.refactorWarnings?.warnings ?? []);
1014
+ ).then(({ data, error: err }) => {
1015
+ if (cancelled) {
1016
+ return;
1017
+ }
1018
+ setMovePlanLoading(false);
1019
+ if (err) {
1020
+ setMoveError(err);
1021
+ setMovePlan(null);
1022
+ return;
1023
+ }
1024
+ setMoveError(null);
1025
+ setMovePlan(data);
955
1026
  });
1027
+ return () => {
1028
+ cancelled = true;
1029
+ };
956
1030
  }, [doc, moveDialogOpen, moveFolderPath, moveName]);
957
1031
 
958
1032
  const handleMove = useCallback(async () => {
959
- if (!doc) {
1033
+ if (!doc || !movePlan?.canApply || !moveConfirmed) {
960
1034
  return;
961
1035
  }
962
1036
  setMoving(true);
963
1037
  setMoveError(null);
1038
+ setMoveOutcome(null);
964
1039
  const { data, error: err } = await apiFetch<MoveDocResponse>(
965
1040
  `/api/docs/${encodeURIComponent(doc.docid)}/move`,
966
1041
  {
@@ -969,6 +1044,9 @@ export default function DocView({ navigate }: PageProps) {
969
1044
  folderPath: moveFolderPath,
970
1045
  name: moveName,
971
1046
  uri: doc.uri,
1047
+ planDigest: movePlan.planDigest,
1048
+ confirmation: FILE_REFACTOR_APPLY_CONFIRMATION,
1049
+ schemaVersion: movePlan.schemaVersion,
972
1050
  }),
973
1051
  }
974
1052
  );
@@ -979,11 +1057,19 @@ export default function DocView({ navigate }: PageProps) {
979
1057
  return;
980
1058
  }
981
1059
 
1060
+ if (data?.status === "applied_with_sync_pending" || data?.warning) {
1061
+ setMoveOutcome(
1062
+ data.warning ??
1063
+ "Files moved, but index sync is still pending. Run Update All to finish."
1064
+ );
1065
+ return;
1066
+ }
1067
+
982
1068
  setMoveDialogOpen(false);
983
1069
  if (data?.uri) {
984
1070
  navigate(`/doc?uri=${encodeURIComponent(data.uri)}`);
985
1071
  }
986
- }, [doc, moveFolderPath, moveName, navigate]);
1072
+ }, [doc, moveConfirmed, moveFolderPath, moveName, movePlan, navigate]);
987
1073
 
988
1074
  const handleStartDuplicate = useCallback(() => {
989
1075
  if (!doc) {
@@ -2207,39 +2293,61 @@ export default function DocView({ navigate }: PageProps) {
2207
2293
  </Dialog>
2208
2294
 
2209
2295
  <Dialog onOpenChange={setRenameDialogOpen} open={renameDialogOpen}>
2210
- <DialogContent>
2296
+ <DialogContent className="sm:max-w-lg">
2211
2297
  <DialogHeader>
2212
2298
  <DialogTitle>Rename document</DialogTitle>
2213
2299
  <DialogDescription>
2214
- Rename the file on disk inside its current folder. This does not
2215
- move it to another collection yet.
2300
+ Rename the file on disk inside its current folder. Reference
2301
+ rewrites follow the exact plan digest below.
2216
2302
  </DialogDescription>
2217
2303
  </DialogHeader>
2218
2304
  <input
2305
+ aria-label="New file name"
2219
2306
  className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm"
2220
2307
  onChange={(event) => setRenameValue(event.target.value)}
2221
2308
  value={renameValue}
2222
2309
  />
2310
+ <RefactorImpactPreview
2311
+ confirmed={renameConfirmed}
2312
+ loading={renamePlanLoading}
2313
+ onConfirmedChange={setRenameConfirmed}
2314
+ outcomeMessage={renameOutcome}
2315
+ outcomeTone="warning"
2316
+ plan={renamePlan}
2317
+ />
2223
2318
  {renameError && (
2224
- <div className="rounded-lg bg-destructive/10 p-3 text-destructive text-sm">
2319
+ <div
2320
+ className="rounded-lg bg-destructive/10 p-3 text-destructive text-sm"
2321
+ role="alert"
2322
+ >
2225
2323
  {renameError}
2226
2324
  </div>
2227
2325
  )}
2228
- {renameWarnings.length > 0 && (
2229
- <div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-500 text-sm">
2230
- {renameWarnings.map((warning) => (
2231
- <div key={warning}>{warning}</div>
2232
- ))}
2233
- </div>
2234
- )}
2235
2326
  <DialogFooter className="gap-2 sm:gap-0">
2236
2327
  <Button
2237
- onClick={() => setRenameDialogOpen(false)}
2328
+ onClick={() => {
2329
+ if (renameOutcome && renamePlan) {
2330
+ setRenameDialogOpen(false);
2331
+ navigate(
2332
+ `/doc?uri=${encodeURIComponent(renamePlan.target.uri)}`
2333
+ );
2334
+ return;
2335
+ }
2336
+ setRenameDialogOpen(false);
2337
+ }}
2238
2338
  variant="outline"
2239
2339
  >
2240
- Cancel
2340
+ {renameOutcome ? "Open note" : "Cancel"}
2241
2341
  </Button>
2242
- <Button disabled={renaming} onClick={() => void handleRename()}>
2342
+ <Button
2343
+ disabled={
2344
+ renaming ||
2345
+ !renamePlan?.canApply ||
2346
+ !renameConfirmed ||
2347
+ Boolean(renameOutcome)
2348
+ }
2349
+ onClick={() => void handleRename()}
2350
+ >
2243
2351
  {renaming && (
2244
2352
  <Loader2Icon className="mr-1.5 size-4 animate-spin" />
2245
2353
  )}
@@ -2250,42 +2358,69 @@ export default function DocView({ navigate }: PageProps) {
2250
2358
  </Dialog>
2251
2359
 
2252
2360
  <Dialog onOpenChange={setMoveDialogOpen} open={moveDialogOpen}>
2253
- <DialogContent>
2361
+ <DialogContent className="sm:max-w-lg">
2254
2362
  <DialogHeader>
2255
2363
  <DialogTitle>Move document</DialogTitle>
2256
2364
  <DialogDescription>
2257
2365
  Move the current note to another folder inside this collection.
2366
+ Reference rewrites follow the exact plan digest below.
2258
2367
  </DialogDescription>
2259
2368
  </DialogHeader>
2260
2369
  <div className="space-y-3">
2261
2370
  <Input
2371
+ aria-label="Destination folder path"
2262
2372
  onChange={(event) => setMoveFolderPath(event.target.value)}
2263
2373
  placeholder="projects/research"
2264
2374
  value={moveFolderPath}
2265
2375
  />
2266
2376
  <Input
2377
+ aria-label="File name"
2267
2378
  onChange={(event) => setMoveName(event.target.value)}
2268
2379
  placeholder="note.md"
2269
2380
  value={moveName}
2270
2381
  />
2382
+ <RefactorImpactPreview
2383
+ confirmed={moveConfirmed}
2384
+ loading={movePlanLoading}
2385
+ onConfirmedChange={setMoveConfirmed}
2386
+ outcomeMessage={moveOutcome}
2387
+ outcomeTone="warning"
2388
+ plan={movePlan}
2389
+ />
2271
2390
  {moveError && (
2272
- <div className="rounded-lg bg-destructive/10 p-3 text-destructive text-sm">
2391
+ <div
2392
+ className="rounded-lg bg-destructive/10 p-3 text-destructive text-sm"
2393
+ role="alert"
2394
+ >
2273
2395
  {moveError}
2274
2396
  </div>
2275
2397
  )}
2276
- {moveWarnings.length > 0 && (
2277
- <div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-500 text-sm">
2278
- {moveWarnings.map((warning) => (
2279
- <div key={warning}>{warning}</div>
2280
- ))}
2281
- </div>
2282
- )}
2283
2398
  </div>
2284
2399
  <DialogFooter className="gap-2 sm:gap-0">
2285
- <Button onClick={() => setMoveDialogOpen(false)} variant="outline">
2286
- Cancel
2400
+ <Button
2401
+ onClick={() => {
2402
+ if (moveOutcome && movePlan) {
2403
+ setMoveDialogOpen(false);
2404
+ navigate(
2405
+ `/doc?uri=${encodeURIComponent(movePlan.target.uri)}`
2406
+ );
2407
+ return;
2408
+ }
2409
+ setMoveDialogOpen(false);
2410
+ }}
2411
+ variant="outline"
2412
+ >
2413
+ {moveOutcome ? "Open note" : "Cancel"}
2287
2414
  </Button>
2288
- <Button disabled={moving} onClick={() => void handleMove()}>
2415
+ <Button
2416
+ disabled={
2417
+ moving ||
2418
+ !movePlan?.canApply ||
2419
+ !moveConfirmed ||
2420
+ Boolean(moveOutcome)
2421
+ }
2422
+ onClick={() => void handleMove()}
2423
+ >
2289
2424
  {moving && <Loader2Icon className="mr-1.5 size-4 animate-spin" />}
2290
2425
  Move
2291
2426
  </Button>