@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.
- package/README.md +10 -1
- package/assets/skill/SKILL.md +41 -0
- package/assets/skill/cli-reference.md +34 -0
- package/assets/skill/examples.md +37 -0
- package/assets/skill/mcp-reference.md +21 -0
- package/package.json +1 -1
- package/src/cli/commands/capture.ts +282 -0
- package/src/cli/commands/index-cmd.ts +17 -6
- package/src/cli/commands/shared.ts +17 -1
- package/src/cli/commands/update.ts +17 -6
- package/src/cli/options.ts +2 -0
- package/src/cli/program.ts +64 -0
- package/src/config/content-types.ts +140 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +14 -0
- package/src/config/loader.ts +11 -2
- package/src/config/types.ts +37 -1
- package/src/core/capture-write.ts +38 -0
- package/src/core/capture.ts +746 -0
- package/src/core/config-mutation.ts +14 -2
- package/src/core/file-ops.ts +21 -2
- package/src/core/note-presets.ts +61 -5
- package/src/ingestion/frontmatter.ts +77 -2
- package/src/ingestion/index.ts +2 -0
- package/src/ingestion/sync-options.ts +29 -0
- package/src/ingestion/sync.ts +104 -16
- package/src/ingestion/types.ts +14 -0
- package/src/mcp/tools/add-collection.ts +8 -5
- package/src/mcp/tools/capture.ts +137 -191
- package/src/mcp/tools/index-cmd.ts +13 -7
- package/src/mcp/tools/index.ts +43 -9
- package/src/mcp/tools/sync.ts +12 -6
- package/src/mcp/tools/workspace-write.ts +16 -10
- package/src/pipeline/hybrid.ts +2 -0
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +2 -0
- package/src/pipeline/vsearch.ts +4 -0
- package/src/sdk/client.ts +156 -20
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +6 -0
- package/src/serve/background-runtime.ts +18 -4
- package/src/serve/config-sync.ts +9 -2
- package/src/serve/public/components/CaptureModal.tsx +248 -10
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/routes/api.ts +238 -26
- package/src/serve/server.ts +12 -0
- package/src/serve/watch-service.ts +12 -2
- package/src/store/migrations/009-content-type-rule-fingerprint.ts +36 -0
- package/src/store/migrations/index.ts +12 -1
- package/src/store/sqlite/adapter.ts +29 -14
- 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
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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:
|
|
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">
|
|
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
|
-
|
|
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} />
|