@gmickel/gno 1.23.0 → 1.25.1

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 (68) hide show
  1. package/README.md +26 -12
  2. package/assets/skill/SKILL.md +37 -19
  3. package/assets/skill/recipes/capture-and-file.md +20 -5
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +1 -0
  6. package/browser-extension/dist/PRIVACY.md +55 -0
  7. package/browser-extension/dist/chunk-vn5f663b.js +50 -0
  8. package/browser-extension/dist/chunk-ydfx5d7p.css +1 -0
  9. package/browser-extension/dist/content.js +1 -0
  10. package/browser-extension/dist/manifest.json +25 -0
  11. package/browser-extension/dist/preview.html +13 -0
  12. package/browser-extension/dist/service-worker.js +40 -0
  13. package/package.json +13 -3
  14. package/spec/cli.md +141 -0
  15. package/spec/db/schema.sql +101 -0
  16. package/spec/mcp.md +10 -0
  17. package/spec/output-schemas/browser-clip-preview.schema.json +83 -0
  18. package/spec/output-schemas/browser-clip.schema.json +586 -0
  19. package/spec/output-schemas/capture-receipt.schema.json +22 -1
  20. package/spec/output-schemas/clipper-csrf.schema.json +12 -0
  21. package/spec/output-schemas/clipper-error.schema.json +46 -0
  22. package/spec/output-schemas/clipper-pair-approval.schema.json +17 -0
  23. package/spec/output-schemas/clipper-pair-start.schema.json +26 -0
  24. package/spec/output-schemas/clipper-pair-status.schema.json +46 -0
  25. package/spec/output-schemas/clipper-revoke.schema.json +28 -0
  26. package/spec/output-schemas/mcp-capture-result.schema.json +12 -1
  27. package/spec/output-schemas/setup-activation-result.schema.json +456 -0
  28. package/spec/output-schemas/setup-command-result.schema.json +93 -0
  29. package/spec/output-schemas/setup-receipt.schema.json +258 -0
  30. package/spec/output-schemas/setup-semantic-receipt.schema.json +195 -0
  31. package/src/cli/commands/completion/scripts.ts +2 -0
  32. package/src/cli/commands/embed.ts +7 -2
  33. package/src/cli/commands/setup-activation.ts +324 -0
  34. package/src/cli/commands/setup-semantic.ts +591 -0
  35. package/src/cli/commands/setup.ts +410 -0
  36. package/src/cli/program.ts +64 -0
  37. package/src/cli/setup-semantic-worker.ts +177 -0
  38. package/src/core/browser-clip-provenance.ts +139 -0
  39. package/src/core/browser-clip.ts +473 -0
  40. package/src/core/capture-write.ts +5 -0
  41. package/src/core/capture.ts +75 -18
  42. package/src/core/config-mutation.ts +94 -64
  43. package/src/core/file-lock.ts +89 -36
  44. package/src/core/folder-setup-planning.ts +453 -0
  45. package/src/core/folder-setup.ts +490 -0
  46. package/src/core/setup-activation.ts +309 -0
  47. package/src/core/setup-receipt.ts +321 -0
  48. package/src/serve/capture-service.ts +420 -0
  49. package/src/serve/clipper-body.ts +62 -0
  50. package/src/serve/clipper-capture.ts +248 -0
  51. package/src/serve/clipper-contract.ts +57 -0
  52. package/src/serve/clipper-idempotency.ts +35 -0
  53. package/src/serve/clipper-pairing.ts +297 -0
  54. package/src/serve/clipper-security-errors.ts +23 -0
  55. package/src/serve/clipper-security.ts +449 -0
  56. package/src/serve/connectors.ts +29 -2
  57. package/src/serve/public/app.tsx +8 -1
  58. package/src/serve/public/globals.built.css +1 -1
  59. package/src/serve/public/index.html +1 -0
  60. package/src/serve/public/lib/clipper-approval.ts +206 -0
  61. package/src/serve/public/pages/ClipperPairing.tsx +210 -0
  62. package/src/serve/routes/api.ts +19 -115
  63. package/src/serve/routes/clipper.ts +394 -0
  64. package/src/serve/server.ts +22 -0
  65. package/src/store/migrations/020-browser-clipper-security.ts +128 -0
  66. package/src/store/migrations/index.ts +2 -0
  67. package/src/store/sqlite/clipper-store-types.ts +104 -0
  68. package/src/store/sqlite/clipper-store.ts +496 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.23.0",
