@gmickel/gno 1.7.1 → 1.9.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 (51) hide show
  1. package/README.md +10 -1
  2. package/assets/skill/SKILL.md +41 -0
  3. package/assets/skill/cli-reference.md +34 -0
  4. package/assets/skill/examples.md +37 -0
  5. package/assets/skill/mcp-reference.md +21 -0
  6. package/package.json +1 -1
  7. package/src/cli/commands/capture.ts +282 -0
  8. package/src/cli/commands/index-cmd.ts +17 -6
  9. package/src/cli/commands/shared.ts +17 -1
  10. package/src/cli/commands/update.ts +17 -6
  11. package/src/cli/options.ts +2 -0
  12. package/src/cli/program.ts +64 -0
  13. package/src/config/content-types.ts +140 -0
  14. package/src/config/defaults.ts +1 -0
  15. package/src/config/index.ts +14 -0
  16. package/src/config/loader.ts +11 -2
  17. package/src/config/types.ts +37 -1
  18. package/src/core/capture-write.ts +38 -0
  19. package/src/core/capture.ts +746 -0
  20. package/src/core/config-mutation.ts +14 -2
  21. package/src/core/file-ops.ts +21 -2
  22. package/src/core/note-presets.ts +61 -5
  23. package/src/ingestion/frontmatter.ts +77 -2
  24. package/src/ingestion/index.ts +2 -0
  25. package/src/ingestion/sync-options.ts +29 -0
  26. package/src/ingestion/sync.ts +104 -16
  27. package/src/ingestion/types.ts +14 -0
  28. package/src/mcp/tools/add-collection.ts +8 -5
  29. package/src/mcp/tools/capture.ts +137 -191
  30. package/src/mcp/tools/index-cmd.ts +13 -7
  31. package/src/mcp/tools/index.ts +43 -9
  32. package/src/mcp/tools/sync.ts +12 -6
  33. package/src/mcp/tools/workspace-write.ts +16 -10
  34. package/src/pipeline/hybrid.ts +2 -0
  35. package/src/pipeline/search.ts +2 -0
  36. package/src/pipeline/types.ts +2 -0
  37. package/src/pipeline/vsearch.ts +4 -0
  38. package/src/sdk/client.ts +156 -20
  39. package/src/sdk/index.ts +2 -0
  40. package/src/sdk/types.ts +6 -0
  41. package/src/serve/background-runtime.ts +18 -4
  42. package/src/serve/config-sync.ts +9 -2
  43. package/src/serve/public/components/CaptureModal.tsx +248 -10
  44. package/src/serve/public/globals.built.css +1 -1
  45. package/src/serve/routes/api.ts +238 -26
  46. package/src/serve/server.ts +12 -0
  47. package/src/serve/watch-service.ts +12 -2
  48. package/src/store/migrations/009-content-type-rule-fingerprint.ts +36 -0
  49. package/src/store/migrations/index.ts +12 -1
  50. package/src/store/sqlite/adapter.ts +29 -14
  51. package/src/store/types.ts +6 -0
@@ -2,10 +2,10 @@
2
2
  * CaptureModal - Quick document creation modal.
3
3
  *
4
4
  * Features:
5
- * - Title, content, collection fields
5
+ * - Title, content, collection, provenance fields
6
6
  * - Auto-generates filename from title
7
7
  * - Remembers last used collection
8
- * - Shows IndexingProgress after creation
8
+ * - Shows capture receipt status after creation
9
9
  */
10
10
 
