@gmickel/gno 1.24.0 → 1.26.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 (96) hide show
  1. package/README.md +18 -4
  2. package/assets/skill/SKILL.md +40 -17
  3. package/assets/skill/recipes/capture-and-file.md +20 -5
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.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 +196 -4
  15. package/spec/db/schema.sql +102 -1
  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/project-profile-apply.schema.json +209 -0
  28. package/spec/output-schemas/project-profile-command.schema.json +160 -0
  29. package/spec/output-schemas/query-diagnose.schema.json +7 -1
  30. package/spec/output-schemas/setup-profile-result.schema.json +87 -0
  31. package/spec/project-profile.schema.json +301 -0
  32. package/src/cli/commands/collection/add.ts +39 -45
  33. package/src/cli/commands/collection/remove.ts +28 -28
  34. package/src/cli/commands/collection/rename.ts +55 -73
  35. package/src/cli/commands/context/add.ts +37 -26
  36. package/src/cli/commands/context/rm.ts +47 -20
  37. package/src/cli/commands/init.ts +55 -125
  38. package/src/cli/commands/models/use.ts +43 -38
  39. package/src/cli/commands/profile-apply.ts +334 -0
  40. package/src/cli/commands/profile.ts +409 -0
  41. package/src/cli/commands/setup-activation.ts +205 -54
  42. package/src/cli/commands/setup-profile.ts +223 -0
  43. package/src/cli/commands/setup.ts +3 -0
  44. package/src/cli/program.ts +110 -9
  45. package/src/config/index.ts +3 -0
  46. package/src/config/project-profile.ts +367 -0
  47. package/src/config/saver.ts +16 -7
  48. package/src/config/types.ts +42 -0
  49. package/src/core/browser-clip-provenance.ts +139 -0
  50. package/src/core/browser-clip.ts +473 -0
  51. package/src/core/capture-write.ts +5 -0
  52. package/src/core/capture.ts +75 -18
  53. package/src/core/config-mutation.ts +138 -76
  54. package/src/core/config-write-lock.ts +89 -0
  55. package/src/core/context-identity.ts +16 -0
  56. package/src/core/context-resolver.ts +2 -12
  57. package/src/core/file-lock.ts +20 -6
  58. package/src/core/folder-setup-planning.ts +6 -21
  59. package/src/core/folder-setup.ts +30 -2
  60. package/src/core/path-rules.ts +53 -0
  61. package/src/core/project-affinity-surface.ts +102 -7
  62. package/src/core/project-profile-apply-state.ts +268 -0
  63. package/src/core/project-profile-apply-validation.ts +95 -0
  64. package/src/core/project-profile-apply.ts +408 -0
  65. package/src/core/project-profile-canonical.ts +71 -0
  66. package/src/core/project-profile-diff.ts +302 -0
  67. package/src/core/project-profile-discovery.ts +519 -0
  68. package/src/core/project-profile-file.ts +37 -0
  69. package/src/core/project-profile-parser.ts +98 -0
  70. package/src/core/project-profile.ts +490 -0
  71. package/src/ingestion/walker.ts +85 -44
  72. package/src/llm/cache.ts +21 -0
  73. package/src/serve/capture-service.ts +420 -0
  74. package/src/serve/clipper-body.ts +62 -0
  75. package/src/serve/clipper-capture.ts +248 -0
  76. package/src/serve/clipper-contract.ts +57 -0
  77. package/src/serve/clipper-idempotency.ts +35 -0
  78. package/src/serve/clipper-pairing.ts +297 -0
  79. package/src/serve/clipper-security-errors.ts +23 -0
  80. package/src/serve/clipper-security.ts +449 -0
  81. package/src/serve/config-sync.ts +2 -2
  82. package/src/serve/public/app.tsx +8 -1
  83. package/src/serve/public/globals.built.css +1 -1
  84. package/src/serve/public/index.html +1 -0
  85. package/src/serve/public/lib/clipper-approval.ts +206 -0
  86. package/src/serve/public/pages/ClipperPairing.tsx +210 -0
  87. package/src/serve/resident-runtime.ts +1 -0
  88. package/src/serve/routes/api.ts +20 -115
  89. package/src/serve/routes/clipper.ts +394 -0
  90. package/src/serve/server.ts +22 -0
  91. package/src/store/migrations/020-browser-clipper-security.ts +128 -0
  92. package/src/store/migrations/021-multi-context-identity.ts +37 -0
  93. package/src/store/migrations/index.ts +4 -0
  94. package/src/store/sqlite/adapter.ts +83 -0
  95. package/src/store/sqlite/clipper-store-types.ts +104 -0
  96. 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.24.0",