3
+ "version": "1.25.1",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
@@ -27,6 +27,8 @@
27
27
  },
28
28
  "files": [
29
29
  "assets",
30
+ "browser-extension/artifacts",
31
+ "browser-extension/dist",
30
32
  "bunfig.toml",
31
33
  "spec",
32
34
  "src",
@@ -53,6 +55,9 @@
53
55
  "scripts": {
54
56
  "dev": "bun run --hot src/index.ts",
55
57
  "build": "bun build src/index.ts",
58
+ "build:clipper": "bun browser-extension/build-cli.ts",
59
+ "package:clipper": "bun browser-extension/package-cli.ts",
60
+ "verify:clipper-package": "bun browser-extension/verify-package.ts",
56
61
  "start": "bun run src/index.ts",
57
62
  "lint": "oxlint --fix --type-aware --type-check && oxfmt .",
58
63
  "lint:check": "oxlint --type-aware --type-check && oxfmt --check .",
@@ -60,8 +65,10 @@
60
65
  "test": "bun test",
61
66
  "test:web": "bun test test/serve/public --timeout 30000",
62
67
  "test:e2e": "bun scripts/web-ui-smoke.ts",
68
+ "test:e2e:clipper": "bun test test/clipper/e2e.test.ts --timeout 240000",
63
69
  "test:e2e:install": "bunx playwright install chromium",
64
70
  "test:package": "bun scripts/package-smoke.ts",
71
+ "test:package:clipper": "bun scripts/package-smoke-clipper-cli.ts",
65
72
  "test:watch": "bun test --watch",
66
73
  "test:coverage": "bun test --coverage",
67
74
  "test:coverage:html": "bun test --coverage --html",
@@ -137,10 +144,11 @@
137
144
  "version:patch": "npm version patch --no-git-tag-version",
138
145
  "version:minor": "npm version minor --no-git-tag-version",
139
146
  "version:major": "npm version major --no-git-tag-version",
140
- "prerelease": "bun run lint:check && bun test && bun run docs:verify && bun run test:package",
147
+ "prerelease": "bun run lint:check && bun test && bun run docs:verify && bun run verify:clipper-package && bun run test:package",
141
148
  "release:dry-run": "gh workflow run publish.yml -f publish=false",
142
149
  "release:trigger": "gh workflow run publish.yml -f publish=true",
143
- "prepare": "lefthook install"
150
+ "prepare": "lefthook install",
151
+ "prepack": "bun run package:clipper"
144
152
  },
145
153
  "dependencies": {
146
154
  "@codemirror/lang-markdown": "6.5.0",
@@ -199,6 +207,7 @@
199
207
  "@vscode/tree-sitter-wasm": "0.3.1",
200
208
  "ajv": "8.17.1",
201
209
  "ajv-formats": "3.0.1",
210
+ "bun": "1.3.14",
202
211
  "docx": "9.5.1",
203
212
  "evalite": "1.0.0-beta.16",
204
213
  "exceljs": "4.4.0",
@@ -221,6 +230,7 @@
221
230
  "bun": ">=1.3.0"
222
231
  },
223
232
  "trustedDependencies": [
233
+ "bun",
224
234
  "node-llama-cpp"
225
235
  ]
226
236
  }
package/spec/cli.md CHANGED
@@ -64,6 +64,7 @@ equivalent files fail closed as ambiguous.
64
64
  | ------------------ | ------ | ------- | ----- | ---- | ----- | -------- |
65
65
  | status | yes | no | no | yes | no | terminal |
66
66
  | init | no | no | no | no | no | terminal |