11
11
  import {
@@ -17,6 +17,7 @@ import {
17
17
  } from "lucide-react";
18
18
  import { useCallback, useEffect, useRef, useState } from "react";
19
19
 
20
+ import type { CaptureReceipt, CaptureSourceKind } from "../../../core/capture";
20
21
  import type { WikiLinkDoc } from "./WikiLinkAutocomplete";
21
22
 
22
23
  import {
@@ -80,6 +81,8 @@ interface CreateDocResponse {
80
81
  relPath?: string;
81
82
  }
82
83
 
84
+ type CaptureResponse = CaptureReceipt;
85
+
83
86
  interface CollectionsResponse {
84
87
  collections: Collection[];
85
88
  }
@@ -102,6 +105,36 @@ function sanitizeFilename(title: string): string {
102
105
 
103
106
  type ModalState = "form" | "submitting" | "success" | "error";
104
107
 
108
+ const SOURCE_KINDS: CaptureSourceKind[] = [
109
+ "direct",
110
+ "web",
111
+ "email",
112
+ "meeting",
113
+ "chat",
114
+ "file",
115
+ "api",
116
+ "unknown",
117
+ ];
118
+
119
+ function statusLabel(status: CaptureReceipt["sync"]["status"]): string {
120
+ switch (status) {
121
+ case "completed":
122
+ return "Completed";
123
+ case "pending":
124
+ return "Pending";
125
+ case "running":
126
+ return "Running";
127
+ case "skipped":
128
+ return "Skipped";
129
+ case "failed":
130
+ return "Failed";
131
+ case "not_requested":
132
+ return "Not requested";
133
+ default:
134
+ return "Unknown";
135
+ }
136
+ }
137
+
105
138
  export function CaptureModal({
106
139
  open,
107
140
  draftTitle = "",
@@ -118,6 +151,12 @@ export function CaptureModal({
118
151
  const [collections, setCollections] = useState<Collection[]>([]);
119
152
  const [tags, setTags] = useState<string[]>([]);
120
153
  const [selectedPresetId, setSelectedPresetId] = useState<string>(presetId);
154
+ const [sourceKind, setSourceKind] = useState<CaptureSourceKind>("direct");
155
+ const [sourceTitle, setSourceTitle] = useState("");
156
+ const [sourceUrl, setSourceUrl] = useState("");
157
+ const [sourceAuthor, setSourceAuthor] = useState("");
158
+ const [sourceObservedAt, setSourceObservedAt] = useState("");
159
+ const [sourceExternalId, setSourceExternalId] = useState("");
121
160
  const [contentTouched, setContentTouched] = useState(false);
122
161
  const [lastGeneratedContent, setLastGeneratedContent] = useState("");
123
162
  const [wikiLinkDocs, setWikiLinkDocs] = useState<WikiLinkDoc[]>([]);
@@ -136,6 +175,9 @@ export function CaptureModal({
136
175
  const [error, setError] = useState<string | null>(null);
137
176
  const [jobId, setJobId] = useState<string | null>(null);
138
177
  const [createdUri, setCreatedUri] = useState<string | null>(null);
178
+ const [captureReceipt, setCaptureReceipt] = useState<CaptureResponse | null>(
179
+ null
180
+ );
139
181
 
140
182
  // Load collections
141
183
  useEffect(() => {
@@ -181,6 +223,12 @@ export function CaptureModal({
181
223
  setTitle("");
182
224
  setContent("");
183
225
  setTags([]);
226
+ setSourceKind("direct");
227
+ setSourceTitle("");
228
+ setSourceUrl("");
229
+ setSourceAuthor("");
230
+ setSourceObservedAt("");
231
+ setSourceExternalId("");
184
232
  setContentTouched(false);
185
233
  setLastGeneratedContent("");
186
234
  setSelectedPresetId("blank");
@@ -188,6 +236,7 @@ export function CaptureModal({
188
236
  setError(null);
189
237
  setJobId(null);
190
238
  setCreatedUri(null);
239
+ setCaptureReceipt(null);
191
240
  setWikiLinkDocs([]);
192
241
  setWikiLinkOpen(false);
193
242
  setWikiLinkQuery("");
@@ -245,18 +294,35 @@ export function CaptureModal({
245
294
  setState("submitting");
246
295
  setError(null);
247
296
 
248
- // Include tags in the POST request (server writes to frontmatter)
249
- const { data, error: err } = await apiFetch<CreateDocResponse>(
250
- "/api/docs",
297
+ const source = {
298
+ kind: sourceKind,
299
+ ...(sourceTitle.trim() && { title: sourceTitle.trim() }),
300
+ ...(sourceUrl.trim() && { url: sourceUrl.trim() }),
301
+ ...(sourceAuthor.trim() && { author: sourceAuthor.trim() }),
302
+ ...(sourceObservedAt.trim() && { observedAt: sourceObservedAt.trim() }),
303
+ ...(sourceExternalId.trim() && { externalId: sourceExternalId.trim() }),
304
+ };
305
+ const presetOnly =
306
+ selectedPresetId &&
307
+ selectedPresetId !== "blank" &&
308
+ content === lastGeneratedContent;
309
+ const submitPresetId =
310
+ presetOnly && selectedPresetId && selectedPresetId !== "blank"
311
+ ? selectedPresetId
312
+ : undefined;
313
+
314
+ const { data, error: err } = await apiFetch<CaptureResponse>(
315
+ "/api/capture",
251
316
  {
252
317
  method: "POST",
253
318
  body: JSON.stringify({
254
319
  collection,
255
320
  title,
256
321
  folderPath: defaultFolderPath || undefined,
257
- content,
258
- presetId: selectedPresetId || undefined,
322
+ content: presetOnly ? undefined : content,
323
+ presetId: submitPresetId,
259
324
  collisionPolicy: "create_with_suffix",
325
+ source,
260
326
  ...(tags.length > 0 && { tags }),
261
327
  }),
262
328
  }
@@ -273,8 +339,9 @@ export function CaptureModal({
273
339
  localStorage.setItem(STORAGE_KEY, collection);
274
340
 
275
341
  setState("success");
276
- setJobId(data.jobId);
342
+ setJobId(data.sync.jobId ?? null);
277
343
  setCreatedUri(data.uri);
344
+ setCaptureReceipt(data);
278
345
  onSuccess?.(data.uri);
279
346
  }
280
347
  }, [
@@ -282,8 +349,15 @@ export function CaptureModal({
282
349
  content,
283
350
  defaultFolderPath,
284
351
  isValid,
352
+ lastGeneratedContent,
285
353
  onSuccess,
286
354
  selectedPresetId,
355
+ sourceAuthor,
356
+ sourceExternalId,
357
+ sourceKind,
358
+ sourceObservedAt,
359
+ sourceTitle,
360
+ sourceUrl,
287
361
  tags,
288
362
  title,
289
363
  ]);
@@ -552,6 +626,116 @@ export function CaptureModal({
552
626
  value={tags}
553
627
  />
554
628
  </div>
629
+
630
+ <details className="rounded-lg border border-border/50 p-3">
631
+ <summary className="cursor-pointer font-medium text-sm">
632
+ Source
633
+ </summary>
634
+ <div className="mt-3 grid gap-3">
635
+ <div>
636
+ <label
637
+ className="mb-1.5 block font-medium text-sm"
638
+ htmlFor="capture-source-kind"
639
+ >
640
+ Kind
641
+ </label>
642
+ <Select
643
+ disabled={state === "submitting"}
644
+ onValueChange={(value) =>
645
+ setSourceKind(value as CaptureSourceKind)
646
+ }
647
+ value={sourceKind}
648
+ >
649
+ <SelectTrigger className="w-full" id="capture-source-kind">
650
+ <SelectValue placeholder="Source kind" />
651
+ </SelectTrigger>
652
+ <SelectContent>
653
+ {SOURCE_KINDS.map((kind) => (
654
+ <SelectItem key={kind} value={kind}>
655
+ {kind}
656
+ </SelectItem>
657
+ ))}
658
+ </SelectContent>
659
+ </Select>
660
+ </div>
661
+ <div className="grid gap-3 sm:grid-cols-2">
662
+ <div>
663
+ <label
664
+ className="mb-1.5 block font-medium text-sm"
665
+ htmlFor="capture-source-title"
666
+ >
667
+ Source title
668
+ </label>
669
+ <Input
670
+ disabled={state === "submitting"}
671
+ id="capture-source-title"
672
+ onChange={(e) => setSourceTitle(e.target.value)}
673
+ value={sourceTitle}
674
+ />
675
+ </div>
676
+ <div>
677
+ <label
678
+ className="mb-1.5 block font-medium text-sm"
679
+ htmlFor="capture-source-author"
680
+ >
681
+ Author
682
+ </label>
683
+ <Input
684
+ disabled={state === "submitting"}
685
+ id="capture-source-author"
686
+ onChange={(e) => setSourceAuthor(e.target.value)}
687
+ value={sourceAuthor}
688
+ />
689
+ </div>
690
+ </div>
691
+ <div>
692
+ <label
693
+ className="mb-1.5 block font-medium text-sm"
694
+ htmlFor="capture-source-url"
695
+ >
696
+ URL
697
+ </label>
698
+ <Input
699
+ disabled={state === "submitting"}
700
+ id="capture-source-url"
701
+ onChange={(e) => setSourceUrl(e.target.value)}
702
+ type="url"
703
+ value={sourceUrl}
704
+ />
705
+ </div>
706
+ <div className="grid gap-3 sm:grid-cols-2">
707
+ <div>
708
+ <label
709
+ className="mb-1.5 block font-medium text-sm"
710
+ htmlFor="capture-source-observed"
711
+ >
712
+ Observed
713
+ </label>
714
+ <Input
715
+ disabled={state === "submitting"}
716
+ id="capture-source-observed"
717
+ onChange={(e) => setSourceObservedAt(e.target.value)}
718
+ type="datetime-local"
719
+ value={sourceObservedAt}
720
+ />
721
+ </div>
722
+ <div>
723
+ <label
724
+ className="mb-1.5 block font-medium text-sm"
725
+ htmlFor="capture-source-external-id"
726
+ >
727
+ External ID
728
+ </label>
729
+ <Input
730
+ disabled={state === "submitting"}
731
+ id="capture-source-external-id"
732
+ onChange={(e) => setSourceExternalId(e.target.value)}
733
+ value={sourceExternalId}
734
+ />
735
+ </div>
736
+ </div>
737
+ </div>
738
+ </details>
555
739
  </div>
556
740
  )}
557
741
 
@@ -581,13 +765,67 @@ export function CaptureModal({
581
765
  <CheckCircle2Icon className="size-5 text-green-500" />
582
766
  </div>
583
767
  <div>
584
- <h3 className="font-medium">Note created successfully</h3>
768
+ <h3 className="font-medium">
769
+ {captureReceipt?.openedExisting
770
+ ? "Opened existing note"
771
+ : "Note captured"}
772
+ </h3>
585
773
  <p className="text-muted-foreground text-sm">
586
- Indexing in progress...
774
+ {captureReceipt?.relPath ?? createdUri}
587
775
  </p>
588
776
  </div>
589
777
  </div>
590
778
 
779
+ {captureReceipt && (
780
+ <div className="grid gap-2 rounded-lg border border-border/50 p-3 text-sm">
781
+ <div className="flex items-center justify-between gap-3">
782
+ <span className="text-muted-foreground">Write</span>
783
+ <span className="font-medium">
784
+ {captureReceipt.createdWithSuffix
785
+ ? "Created with suffix"
786
+ : captureReceipt.overwritten
787
+ ? "Overwritten"
788
+ : captureReceipt.openedExisting
789
+ ? "Opened existing"
790
+ : "Created"}
791
+ </span>
792
+ </div>
793
+ <div className="flex items-center justify-between gap-3">
794
+ <span className="text-muted-foreground">FTS sync</span>
795
+ <span className="font-medium">
796
+ {statusLabel(captureReceipt.sync.status)}
797
+ </span>
798
+ </div>
799
+ {captureReceipt.sync.reason && (
800
+ <p className="text-muted-foreground text-xs">
801
+ {captureReceipt.sync.reason}
802
+ </p>
803
+ )}
804
+ {captureReceipt.sync.error && (
805
+ <p className="text-destructive text-xs">
806
+ {captureReceipt.sync.error}
807
+ </p>
808
+ )}
809
+ <div className="flex items-center justify-between gap-3">
810
+ <span className="text-muted-foreground">Embedding</span>
811
+ <span className="font-medium">
812
+ {statusLabel(captureReceipt.embed.status)}
813
+ </span>
814
+ </div>
815
+ {captureReceipt.embed.reason && (
816
+ <p className="text-muted-foreground text-xs">
817
+ {captureReceipt.embed.reason}
818
+ </p>
819
+ )}
820
+ {captureReceipt.sync.status === "pending" && !jobId && (
821
+ <p className="text-muted-foreground text-xs">
822
+ Sync is pending without a job id. Check status or run sync
823
+ from the collections page.
824
+ </p>
825
+ )}
826
+ </div>
827
+ )}
828
+
591
829
  {jobId && (
592
830
  <div className="rounded-lg border border-border/50 p-3">
593
831
  <IndexingProgress compact jobId={jobId} />