3
+ "version": "1.26.0",
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
@@ -65,6 +65,10 @@ equivalent files fail closed as ambiguous.
65
65
  | status | yes | no | no | yes | no | terminal |
66
66
  | init | no | no | no | no | no | terminal |
67
67
  | setup | yes | no | no | no | no | terminal |
68
+ | profile check | yes | no | no | no | no | terminal |
69
+ | profile show | yes | no | no | no | no | terminal |
70
+ | profile diff | yes | no | no | no | no | terminal |
71
+ | profile apply | yes | no | no | no | no | terminal |
68
72
  | collection add | no | no | no | no | no | terminal |
69
73
  | collection list | yes | no | no | yes | no | terminal |
70
74
  | collection remove | no | no | no | no | no | terminal |
@@ -125,6 +129,89 @@ equivalent files fail closed as ambiguous.
125
129
 
126
130
  ## Commands
127
131
 
132
+ ### gno profile
133
+
134
+ Inspect a repository-owned `.gno/index.yml` retrieval profile without changing
135
+ the user config, index database, model cache, or tracked files.
136
+
137
+ **Synopsis:**
138
+
139
+ ```bash
140
+ gno profile check [path] [--json]
141
+ gno profile show [path] [--json]
142
+ gno profile diff [path] [--json]
143
+ gno profile apply [path] [--json]
144
+ ```
145
+
146
+ With no path, discovery starts at the canonical current directory and walks
147
+ upward. Each directory is checked before its parent, so the nearest nested
148
+ profile wins and any ancestor profile is reported as shadowed; profiles are
149
+ never merged. Discovery stops after checking the first Git root (`.git` may be a
150
+ directory or a worktree file), before crossing a filesystem device boundary,
151
+ or at the filesystem root. This gives a nested repository precedence over its
152
+ parent repository and lets a monorepo subtree intentionally shadow the root
153
+ profile.
154
+
155
+ An explicit directory path is an exact profile-root override: only
156
+ `<path>/.gno/index.yml` is considered. An explicit
157
+ `<path>/.gno/index.yml` file selects that exact profile. Explicit overrides do
158
+ not fall back to ancestors. The profile file and its `.gno` path must resolve
159
+ inside the selected canonical profile root; symlink escapes fail closed.
160
+ Remote callers cannot enable discovery or cause filesystem probes.
161
+
162
+ `check` validates discovery, schema, referenced paths, local preset aliases,
163
+ and, with global `--offline`, exact cache availability without downloading.
164
+ `show` returns the same receipt plus normalized portable desired state. `diff`
165
+ compares that desired state with the selected user config. It reports stale
166
+ same-name/path mappings and explicit repair/removal choices but never applies
167
+ either choice. A missing user config is treated as empty desired-state input;
168
+ an unreadable or invalid config is an actionable validation diagnostic.
169
+
170
+ The three read-only JSON forms use
171
+ [`project-profile-command@1.0`](./output-schemas/project-profile-command.schema.json).
172
+ Receipts contain no absolute profile, config, database, cache, or model paths,
173
+ no timestamps, and no model URIs. Diagnostics and changes are canonically
174
+ ordered, so identical local state produces byte-identical JSON.
175
+
176
+ `apply` rebuilds the shared diff against config reloaded inside a cross-process
177
+ runtime lock, then creates or updates only resources declared by the profile
178
+ through a resumable operation. It can initialize a missing user config. Omitted collections,
179
+ collection-scoped contexts, and content-type rules remain untouched; stale
180
+ same-path collections are reported as skipped and retained. A stale same-name
181
+ collection root is repaired in place without deleting its collection or index
182
+ identity. Apply also updates a timestamp-free `projectProfileBindings` record
183
+ in the local user config with the canonical absolute profile path, profile
184
+ fingerprint, and projected collection. The public `profile_binding` resource
185
+ receipt identifies only the collection and never exposes that local path.
186
+ Model preset aliases resolve to collection-local model overrides.
187
+ Apply synchronizes the config projection but does not index documents; changed
188
+ collections appear in `pendingIndexing`.
189
+ `affinityDefaults` remains profile-scoped and is compiled at trusted local
190
+ retrieval time; apply does not overwrite the user config `projectAffinity`
191
+ default. Its apply resource is therefore `project_affinity/profile/skipped`,
192
+ while the nearest valid profile can still supply request-local affinity.
193
+
194
+ The deterministic JSON result uses
195
+ [`project-profile-apply@1.0`](./output-schemas/project-profile-apply.schema.json).
196
+ Its created/reused/updated/skipped resource receipt is also atomically saved as
197
+ `project-profiles/apply-receipt.json` under the user data directory. The config,
198
+ index, receipt, and lock paths must all remain outside the selected profile
199
+ root. The tracked `.gno/index.yml` is never written. Interrupted or concurrent
200
+ applies resume from fresh config state and converge idempotently.
201
+
202
+ **Exit Codes:**
203
+
204
+ - `0`: profile found and valid (`diff` may still report changes)
205
+ - `1`: profile missing, ambiguous discovery could not be resolved safely, or
206
+ profile/config validation failed, including runtime-path overlap
207
+ - `2`: unexpected local I/O, lock, receipt, or index-store failure
208
+
209
+ `gno setup` calls the same local `check` composition before its folder
210
+ transaction. Inspection is read-only and non-fatal. Plain setup prints bounded
211
+ preview/apply guidance but preserves the existing setup result contract.
212
+ Explicit `--apply-profile` runs the same lock-safe apply path before lexical
213
+ setup and emits `setup-profile-result@1.0`.
214
+
128
215
  ### gno status