67
+ | setup | yes | no | no | no | no | terminal |
67
68
  | collection add | no | no | no | no | no | terminal |
68
69
  | collection list | yes | no | no | yes | no | terminal |
69
70
  | collection remove | no | no | no | no | no | terminal |
@@ -353,6 +354,96 @@ projected wiki/markdown edges and surface in graph traversal/diagnose metadata.
353
354
 
354
355
  ---
355
356
 
357
+ ### gno setup
358
+
359
+ Add or reuse one folder collection, build its lexical index, and prove a real
360
+ corpus-derived retrieval before reporting success. Semantic indexing is handed
361
+ to one standalone background worker and never delays lexical success.
362
+
363
+ **Synopsis:**
364
+
365
+ ```bash
366
+ gno setup <folder> [-n|--name <name>] [--exclude <pattern>]...
367
+ [--authorize-secret-risk] [--connector <id>]...
368
+ [--no-semantic] [--json]
369
+ ```
370
+
371
+ **Options:**
372
+
373
+ | Option | Type | Default | Description |
374
+ | ------------------------- | ---------- | ------------- | ---------------------------------------------------------------------------- |
375
+ | `-n, --name <name>` | string | dirname | Requested collection name; exact-root reruns reuse the configured collection |
376
+ | `--exclude <pattern>` | repeatable | core defaults | One literal exclusion per occurrence; never CSV |
377
+ | `--authorize-secret-risk` | boolean | false | Explicitly authorize likely credentials, private keys, or env files |
378
+ | `--connector <id>` | repeatable | none | Install or reuse and verify one supported connector after lexical proof |
379
+ | `--no-semantic` | boolean | false | Prove lexical retrieval but record semantic work as skipped |
380
+ | `--json` | boolean | false | Emit one closed setup result object |
381
+
382
+ Omitting `--exclude` preserves the core create defaults or the filters already
383
+ configured for an exact-root rerun. An empty occurrence is invalid. Global
384
+ `--yes` accepts safe defaults only; it never authorizes secret risk. Only an
385
+ interactive terminal may ask one default-No question after the lexical core
386
+ returns `secret_risk`. JSON, `--yes`, non-TTY, decline, and EOF fail closed.
387
+
388
+ The command bootstraps missing config/data/database state without adding the
389
+ folder through `init`, then runs the folder setup transaction. Success requires
390
+ a completed `FolderSetupReceipt@1.0`, `activation.ready=true`, and a non-empty
391
+ exact `activation.evidence.resultUri`. Terminal stage progress uses stderr;
392
+ `--quiet` suppresses progress but not the final result. JSON writes exactly one
393
+ canonical result to stdout on both success and domain failure, with no progress.
394
+
395
+ After lexical proof, the command records one private atomic
396
+ `setup-semantic@1.0` receipt per canonical index/folder and starts one detached,
397
+ collection-scoped Bun worker. A matching live worker is reused; a dead worker is
398
+ replaced; and a live worker with an older identity remains authoritative until
399
+ it exits so its completion receipt cannot be stranded. A later setup rerun may
400
+ then schedule the new identity. The worker uses the normal model
401
+ download/offline policy, records `completed` only when embedding and vector
402
+ synchronization finish without errors, and exits. Spawn, download, partial
403
+ embedding, and vector-sync failures retain an exact foreground
404
+ `gno ... embed <collection>` remediation and never change proven lexical exit 0.
405
+ Direct setup never contacts a resident, MCP, or Web runtime.
406
+
407
+ `--connector` is explicit and repeatable; omission performs no connector
408
+ inspection, installation, or verification. Supported IDs are
409
+ `claude-code-skill`, `claude-desktop-mcp`, `cursor-mcp`, `codex-skill`,
410
+ `opencode-skill`, `openclaw-skill`, and `hermes-skill`. Exact duplicates dedupe
411
+ in first-seen order. Unknown IDs fail before setup side effects.
412
+ Any connector-mode JSON invocation emits `setup-activation-result@1.0`.
413
+ Argument or lexical failure uses outer `status: failed`, nests the unchanged
414
+ failed setup result, and keeps `connectors: []`; it retains the original setup
415
+ exit code and performs no connector action.
416
+
417
+ After proven lexical success and after the lexical store closes, the direct CLI
418
+ opens a new standalone store for connector composition. Missing targets use the
419
+ existing read-only installer; existing entries are reused without overwrite.
420
+ Malformed or unreadable entries are preserved. MCP targets run the shipped
421
+ bounded read-only activation smoke. Skill targets install but return
422
+ `skipped/target_runtime_unverifiable` because they expose no safe runtime hook.
423
+ Connector failures or skips do not roll back lexical success: the command exits
424
+ 0 with `completed_with_actions` and bounded per-target remediation. No raw child
425
+ output or client config path is serialized. Reruns delegate passed-receipt reuse
426
+ and recoverable retry to the shipped activation verifier; setup introduces no
427
+ second connector fingerprint or cache.
428
+
429
+ **Structured schemas:**
430
+
431
+ - [`setup-command-result@1.0`](./output-schemas/setup-command-result.schema.json)
432
+ - [`setup-activation-result@1.0`](./output-schemas/setup-activation-result.schema.json)
433
+ - [`setup-semantic@1.0`](./output-schemas/setup-semantic-receipt.schema.json)
434
+ - [`FolderSetupReceipt@1.0`](./output-schemas/setup-receipt.schema.json)
435
+
436
+ **Exit Codes:**
437
+
438
+ - 0: Lexical setup completed with a real exact result URI; semantic state may
439
+ be scheduled, running, pending, completed, or skipped; requested connector
440
+ actions may require bounded follow-up
441
+ - 1: Invalid/safe-input rejection, collection/filter/index disagreement,
442
+ secret-risk refusal, or declined confirmation
443
+ - 2: Config, receipt, IO, store, indexing, proof, or internal invariant failure
444
+
445
+ ---
446
+
356
447
  ### gno collection add