129
216
 
130
217
  Display index status and health information.
@@ -365,7 +452,7 @@ to one standalone background worker and never delays lexical success.
365
452
  ```bash
366
453
  gno setup <folder> [-n|--name <name>] [--exclude <pattern>]...
367
454
  [--authorize-secret-risk] [--connector <id>]...
368
- [--no-semantic] [--json]
455
+ [--apply-profile] [--no-semantic] [--json]
369
456
  ```
370
457
 
371
458
  **Options:**
@@ -376,6 +463,7 @@ gno setup <folder> [-n|--name <name>] [--exclude <pattern>]...
376
463
  | `--exclude <pattern>` | repeatable | core defaults | One literal exclusion per occurrence; never CSV |
377
464
  | `--authorize-secret-risk` | boolean | false | Explicitly authorize likely credentials, private keys, or env files |
378
465
  | `--connector <id>` | repeatable | none | Install or reuse and verify one supported connector after lexical proof |
466
+ | `--apply-profile` | boolean | false | Apply a valid discovered project profile before lexical setup |
379
467
  | `--no-semantic` | boolean | false | Prove lexical retrieval but record semantic work as skipped |
380
468
  | `--json` | boolean | false | Emit one closed setup result object |
381
469
 
@@ -392,6 +480,53 @@ exact `activation.evidence.resultUri`. Terminal stage progress uses stderr;
392
480
  `--quiet` suppresses progress but not the final result. JSON writes exactly one
393
481
  canonical result to stdout on both success and domain failure, with no progress.
394
482
 
483
+ Before the folder transaction, setup performs the same local read-only
484
+ `gno profile check` composition from the supplied folder. Missing and invalid
485
+ profiles remain optional and never block ordinary setup. Terminal output shows
486
+ a valid profile fingerprint plus `profile diff`/`--apply-profile` guidance, or
487
+ an invalid-profile diagnostic, before setup mutates local state.
488
+
489
+ `--apply-profile` is explicit. When the check is valid, setup runs the existing
490
+ cross-process lock-safe, create/update-only `profile apply` path first, then
491
+ uses the applied profile collection's canonical root, name, and filters for
492
+ lexical setup. A nested folder invocation therefore indexes the declared
493
+ profile root rather than creating a duplicate subdirectory collection. The
494
+ profile never deletes omitted resources or overwrites the user-level
495
+ `projectAffinity` default.
496
+
497
+ Once a valid profile has been discovered for an explicit `--apply-profile`
498
+ request, apply is a fail-closed prerequisite. A validation failure returns exit
499
+ 1; an apply I/O/lock/store failure, thrown apply transport, missing result, or
500
+ malformed success receipt returns exit 2. In every case setup aborts before its
501
+ folder transaction or connector work. A late apply failure may have persisted
502
+ resumable create/update-only profile state, but cannot trigger ordinary setup
503
+ reconciliation. The outer result is `failed`, the nested lexical error code is
504
+ `profile_apply_failed`, and `profile.apply` retains a returned apply result or
505
+ is `null` when apply produced no result.
506
+
507
+ Missing or invalid profile inspection remains the optional fallback described
508
+ below. A thrown inspection transport/runtime failure for an explicit
509
+ `--apply-profile` request instead returns exit 2 with
510
+ `profile_inspection_failed` before apply or setup mutation. Because no trusted
511
+ profile check result exists, this failure uses the unchanged
512
+ `setup-command-result@1.0` shape rather than fabricating `profile.check`.
513
+
514
+ For a valid profile, `--apply-profile` is mutually exclusive with explicit
515
+ `--name` and `--exclude` values. A conflict returns exit 1 with
516
+ `profile_option_conflict` before config, store, or index mutation. Profile
517
+ include/exclude values use validated Bun-glob semantics; plain exclusion
518
+ components retain component matching. Brace alternatives are rejected; express
519
+ their branches as separate include/exclude entries. Discovery permission/I/O
520
+ failures return exit 2, while missing, disabled, invalid, and unsafe profiles
521
+ return exit 1.
522
+
523
+ Opt-in JSON uses
524
+ [`setup-profile-result@1.0`](./output-schemas/setup-profile-result.schema.json)
525
+ and includes the closed `profile.check`, nullable `profile.apply`, unchanged
526
+ setup result, and connectors. A missing/invalid profile keeps setup usable and
527
+ returns `completed_with_actions` with `profile.apply: null`. Without
528
+ `--apply-profile`, the existing setup/activation JSON bytes remain unchanged.
529
+
395
530
  After lexical proof, the command records one private atomic
396
531
  `setup-semantic@1.0` receipt per canonical index/folder and starts one detached,
397
532
  collection-scoped Bun worker. A matching live worker is reused; a dead worker is
@@ -430,6 +565,7 @@ second connector fingerprint or cache.
430
565
 
431
566
  - [`setup-command-result@1.0`](./output-schemas/setup-command-result.schema.json)
432
567
  - [`setup-activation-result@1.0`](./output-schemas/setup-activation-result.schema.json)
568
+ - [`setup-profile-result@1.0`](./output-schemas/setup-profile-result.schema.json)
433
569
  - [`setup-semantic@1.0`](./output-schemas/setup-semantic-receipt.schema.json)
434
570
  - [`FolderSetupReceipt@1.0`](./output-schemas/setup-receipt.schema.json)
435
571
 
@@ -1167,6 +1303,24 @@ Capture writes structured `source:` frontmatter and returns the shared
1167
1303
  [`capture-receipt`](./output-schemas/capture-receipt.schema.json). `--source-date`
1168
1304
  maps to `source.observedAt`; `--source-id` maps to `source.externalId`.
1169
1305
 
1306
+ Browser-clip receipts MAY extend `source` with `canonicalUrl`, `site`,
1307
+ `publishedAt`, and the closed
1308
+ [`browser-clip`](./output-schemas/browser-clip.schema.json) provenance object.
1309
+ The provenance preserves exact selection text separately from canonical final
1310
+ Markdown and carries extraction/final hashes, deterministic identity and
1311
+ preview digests, normalized dates/URLs, browser metadata, server capture time,
1312
+ and bounded warnings. Existing capture inputs and receipts remain compatible.
1313
+ Browser-clip URLs use a closed, credential-free HTTP(S) subset: ASCII DNS or
1314
+ punycode hosts (or IPv4), optional ports from 1 through 65535, and ASCII
1315
+ RFC 3986 path/query/fragment characters with well-formed percent escapes. Raw
1316
+ Unicode hosts and IPv6 literals are rejected; browser clients should submit
1317
+ their serialized IDNA/punycode URL. Free-form clip strings reject C0/C1 control
1318
+ characters except tab, LF, and CR.
1319
+ For browser provenance, `open_existing` MUST open only when the stored
1320
+ `clipIdentity` matches; missing or different provenance MUST return
1321
+ `collisionPolicyResult: "conflict"` without writing. `create_with_suffix`
1322
+ creates a distinct note.
1323
+
1170
1324
  **Path and Collision Rules:**
1171
1325
 
1172
1326
  - Explicit `--path` wins.
@@ -1320,6 +1474,10 @@ retrieval results and is used as trusted guidance during grounded answer
1320
1474
  generation. Matching scopes compose once in this order: global, collection,
1321
1475
  then path prefixes from broadest to most specific. Context guides interpretation;
1322
1476
  it is not searched and does not change ranking.