357
448
 
358
449
  Add a new collection to the index.
@@ -1076,6 +1167,24 @@ Capture writes structured `source:` frontmatter and returns the shared
1076
1167
  [`capture-receipt`](./output-schemas/capture-receipt.schema.json). `--source-date`
1077
1168
  maps to `source.observedAt`; `--source-id` maps to `source.externalId`.
1078
1169
 
1170
+ Browser-clip receipts MAY extend `source` with `canonicalUrl`, `site`,
1171
+ `publishedAt`, and the closed
1172
+ [`browser-clip`](./output-schemas/browser-clip.schema.json) provenance object.
1173
+ The provenance preserves exact selection text separately from canonical final
1174
+ Markdown and carries extraction/final hashes, deterministic identity and
1175
+ preview digests, normalized dates/URLs, browser metadata, server capture time,
1176
+ and bounded warnings. Existing capture inputs and receipts remain compatible.
1177
+ Browser-clip URLs use a closed, credential-free HTTP(S) subset: ASCII DNS or
1178
+ punycode hosts (or IPv4), optional ports from 1 through 65535, and ASCII
1179
+ RFC 3986 path/query/fragment characters with well-formed percent escapes. Raw
1180
+ Unicode hosts and IPv6 literals are rejected; browser clients should submit
1181
+ their serialized IDNA/punycode URL. Free-form clip strings reject C0/C1 control
1182
+ characters except tab, LF, and CR.
1183
+ For browser provenance, `open_existing` MUST open only when the stored
1184
+ `clipIdentity` matches; missing or different provenance MUST return
1185
+ `collisionPolicyResult: "conflict"` without writing. `create_with_suffix`
1186
+ creates a distinct note.
1187
+
1079
1188
  **Path and Collision Rules:**
1080
1189
 
1081
1190
  - Explicit `--path` wins.
@@ -2861,6 +2970,38 @@ is blocked.
2861
2970
  - Mounts stateful Streamable HTTP MCP at `/mcp` only after the fail-closed
2862
2971
  actual-peer, Host, Origin, bearer, body, rate, request, queue, and session
2863
2972
  boundary initializes
2973
+ - On the loopback `gno serve` listener only, mounts the browser-clipper pairing,
2974
+ preview, and capture routes. These require exact extension/same-origin
2975
+ policies and dedicated origin-bound capture grants; MCP bearer tokens,
2976
+ `GNO_API_TOKEN`, and `gateway.enableWrite` do not authorize them. Clipper
2977
+ routes are structurally absent from non-loopback listeners. Capture
2978
+ idempotency persists exact content-free plan hashes and reconciles an
2979
+ interrupted atomic write only at its original path; changed plans fail
2980
+ closed without a duplicate write.
2981
+ - Serves the standalone `/clipper/pair` approval page. Its exact pair-ID
2982
+ fragment is synchronously validated and scrubbed before normal workspace
2983
+ state initializes; the user enters the eight-digit code manually. The page
2984
+ obtains same-origin CSRF state, never receives the extension grant, and does
2985
+ not persist pairing material.
2986
+ - Browser-clipper HTTP write receipts add `schemaVersion: "1.0"` to the closed
2987
+ shared capture receipt. HTTP 409 can therefore be either a valid provenance
2988
+ conflict receipt or a closed clipper error; clients parse the body contract
2989
+ before classifying the outcome.
2990
+ - Browser clip response/status closure is exact: HTTP 200 only for
2991
+ `opened_existing`; HTTP 202 only for `created`, `created_with_suffix`, or
2992
+ `overwritten`; HTTP 409 for either a valid `conflict` receipt or the matching
2993
+ closed `clipper-error@1.0`. Unknown versions, fields, codes, non-JSON bodies,
2994
+ and status/body mismatches fail as invalid responses. `CLIPPER_OFFLINE`,
2995
+ `CLIPPER_INVALID_RESPONSE`, and `CLIPPER_CLIENT` are client-only
2996
+ classifications.
2997
+ - Browser clip provenance contains exactly `extractionHash`, `finalBodyHash`,
2998
+ `clipIdentity`, and `previewDigest`; it does not contain `sourceHash`.
2999
+ - The Chromium service worker stores the loopback origin, plaintext bounded
3000
+ grant, and at most one `{payload, previewDigest, idempotencyKey}` in protected
3001
+ local extension storage. Unfinished pairing state uses extension session
3002
+ storage. Page extraction and the approval page receive neither grant nor
3003
+ pending-write state. Recovery reuses the same logical write or fails closed
3004
+ without changing destination.
2864
3005
  - On `--detach`: forks a detached child with stdio redirected to `--log-file`, writes pid-file JSON (`{pid, port, cmd:"serve", version, started_at}`), prints `{pid, url}` on stdout, exits 0
2865
3006
  - On `--status`: output matches the [process-status schema](./output-schemas/process-status.schema.json). Liveness via `process.kill(pid, 0)`; stale pid-files (ESRCH) are reported as `running:false`. Live status best-effort reads the same redacted `resident-status@1.0` snapshot from the recorded listener.
2866
3007
  - On `--stop`: sends SIGTERM, polls every 100ms for up to 10s, falls back to SIGKILL, polls 2s more, unlinks pid-file if the process cleaned up after itself
@@ -610,3 +610,104 @@ CREATE TABLE IF NOT EXISTS saved_capsule_reverification_state (
610
610
  registration_epoch INTEGER NOT NULL DEFAULT 0
611
611
  CHECK (registration_epoch >= 0)
612
612
  );