1477
+ Multiple distinct normalized texts may share one scope. The persisted identity
1478
+ is `(scopeType, canonicalScopeKey, normalizedText)`. Text normalization removes
1479
+ a leading BOM, converts CRLF/CR to LF, applies NFC, and trims surrounding
1480
+ whitespace; an exact normalized duplicate is rejected.
1323
1481
 
1324
1482
  **Synopsis:**
1325
1483
 
@@ -1393,18 +1551,20 @@ gno context check [--json|--md]
1393
1551
 
1394
1552
  ### gno context rm
1395
1553
 
1396
- Remove a context.
1554
+ Remove a context. Scope-only removal succeeds only when exactly one record
1555
+ matches. If multiple texts share the scope, the caller MUST pass exact text;
1556
+ ambiguous removal fails without mutation.
1397
1557
 
1398
1558
  **Synopsis:**
1399
1559
 
1400
1560
  ```bash
1401
- gno context rm <scope>
1561
+ gno context rm <scope> [text]
1402
1562
  ```
1403
1563
 
1404
1564
  **Exit Codes:**
1405
1565
 
1406
1566
  - 0: Success
1407
- - 1: Scope not found
1567
+ - 1: Invalid/not-found scope, text not found, or ambiguous scope-only removal
1408
1568
 
1409
1569
  ---
1410
1570
 
@@ -2952,6 +3112,38 @@ is blocked.
2952
3112
  - Mounts stateful Streamable HTTP MCP at `/mcp` only after the fail-closed
2953
3113
  actual-peer, Host, Origin, bearer, body, rate, request, queue, and session
2954
3114
  boundary initializes
3115
+ - On the loopback `gno serve` listener only, mounts the browser-clipper pairing,
3116
+ preview, and capture routes. These require exact extension/same-origin
3117
+ policies and dedicated origin-bound capture grants; MCP bearer tokens,
3118
+ `GNO_API_TOKEN`, and `gateway.enableWrite` do not authorize them. Clipper
3119
+ routes are structurally absent from non-loopback listeners. Capture
3120
+ idempotency persists exact content-free plan hashes and reconciles an
3121
+ interrupted atomic write only at its original path; changed plans fail
3122
+ closed without a duplicate write.
3123
+ - Serves the standalone `/clipper/pair` approval page. Its exact pair-ID
3124
+ fragment is synchronously validated and scrubbed before normal workspace
3125
+ state initializes; the user enters the eight-digit code manually. The page
3126
+ obtains same-origin CSRF state, never receives the extension grant, and does
3127
+ not persist pairing material.
3128
+ - Browser-clipper HTTP write receipts add `schemaVersion: "1.0"` to the closed
3129
+ shared capture receipt. HTTP 409 can therefore be either a valid provenance
3130
+ conflict receipt or a closed clipper error; clients parse the body contract
3131
+ before classifying the outcome.
3132
+ - Browser clip response/status closure is exact: HTTP 200 only for
3133
+ `opened_existing`; HTTP 202 only for `created`, `created_with_suffix`, or
3134
+ `overwritten`; HTTP 409 for either a valid `conflict` receipt or the matching
3135
+ closed `clipper-error@1.0`. Unknown versions, fields, codes, non-JSON bodies,
3136
+ and status/body mismatches fail as invalid responses. `CLIPPER_OFFLINE`,
3137
+ `CLIPPER_INVALID_RESPONSE`, and `CLIPPER_CLIENT` are client-only
3138
+ classifications.
3139
+ - Browser clip provenance contains exactly `extractionHash`, `finalBodyHash`,
3140
+ `clipIdentity`, and `previewDigest`; it does not contain `sourceHash`.
3141
+ - The Chromium service worker stores the loopback origin, plaintext bounded
3142
+ grant, and at most one `{payload, previewDigest, idempotencyKey}` in protected
3143
+ local extension storage. Unfinished pairing state uses extension session
3144
+ storage. Page extraction and the approval page receive neither grant nor
3145
+ pending-write state. Recovery reuses the same logical write or fails closed
3146
+ without changing destination.
2955
3147
  - 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
2956
3148
  - 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.
2957
3149
  - 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
@@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS contexts (
61
61
  scope_key TEXT NOT NULL,
62
62
  text TEXT NOT NULL,
63
63
  synced_at TEXT NOT NULL DEFAULT (datetime('now')),
64
- PRIMARY KEY (scope_type, scope_key)
64
+ PRIMARY KEY (scope_type, scope_key, text)
65
65
  );
66
66
 
67
67
  -- ─────────────────────────────────────────────────────────────────────────────
@@ -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
+ }