613
+
614
+ -- ─────────────────────────────────────────────────────────────────────────────
615
+ -- Browser clipper security state
616
+ -- ─────────────────────────────────────────────────────────────────────────────
617
+
618
+ CREATE TABLE IF NOT EXISTS clipper_grants (
619
+ id TEXT PRIMARY KEY,
620
+ token_hash TEXT NOT NULL UNIQUE,
621
+ origin TEXT NOT NULL,
622
+ scope TEXT NOT NULL DEFAULT 'capture'
623
+ CHECK (scope = 'capture'),
624
+ created_at_ms INTEGER NOT NULL CHECK (created_at_ms >= 0),
625
+ expires_at_ms INTEGER NOT NULL CHECK (expires_at_ms > created_at_ms),
626
+ revoked_at_ms INTEGER
627
+ CHECK (revoked_at_ms IS NULL OR revoked_at_ms >= created_at_ms),
628
+ CHECK (length(id) BETWEEN 1 AND 128),
629
+ CHECK (
630
+ length(token_hash) = 64
631
+ AND token_hash NOT GLOB '*[^0-9a-f]*'
632
+ ),
633
+ CHECK (
634
+ length(origin) = 51
635
+ AND substr(origin, 1, 19) = 'chrome-extension://'
636
+ AND substr(origin, 20) NOT GLOB '*[^a-p]*'
637
+ )
638
+ );
639
+
640
+ CREATE INDEX IF NOT EXISTS idx_clipper_grants_expiry
641
+ ON clipper_grants(expires_at_ms, id);
642
+
643
+ CREATE TABLE IF NOT EXISTS clipper_capture_idempotency (
644
+ grant_id TEXT NOT NULL,
645
+ key_hash TEXT NOT NULL,
646
+ request_digest TEXT NOT NULL,
647
+ collection TEXT NOT NULL,
648
+ rel_path TEXT NOT NULL,
649
+ collision_policy_result TEXT NOT NULL
650
+ CHECK (
651
+ collision_policy_result IN (
652
+ 'created',
653
+ 'opened_existing',
654
+ 'created_with_suffix',
655
+ 'overwritten',
656
+ 'conflict'
657
+ )
658
+ ),
659
+ content_hash TEXT NOT NULL,
660
+ clip_identity TEXT NOT NULL,
661
+ state TEXT NOT NULL CHECK (state IN ('pending', 'completed')),
662
+ status_code INTEGER,
663
+ response_json TEXT,
664
+ created_at_ms INTEGER NOT NULL CHECK (created_at_ms >= 0),
665
+ completed_at_ms INTEGER
666
+ CHECK (completed_at_ms IS NULL OR completed_at_ms >= created_at_ms),
667
+ PRIMARY KEY (grant_id, key_hash),
668
+ FOREIGN KEY (grant_id)
669
+ REFERENCES clipper_grants(id)
670
+ ON DELETE CASCADE,
671
+ CHECK (
672
+ length(key_hash) = 64
673
+ AND key_hash NOT GLOB '*[^0-9a-f]*'
674
+ ),
675
+ UNIQUE (grant_id, request_digest),
676
+ CHECK (
677
+ length(request_digest) = 64
678
+ AND request_digest NOT GLOB '*[^0-9a-f]*'
679
+ ),
680
+ CHECK (length(CAST(collection AS BLOB)) BETWEEN 1 AND 256),
681
+ CHECK (length(CAST(rel_path AS BLOB)) BETWEEN 1 AND 8192),
682
+ CHECK (
683
+ length(content_hash) = 64
684
+ AND content_hash NOT GLOB '*[^0-9a-f]*'
685
+ ),
686
+ CHECK (
687
+ length(clip_identity) = 64
688
+ AND clip_identity NOT GLOB '*[^0-9a-f]*'
689
+ ),
690
+ CHECK (
691
+ response_json IS NULL
692
+ OR length(CAST(response_json AS BLOB)) <= 2097152
693
+ ),
694
+ CHECK (
695
+ (
696
+ state = 'pending'
697
+ AND status_code IS NULL
698
+ AND response_json IS NULL
699
+ AND completed_at_ms IS NULL
700
+ )
701
+ OR
702
+ (
703
+ state = 'completed'
704
+ AND status_code BETWEEN 200 AND 599
705
+ AND response_json IS NOT NULL
706
+ AND json_valid(response_json)
707
+ AND completed_at_ms IS NOT NULL
708
+ )
709
+ )
710
+ );
711
+
712
+ CREATE INDEX IF NOT EXISTS idx_clipper_idempotency_created
713
+ ON clipper_capture_idempotency(created_at_ms, grant_id, key_hash);
package/spec/mcp.md CHANGED
@@ -1101,6 +1101,16 @@ Create a new document in a collection (write-enabled).
1101
1101
  - Capture writes structured `source:` frontmatter with `kind`, `capturedAt`,
1102
1102
  and optional `url`, `uri`, `docid`, `mime`, `ext`, `author`, `observedAt`,
1103
1103
  `externalId`, and `title`
1104
+ - Browser-clip receipts MAY additionally carry normalized `canonicalUrl`,
1105
+ `site`, `publishedAt`, and closed `browserClip` provenance as defined by
1106
+ `browser-clip.schema.json`; existing `gno_capture` inputs remain compatible
1107
+ - Browser provenance preserves exact selection separately from canonical final
1108
+ Markdown and includes extraction/final hashes, deterministic clip/preview
1109
+ digests, normalized dates/URLs, browser metadata, server capture time, and
1110
+ bounded warnings
1111
+ - Browser `open_existing` MUST require a matching stored `clipIdentity`;
1112
+ missing or different provenance returns `collisionPolicyResult: "conflict"`
1113
+ without writing, while `create_with_suffix` creates a distinct note
1104
1114
  - Tags are validated and normalized to lowercase
1105
1115
  - For Markdown files, tags are added to frontmatter
1106
1116
  - For non-Markdown files, tags are stored as user-source in the database
@@ -0,0 +1,83 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "gno://schemas/browser-clip-preview@1.0",
4
+ "title": "Browser Clip Preview",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "preview", "provenance", "plan"],
8
+ "properties": {
9
+ "schemaVersion": { "const": "1.0" },
10
+ "preview": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["body", "digest", "source", "destination", "tags"],
14
+ "properties": {
15
+ "body": { "type": "string" },
16
+ "digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
17
+ "source": { "$ref": "#/definitions/source" },
18
+ "destination": {
19
+ "$ref": "gno://schemas/browser-clip@1.0#/properties/destination"
20
+ },
21
+ "tags": {
22
+ "type": "array",
23
+ "items": { "type": "string" }
24
+ }
25
+ }
26
+ },
27
+ "provenance": {
28
+ "$ref": "gno://schemas/browser-clip@1.0#/definitions/provenance"
29
+ },
30
+ "plan": {
31
+ "type": "object",
32
+ "additionalProperties": false,
33
+ "required": ["collection", "relPath", "outcome", "provenanceConflict"],
34
+ "properties": {
35
+ "collection": { "type": "string", "minLength": 1 },
36
+ "relPath": { "type": "string", "minLength": 1 },
37
+ "outcome": {
38
+ "enum": [
39
+ "created",
40
+ "opened_existing",
41
+ "created_with_suffix",
42
+ "overwritten",
43
+ "conflict"
44
+ ]
45
+ },
46
+ "provenanceConflict": { "type": "boolean" }
47
+ }
48
+ }
49
+ },
50
+ "definitions": {
51
+ "source": {
52
+ "type": "object",
53
+ "additionalProperties": false,
54
+ "required": [
55
+ "kind",
56
+ "title",
57
+ "url",
58
+ "observedAt",
59
+ "capturedAt",
60
+ "browserClip"
61
+ ],
62
+ "properties": {
63
+ "kind": { "const": "web" },
64
+ "title": { "type": "string" },
65
+ "url": { "type": "string", "format": "uri" },
66
+ "author": { "type": "string" },
67
+ "observedAt": { "type": "string", "format": "date-time" },
68
+ "capturedAt": { "type": "string", "format": "date-time" },
69
+ "canonicalUrl": { "type": "string", "format": "uri" },
70
+ "site": { "type": "string" },
71
+ "publishedAt": {
72
+ "oneOf": [
73
+ { "type": "string", "format": "date" },
74
+ { "type": "string", "format": "date-time" }
75
+ ]
76
+ },
77
+ "browserClip": {
78
+ "$ref": "gno://schemas/browser-clip@1.0#/definitions/provenance"
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }