@hraness/sponge 0.2.5 → 0.3.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 (227) hide show
  1. package/README.md +23 -10
  2. package/bin/sponge.js +8 -7
  3. package/dist/contracts/index.js +24 -6
  4. package/dist/contracts/validate.js +106 -29
  5. package/dist/migrations/008.d.ts +3 -0
  6. package/dist/migrations/008.js +5 -0
  7. package/dist/migrations/009.d.ts +3 -0
  8. package/dist/migrations/009.js +8 -0
  9. package/dist/migrations/010.d.ts +3 -0
  10. package/dist/migrations/010.js +11 -0
  11. package/dist/migrations/index.d.ts +15 -0
  12. package/dist/migrations/index.js +39 -0
  13. package/dist/src/acquisition/capture.js +87 -28
  14. package/dist/src/acquisition/cleanup.d.ts +0 -4
  15. package/dist/src/acquisition/cleanup.js +3 -5
  16. package/dist/src/acquisition/errors.js +17 -2
  17. package/dist/src/acquisition/files.d.ts +0 -1
  18. package/dist/src/acquisition/files.js +14 -11
  19. package/dist/src/acquisition/index.d.ts +2 -3
  20. package/dist/src/acquisition/index.js +1 -2
  21. package/dist/src/acquisition/lock.js +1 -1
  22. package/dist/src/acquisition/normalize.js +57 -18
  23. package/dist/src/acquisition/pins.js +22 -4
  24. package/dist/src/acquisition/process.js +67 -29
  25. package/dist/src/acquisition/runtime.d.ts +5 -7
  26. package/dist/src/acquisition/runtime.js +129 -68
  27. package/dist/src/acquisition/verify-helper.js +10 -3
  28. package/dist/src/annotations/quote.js +40 -12
  29. package/dist/src/annotations/store.d.ts +0 -1
  30. package/dist/src/annotations/store.js +66 -14
  31. package/dist/src/app/article.d.ts +2 -1
  32. package/dist/src/app/article.js +66 -70
  33. package/dist/src/app/bundle-files.d.ts +0 -4
  34. package/dist/src/app/bundle-files.js +11 -14
  35. package/dist/src/app/markdown.js +18 -6
  36. package/dist/src/app/source.d.ts +0 -4
  37. package/dist/src/app/source.js +18 -14
  38. package/dist/src/cli/arguments.d.ts +2 -8
  39. package/dist/src/cli/arguments.js +67 -80
  40. package/dist/src/cli/commands.d.ts +42 -0
  41. package/dist/src/cli/commands.js +795 -0
  42. package/dist/src/cli/config.js +12 -9
  43. package/dist/src/cli/digest.d.ts +1 -1
  44. package/dist/src/cli/digest.js +39 -17
  45. package/dist/src/cli/import.js +25 -4
  46. package/dist/src/cli/indexing.d.ts +2 -0
  47. package/dist/src/cli/indexing.js +26 -9
  48. package/dist/src/cli/main.js +47 -386
  49. package/dist/src/cli/portability.d.ts +0 -2
  50. package/dist/src/cli/portability.js +13 -8
  51. package/dist/src/cli/queue.js +12 -8
  52. package/dist/src/cli/runtimes.d.ts +1 -3
  53. package/dist/src/cli/runtimes.js +13 -6
  54. package/dist/src/cli/terminal.js +1 -1
  55. package/dist/src/cli/worker.d.ts +7 -0
  56. package/dist/src/cli/worker.js +54 -15
  57. package/dist/src/derivations/kinds.d.ts +34 -0
  58. package/dist/src/derivations/kinds.js +14 -0
  59. package/dist/src/derivations/types.d.ts +48 -0
  60. package/dist/src/derivations/types.js +1 -0
  61. package/dist/src/digest/derivation.d.ts +25 -0
  62. package/dist/src/digest/derivation.js +124 -0
  63. package/dist/src/digest/editorial.d.ts +10 -46
  64. package/dist/src/digest/editorial.js +73 -59
  65. package/dist/src/digest/endpoint.js +63 -27
  66. package/dist/src/digest/pipeline.d.ts +53 -32
  67. package/dist/src/digest/pipeline.js +357 -125
  68. package/dist/src/digest/schema.d.ts +44 -17
  69. package/dist/src/digest/schema.js +132 -44
  70. package/dist/src/errors.d.ts +46 -0
  71. package/dist/src/errors.js +117 -0
  72. package/dist/src/identity/query-policy.d.ts +0 -5
  73. package/dist/src/identity/query-policy.js +61 -9
  74. package/dist/src/identity/uri.d.ts +6 -5
  75. package/dist/src/identity/uri.js +37 -18
  76. package/dist/src/identity/visibility.d.ts +11 -1
  77. package/dist/src/identity/visibility.js +81 -17
  78. package/dist/src/importers/apply.js +104 -36
  79. package/dist/src/importers/custody.js +27 -9
  80. package/dist/src/importers/inspect.js +125 -38
  81. package/dist/src/importers/jungle-digest.d.ts +31 -24
  82. package/dist/src/importers/jungle-digest.js +217 -115
  83. package/dist/src/importers/jungle-store.d.ts +20 -11
  84. package/dist/src/importers/jungle-store.js +136 -41
  85. package/dist/src/importers/jungle.d.ts +32 -16
  86. package/dist/src/importers/jungle.js +145 -32
  87. package/dist/src/importers/manifest.js +57 -21
  88. package/dist/src/importers/verify-helper.js +18 -4
  89. package/dist/src/indexing/image.js +29 -10
  90. package/dist/src/indexing/index.d.ts +37 -6
  91. package/dist/src/indexing/index.js +360 -124
  92. package/dist/src/indexing/jobs.js +156 -47
  93. package/dist/src/indexing/prune-run.d.ts +2 -0
  94. package/dist/src/indexing/prune-run.js +69 -26
  95. package/dist/src/indexing/prune.d.ts +12 -3
  96. package/dist/src/indexing/prune.js +85 -35
  97. package/dist/src/indexing/runner.d.ts +2 -1
  98. package/dist/src/indexing/runner.js +83 -40
  99. package/dist/src/indexing/upgrade.d.ts +9 -0
  100. package/dist/src/indexing/upgrade.js +79 -0
  101. package/dist/src/inference/asr/bridge.js +6 -2
  102. package/dist/src/inference/asr/protocol.js +109 -19
  103. package/dist/src/inference/asr/runtime.js +58 -21
  104. package/dist/src/inference/asr/windows.js +9 -5
  105. package/dist/src/inference/embedding/index.d.ts +2 -6
  106. package/dist/src/inference/embedding/index.js +89 -29
  107. package/dist/src/ingest/attempt.d.ts +29 -0
  108. package/dist/src/ingest/attempt.js +55 -0
  109. package/dist/src/media/capture-helper.js +7 -2
  110. package/dist/src/media/capture.d.ts +2 -1
  111. package/dist/src/media/capture.js +124 -43
  112. package/dist/src/media/handler.d.ts +4 -2
  113. package/dist/src/media/handler.js +88 -70
  114. package/dist/src/media/transcribe.js +63 -21
  115. package/dist/src/metadata/article.js +49 -17
  116. package/dist/src/models/artifacts.d.ts +10 -8
  117. package/dist/src/models/artifacts.js +237 -106
  118. package/dist/src/models/catalog.d.ts +2 -5
  119. package/dist/src/models/catalog.js +94 -25
  120. package/dist/src/models/download.js +55 -15
  121. package/dist/src/models/runtime.d.ts +0 -2
  122. package/dist/src/models/runtime.js +69 -33
  123. package/dist/src/pdf/capture-helper.js +3 -1
  124. package/dist/src/pdf/capture.d.ts +2 -1
  125. package/dist/src/pdf/capture.js +95 -27
  126. package/dist/src/pdf/errors.js +12 -1
  127. package/dist/src/pdf/handler.d.ts +2 -1
  128. package/dist/src/pdf/handler.js +70 -71
  129. package/dist/src/pdf/index.d.ts +1 -3
  130. package/dist/src/pdf/index.js +0 -2
  131. package/dist/src/pdf/input.js +12 -7
  132. package/dist/src/pdf/normalize.js +80 -21
  133. package/dist/src/pdf/source-identity.js +3 -2
  134. package/dist/src/pdf/source.js +12 -8
  135. package/dist/src/pdf/verify-helper.js +15 -3
  136. package/dist/src/portability/backup.d.ts +2 -9
  137. package/dist/src/portability/backup.js +108 -22
  138. package/dist/src/portability/database.js +51 -17
  139. package/dist/src/portability/export.js +35 -9
  140. package/dist/src/portability/files.d.ts +1 -4
  141. package/dist/src/portability/files.js +127 -67
  142. package/dist/src/portability/gc.js +65 -22
  143. package/dist/src/publish/component.js +1 -1
  144. package/dist/src/publish/feed.d.ts +2 -2
  145. package/dist/src/publish/feed.js +75 -29
  146. package/dist/src/publish/identity.d.ts +5 -1
  147. package/dist/src/publish/identity.js +31 -11
  148. package/dist/src/publish/index.d.ts +2 -3
  149. package/dist/src/publish/index.js +34 -21
  150. package/dist/src/publish/projection.d.ts +2 -9
  151. package/dist/src/publish/projection.js +21 -14
  152. package/dist/src/search/extension.js +3 -1
  153. package/dist/src/search/hybrid.d.ts +3 -8
  154. package/dist/src/search/hybrid.js +45 -11
  155. package/dist/src/search/lexical.js +18 -4
  156. package/dist/src/search/query.js +56 -25
  157. package/dist/src/search/vector.js +37 -10
  158. package/dist/src/service/launch-agent.d.ts +5 -3
  159. package/dist/src/service/launch-agent.js +272 -83
  160. package/dist/src/service/lifetime.js +47 -14
  161. package/dist/src/service/loop.d.ts +3 -3
  162. package/dist/src/service/loop.js +47 -19
  163. package/dist/src/store/annotations.d.ts +32 -0
  164. package/dist/src/store/annotations.js +44 -0
  165. package/dist/src/store/backups.d.ts +18 -0
  166. package/dist/src/store/backups.js +110 -0
  167. package/dist/src/store/blobs.d.ts +24 -0
  168. package/dist/src/store/blobs.js +139 -0
  169. package/dist/src/store/changes.d.ts +5 -12
  170. package/dist/src/store/changes.js +108 -56
  171. package/dist/src/store/context.d.ts +15 -0
  172. package/dist/src/store/context.js +19 -0
  173. package/dist/src/store/derivations.d.ts +42 -0
  174. package/dist/src/store/derivations.js +104 -0
  175. package/dist/src/store/files.d.ts +0 -2
  176. package/dist/src/store/files.js +19 -24
  177. package/dist/src/store/ids.d.ts +30 -0
  178. package/dist/src/store/ids.js +34 -0
  179. package/dist/src/store/imports.d.ts +26 -0
  180. package/dist/src/store/imports.js +57 -0
  181. package/dist/src/store/index.d.ts +3 -186
  182. package/dist/src/store/index.js +2 -809
  183. package/dist/src/store/lease.d.ts +20 -0
  184. package/dist/src/store/lease.js +62 -0
  185. package/dist/src/store/library.d.ts +75 -0
  186. package/dist/src/store/library.js +174 -0
  187. package/dist/src/store/open.d.ts +7 -0
  188. package/dist/src/store/open.js +183 -0
  189. package/dist/src/store/queue.d.ts +65 -0
  190. package/dist/src/store/queue.js +327 -0
  191. package/dist/src/store/revisions.d.ts +12 -0
  192. package/dist/src/store/revisions.js +189 -0
  193. package/dist/src/store/search-index.d.ts +63 -0
  194. package/dist/src/store/search-index.js +190 -0
  195. package/dist/src/store/sync-ready.d.ts +6 -0
  196. package/dist/src/store/sync-ready.js +121 -0
  197. package/dist/src/store/types.d.ts +1 -5
  198. package/dist/src/store/types.js +1 -8
  199. package/dist/src/util/canonical.d.ts +2 -0
  200. package/dist/src/util/canonical.js +17 -0
  201. package/dist/src/util/exact.d.ts +2 -0
  202. package/dist/src/util/exact.js +9 -0
  203. package/dist/src/util/fsync.d.ts +4 -0
  204. package/dist/src/util/fsync.js +22 -0
  205. package/dist/src/util/hash.d.ts +4 -0
  206. package/dist/src/util/hash.js +8 -0
  207. package/dist/src/worker/basic.d.ts +0 -1
  208. package/dist/src/worker/basic.js +23 -7
  209. package/dist/src/worker/custody.d.ts +4 -1
  210. package/dist/src/worker/custody.js +111 -38
  211. package/dist/src/worker/engine.d.ts +2 -1
  212. package/dist/src/worker/engine.js +42 -20
  213. package/dist/src/worker/identity.js +13 -5
  214. package/dist/src/worker/journal.d.ts +1 -1
  215. package/dist/src/worker/journal.js +54 -24
  216. package/dist/src/worker/lock.d.ts +18 -0
  217. package/dist/src/worker/lock.js +196 -0
  218. package/dist/src/worker/runtime-gc.d.ts +4 -3
  219. package/dist/src/worker/runtime-gc.js +68 -41
  220. package/dist/src/worker/supervisor-child.js +19 -6
  221. package/dist/src/worker/supervisor.js +41 -8
  222. package/native/runtime-manifest.json +4 -4
  223. package/package.json +10 -2
  224. package/dist/src/digest/store.d.ts +0 -57
  225. package/dist/src/digest/store.js +0 -77
  226. package/dist/src/store/foundation.d.ts +0 -7
  227. package/dist/src/store/foundation.js +0 -7
@@ -4,412 +4,73 @@ import { lstat } from 'node:fs/promises';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { terminalSafe } from './terminal.js';
6
6
  import { parseArguments } from './arguments.js';
7
- import { readConnections } from './config.js';
8
- import { pdfSource } from '../pdf/index.js';
9
- import { articleSource, mediaSource, InputError } from '../app/source.js';
10
- import { openLibrary, StoreError } from '../store/index.js';
11
- import { defaultCapturePins, inspectCaptureRuntime, provisionCapture, captureSetupPlan } from '../acquisition/index.js';
12
- import { queueAction } from './queue.js';
13
- import { portabilityAction, restoreAction } from './portability.js';
14
- import { workerAction, serveWorker, hasDueAcquisition } from './worker.js';
15
- import { nativeSetupPlan, provisionNative, inspectNative } from '../models/runtime.js';
16
- import { nativeDownloadAuthGuidance } from '../models/download.js';
17
- import { drainIndexQueue } from '../indexing/runner.js';
18
- import { indexAction } from './indexing.js';
19
- import { pruneText } from '../indexing/prune-run.js';
20
- import { importAction, jungleAction, jungleText } from './import.js';
21
- import { digestText, reviewFromSpec } from './digest.js';
22
- import { connectEndpoint } from '../digest/endpoint.js';
23
- import { createDigest } from '../digest/pipeline.js';
24
- import { createQueryEmbedder, createTextEmbedder } from '../search/query.js';
25
- import { runtimeGcAction, runtimeGcText, setupRuntimeGc } from './runtimes.js';
26
- import { drainLibrary } from '../worker/basic.js';
27
- import { minAge, publishSite } from '../publish/index.js';
7
+ import { command, usageOf } from './commands.js';
8
+ import { openLibrary } from '../store/index.js';
9
+ import { defaultCapturePins } from '../acquisition/index.js';
28
10
  import { failure } from '../acquisition/errors.js';
29
- const help = `sponge — a local article library
30
-
31
- init Create a private library
32
- setup --capture|--asr|--embedding Download one pinned runtime
33
- add URL_OR_PDF [--kind article|pdf|media] [--connection ID] [--wait]
34
- run --drain Process queued articles sequentially
35
- queue status Show jobs and their states
36
- queue cancel JOB Cancel a queued or active job
37
- queue retry JOB Retry a failed job after resolving its cause
38
- queue recover Reconcile a stopped worker after cleanup
39
- list List saved and queued items
40
- show ITEM [--format markdown|json]
41
- asset ITEM SHA Write a verified asset to stdout
42
- search QUERY [--mode hybrid|lexical] [--profile default] [--limit 20]
43
- refresh ITEM [--wait] Capture a new revision
44
- delete ITEM Hide an item and remove it from search
45
- index create [--profile default] Queue a new embedding generation
46
- index status GENERATION Inspect generation coverage
47
- index run Drain queued embedding jobs
48
- index rollback GENERATION Reactivate a compatible complete generation
49
- index prune [--keep 1] [--dry-run] [--vacuum] Delete vectors of retired generations
50
- doctor Check library and runtime states
51
- export ITEM --output DIR Export Markdown and local assets
52
- backup --output FILE Save a verified offline backup
53
- restore FILE --into NEW_DIR Restore into a new private library
54
- gc --dry-run Write a report of unreferenced blobs
55
- gc --apply REPORT Recheck and remove reported blobs
56
- gc --runtimes [--dry-run] Remove superseded runtimes, models and worker journals
57
- worker install [--language en] Enable the optional background worker
58
- worker status Inspect the background worker
59
- worker uninstall Stop it and preserve the library
60
- import MANIFEST [--apply] Preview or import existing Wordcell captures
61
- import jungle DIR [--apply] Preview or import Jungle Reading notes, their captures and published state
62
- visibility ITEM public|private|default Override or restore the default
63
- highlight ITEM TEXT [--note BODY] [--occurrence N] Anchor a verbatim quote
64
- comment ITEM BODY [--on ANNOTATION] Comment on an item or reply
65
- publish --dry-run [--min-age-days N] Show the public reading-list entries
66
- publish --output DIR [--title T] [--base-url https://...] [--min-age-days N]
67
- Write a signed static feed site with <sponge-list>
68
- digest ITEM --endpoint http://127.0.0.1:PORT --model NAME [--check-support]
69
- Draft a digest with a local model server you started
70
- digest ITEM Show the current digest and its review state
71
- review DIGEST --accept SPEC --reject SPEC Review fields: all, gist, idea:N, quote:N.M
72
-
73
- Global flags: --library /absolute/path --json
74
- Items are public by default only when captured from a public source.
75
- Use -- before an option-like search query. Capture setup is explicit.
76
- Digest endpoints must be literal loopback HTTP (127.0.0.1 or [::1]); Sponge never starts a model server.
77
- Media: --language en enables local English ASR; --force-local bypasses captions.
78
- These flags apply to --wait, run --drain, or worker serve.
79
- worker install --language en saves the English ASR choice for background work.
80
- For trusted local sources, use --allow-private-network with --wait or run.
81
- `;
82
- const codes = new Set(['grant-permission', 'repair-auth', 'unsupported', 'unavailable', 'rate-limited', 'partial', 'cleanup-required', 'cancelled', 'invalid-input', 'unclassified', 'integrity', 'limit-exceeded', 'runtime-missing', 'schema-newer', 'owner-busy']);
83
- function errorCode(error, fallback) { const code = error?.code; return code && codes.has(code) ? code : fallback; }
84
- function exitFor(code) { return code === 'cancelled' ? 130 : code === 'invalid-input' ? 2 : code === 'partial' ? 3 : ['grant-permission', 'repair-auth', 'cleanup-required', 'runtime-missing', 'owner-busy'].includes(code) ? 4 : 1; }
11
+ import { SpongeError, codeOf, publicCode, exitFor, hints, debugChain } from '../errors.js';
12
+ /** Failures print one categorical JSON line: the public code, its specific `reason` when narrower, and a fixed hint.
13
+ * Causes, details, paths and upstream messages appear only with --debug or SPONGE_DEBUG=1. */
14
+ function report(error, requestId, debug) {
15
+ const reason = codeOf(error), code = publicCode(reason), issues = error?.issues, usage = error?.detail?.command;
16
+ if (Array.isArray(issues))
17
+ process.stderr.write(JSON.stringify({ issues }) + '\n');
18
+ const hint = code === 'invalid-input' && typeof usage === 'string' && command(usage)
19
+ ? `Usage: ${usageOf(usage).replace(/\n/g, ' | ')}`
20
+ : hints[code];
21
+ process.stderr.write(JSON.stringify({ ...failure(requestId, code), ...(reason !== code ? { reason } : {}), hint }) + '\n');
22
+ if (debug)
23
+ process.stderr.write(debugChain(error));
24
+ return exitFor(code);
25
+ }
85
26
  export async function main(argv, version) {
86
27
  const requestId = randomUUID();
87
- let store;
28
+ let store, args;
88
29
  const controller = new AbortController();
89
30
  const cancel = () => controller.abort();
90
31
  process.on('SIGINT', cancel);
91
32
  process.on('SIGTERM', cancel);
92
33
  try {
93
- const args = parseArguments(argv), emit = (data, text) => { const output = args.json ? JSON.stringify({ schemaVersion: 1, requestId, ok: true, ...data }) + '\n' : (text ?? JSON.stringify(data)) + '\n'; process.stdout.write(process.stdout.isTTY ? terminalSafe(output) : output); };
94
- if (args.command === 'help') {
95
- process.stdout.write(help);
96
- return 0;
97
- }
98
- if (args.command === 'version') {
99
- process.stdout.write(version + '\n');
100
- return 0;
101
- }
102
- if (args.command === 'restore') {
103
- emit(await restoreAction(args.positional[0], String(args.flags.into)));
104
- return 0;
105
- }
34
+ args = parseArguments(argv);
35
+ const json = args.json, spec = command(args.command);
36
+ const emit = (data, text) => {
37
+ const output = json
38
+ ? JSON.stringify({ schemaVersion: 1, requestId, ok: true, ...data }) + '\n'
39
+ : (text ?? JSON.stringify(data)) + '\n';
40
+ process.stdout.write(process.stdout.isTTY ? terminalSafe(output) : output);
41
+ };
42
+ const context = {
43
+ args,
44
+ version,
45
+ signal: controller.signal,
46
+ emit,
47
+ write: (text) => {
48
+ process.stdout.write(text);
49
+ },
50
+ };
51
+ if (spec.library === 'none')
52
+ return await spec.handler(context);
106
53
  const root = args.library ?? join(homedir(), '.local', 'share', 'sponge');
107
- if (args.command !== 'init') {
54
+ if (spec.library === 'open') {
108
55
  try {
109
56
  await lstat(join(root, '.sponge-library'));
110
57
  }
111
58
  catch (error) {
112
59
  if (error.code === 'ENOENT')
113
- throw new StoreError('unavailable');
60
+ throw new SpongeError('unavailable', { cause: error });
114
61
  throw error;
115
62
  }
116
63
  }
117
64
  store = await openLibrary(root);
118
- if (args.command === 'init') {
119
- emit({ library: store.root }, `Library: ${store.root}`);
120
- return 0;
121
- }
122
- if (args.command === 'worker' && args.positional[0] !== 'serve') {
123
- emit(await workerAction(store, args.positional[0], version, undefined, { language: args.flags.language }));
124
- return 0;
125
- }
126
- if (args.command === 'gc' && args.flags.runtimes) {
127
- const report = await runtimeGcAction(store, version, args.flags['dry-run'] === true);
128
- emit(report, runtimeGcText(report));
129
- return 0;
130
- }
131
- if (args.command === 'export' || args.command === 'backup' || args.command === 'gc') {
132
- emit(await portabilityAction(store, args.command, { itemId: args.positional[0], output: args.flags.output, dryRun: args.flags['dry-run'] === true, report: args.flags.apply }));
133
- return 0;
134
- }
135
- const runtimeRoot = join(store.root, 'runtime', 'capture', defaultCapturePins.id);
136
- const nativeRoot = join(store.root, 'runtime', 'models');
137
- if (args.command === 'setup' && !args.flags.capture) {
138
- const kind = args.flags.asr ? 'asr' : 'embedding', plan = await nativeSetupPlan(kind, version);
139
- process.stderr.write(JSON.stringify({ setup: kind, ...plan }) + '\n');
140
- try {
141
- await provisionNative(kind, nativeRoot, version, controller.signal);
142
- }
143
- catch (error) {
144
- if (error.code === 'repair-auth' && !args.json)
145
- process.stderr.write(nativeDownloadAuthGuidance + '\n');
146
- throw error;
147
- }
148
- const gc = await setupRuntimeGc(store, version);
149
- emit({ [kind]: 'ready', gc });
150
- return 0;
151
- }
152
- if (args.command === 'setup') {
153
- const plan = captureSetupPlan(defaultCapturePins);
154
- process.stderr.write(`Capture setup: ${plan.downloadBytes} pinned artifact bytes, plus frozen dependency downloads. Temporary installation limit: ${plan.stagingBudgetBytes} bytes; retained runtime limit: ${plan.installationBudgetBytes} bytes; dependency installation deadline: ${plan.installationDeadlineMs / 60000} minutes. ${plan.requiredFreeBytes} free bytes required.\n`);
155
- await provisionCapture({ runtimeRoot: join(store.root, 'runtime', 'capture'), pins: defaultCapturePins, signal: controller.signal });
156
- const gc = await setupRuntimeGc(store, version);
157
- emit({ capture: 'ready', gc }, 'skipped' in gc ? `Capture ready\nRuntime GC skipped: ${gc.skipped}` : `Capture ready\n${runtimeGcText(gc)}`);
158
- return 0;
159
- }
160
- if (args.command === 'list') {
161
- const items = store.list();
162
- emit({ items }, items.map(item => `${item.itemId}\t${item.title}`).join('\n'));
163
- return 0;
164
- }
165
- if (args.command === 'show') {
166
- const item = store.read(args.positional[0]);
167
- if (!item)
168
- throw new StoreError('unavailable');
169
- emit({ item, identity: store.identity(item.itemId), annotations: store.listAnnotations(item.itemId), digest: store.currentDigest(item.itemId) ?? null, editorial: store.currentEditorial(item.itemId) ?? null }, item.markdown);
170
- return item.status === 'partial' ? 3 : 0;
171
- }
172
- if (args.command === 'asset') {
173
- const item = store.read(args.positional[0]), asset = item?.assets.find(a => a.sha256 === args.positional[1]);
174
- if (!asset)
175
- throw new StoreError('unavailable');
176
- const bytes = store.readBlob(asset);
177
- if (args.json)
178
- emit({ asset });
179
- else {
180
- if (process.stdout.isTTY)
181
- throw new InputError();
182
- process.stdout.write(bytes);
183
- }
184
- return 0;
185
- }
186
- if (args.command === 'index') {
187
- const action = args.positional[0], runtime = ['create', 'run'].includes(action) ? await inspectNative('embedding', nativeRoot, version) : undefined;
188
- const result = await indexAction(store, action, args.positional[1], { runtime, profile: args.flags.profile, signal: controller.signal, ...(action === 'prune' ? { keep: args.flags.keep === undefined ? undefined : Number(args.flags.keep), dryRun: args.flags['dry-run'] === true, vacuum: args.flags.vacuum === true } : {}) });
189
- emit(result, result.prune ? pruneText(result.prune) : undefined);
190
- const failed = result.results?.find(r => !r.ok);
191
- return failed ? exitFor(failed.code) : 0;
192
- }
193
- if (args.command === 'search') {
194
- if (args.flags.mode === 'lexical') {
195
- const items = store.search(args.positional[0], Number(args.flags.limit ?? 20));
196
- emit({ items }, items.map(item => `${item.itemId}\t${item.title}`).join('\n'));
197
- return 0;
198
- }
199
- let embedding;
200
- try {
201
- embedding = await inspectNative('embedding', nativeRoot, version);
202
- }
203
- catch { /* Hybrid explicitly reports semantic unavailability and preserves lexical results. */ }
204
- const result = await store.searchHybrid(args.positional[0], { profileId: args.flags.profile, limit: Number(args.flags.limit ?? 20), signal: controller.signal, ...(embedding ? { embed: createQueryEmbedder(store, embedding) } : {}) });
205
- emit({ items: result.hits, warnings: result.warnings, generation: result.generation }, [...result.hits.map(item => `${item.itemId}\t${item.title}`), ...result.warnings.map(warning => `Warning: ${warning}`)].join('\n'));
206
- return controller.signal.aborted ? 130 : 0;
207
- }
208
- if (args.command === 'visibility') {
209
- const value = args.positional[1] === 'default' ? null : args.positional[1];
210
- const state = store.setVisibility(args.positional[0], value);
211
- emit({ itemId: args.positional[0], visibility: state }, `${state.effective}${state.override ? ' (set)' : ' (default)'}`);
212
- return 0;
213
- }
214
- if (args.command === 'highlight') {
215
- const annotation = store.addHighlight({ itemId: args.positional[0], quote: args.positional[1], ...(typeof args.flags.note === 'string' ? { note: args.flags.note } : {}), ...(args.flags.occurrence !== undefined ? { occurrence: Number(args.flags.occurrence) } : {}) });
216
- emit({ annotation }, annotation.id);
217
- return 0;
218
- }
219
- if (args.command === 'comment') {
220
- const annotation = store.addComment({ itemId: args.positional[0], body: args.positional[1], ...(typeof args.flags.on === 'string' ? { parentId: args.flags.on } : {}) });
221
- emit({ annotation }, annotation.id);
222
- return 0;
223
- }
224
- if (args.command === 'publish') {
225
- const minAgeDays = Number(args.flags['min-age-days'] ?? 0);
226
- if (typeof args.flags.output === 'string') {
227
- const result = publishSite(store, { output: args.flags.output, minAgeDays, ...(typeof args.flags.title === 'string' ? { title: args.flags.title } : {}), ...(typeof args.flags['base-url'] === 'string' ? { baseUrl: args.flags['base-url'] } : {}) });
228
- emit(result, `Published ${result.entries} entr${result.entries === 1 ? 'y' : 'ies'} to ${result.output}`);
229
- return 0;
230
- }
231
- const all = store.publicProjection(), projection = { ...all, entries: minAge(all.entries, minAgeDays) };
232
- emit({ dryRun: true, ...projection, ...(minAgeDays ? { held: all.entries.length - projection.entries.length } : {}) }, [...projection.entries.map(entry => `${entry.uri}\t${entry.title}`), ...(projection.skipped.length ? [`Skipped ${projection.skipped.length} public item(s) without a public link`] : [])].join('\n') || 'Nothing public');
233
- return 0;
234
- }
235
- if (args.command === 'digest') {
236
- if (!args.flags.endpoint) {
237
- const digest = store.currentDigest(args.positional[0]);
238
- if (!digest)
239
- throw new StoreError('unavailable');
240
- emit({ digest }, digestText(digest));
241
- return 0;
242
- }
243
- const model = await connectEndpoint({ endpoint: String(args.flags.endpoint), model: String(args.flags.model), signal: controller.signal });
244
- const embed = args.flags['check-support'] ? createTextEmbedder(store, await inspectNative('embedding', nativeRoot, version)) : undefined;
245
- const result = await createDigest(store, args.positional[0], model, { signal: controller.signal, ...(embed ? { embed } : {}) });
246
- emit({ digest: result.derivation, duplicate: result.duplicate, ...(result.trace ? { trace: result.trace } : {}) }, digestText(result.derivation));
247
- return 0;
248
- }
249
- if (args.command === 'review') {
250
- const prior = store.readDigest(args.positional[0]);
251
- if (!prior)
252
- throw new StoreError('unavailable');
253
- const digest = store.reviewDigest(prior.id, reviewFromSpec(prior, args.flags.accept, args.flags.reject));
254
- emit({ digest }, digestText(digest));
255
- return 0;
256
- }
257
- if (args.command === 'delete') {
258
- store.delete(args.positional[0]);
259
- emit({ itemId: args.positional[0], deleted: true }, 'Deleted');
260
- return 0;
261
- }
262
- if (args.command === 'queue') {
263
- const result = await queueAction(store, args.positional[0], args.positional[1]);
264
- emit(result);
265
- return 0;
266
- }
267
- if (args.command === 'doctor') {
268
- let capture = 'ready';
269
- try {
270
- await inspectCaptureRuntime(runtimeRoot, defaultCapturePins);
271
- }
272
- catch (error) {
273
- capture = errorCode(error, 'runtime-missing');
274
- }
275
- const states = {};
276
- for (const kind of ['asr', 'embedding']) {
277
- try {
278
- await inspectNative(kind, nativeRoot, version);
279
- states[kind] = 'ready';
280
- }
281
- catch (error) {
282
- states[kind] = errorCode(error, 'runtime-missing');
283
- }
284
- }
285
- const cleanup = store.pendingCleanupCount();
286
- emit({ library: 'ready', capture, ...states, pendingCleanup: cleanup }, `Library: ready\nCapture: ${capture}\nASR: ${states.asr}\nEmbedding: ${states.embedding}\nPending cleanup: ${cleanup}`);
287
- return cleanup || capture !== 'ready' ? 4 : 0;
288
- }
289
- if (args.command === 'run' && !hasDueAcquisition(store)) {
290
- if (store.ownerSnapshot())
291
- throw new StoreError('owner-busy');
292
- if (store.pendingCleanupCount())
293
- throw Object.assign(Error('cleanup-required'), { code: 'cleanup-required' });
294
- if (!store.listJobs().some(job => job.operation === 'index' && job.state === 'queued')) {
295
- emit({ results: [], indexResults: [] }, 'Queue empty');
296
- return 0;
297
- }
298
- const runtime = await inspectNative('embedding', nativeRoot, version), indexResults = await drainIndexQueue(store, { runtime, signal: controller.signal });
299
- emit({ results: [], indexResults });
300
- const failed = indexResults.find(result => !result.ok);
301
- return failed && !failed.ok ? exitFor(failed.code) : controller.signal.aborted ? 130 : 0;
302
- }
303
- const connections = await readConnections(store.root);
304
- let jobId, itemId;
305
- if (args.command === 'add') {
306
- const connection = args.flags.connection ? connections.find(c => c.id === args.flags.connection) : undefined;
307
- if (args.flags.connection && !connection)
308
- throw new InputError();
309
- const input = args.positional[0], kind = (args.flags.kind ?? (/\.pdf(?:[?#]|$)/i.test(input) || input.startsWith('file:') ? 'pdf' : 'article'));
310
- const source = (kind === 'pdf' ? pdfSource : kind === 'media' ? mediaSource : articleSource)(input, connection?.realm ?? 'public');
311
- const submission = store.submitSource({ kind, ...source, idempotencyKey: 'save:' + source.sourceKey });
312
- jobId = submission.jobId;
313
- itemId = submission.itemId;
314
- const job = store.listJobs().find(j => j.id === jobId);
315
- if (!args.flags.wait) {
316
- emit({ ...submission, state: job?.state ?? 'queued' }, `${itemId}\t${job?.state ?? 'queued'}`);
317
- return 0;
318
- }
319
- if (job?.state === 'succeeded') {
320
- const item = store.read(itemId);
321
- emit({ itemId, jobId, status: item.status }, `${itemId}\t${item.status}`);
322
- return item.status === 'partial' ? 3 : 0;
323
- }
324
- }
325
- else if (args.command === 'refresh') {
326
- const previous = store.listJobs().find(j => j.item_id === args.positional[0]), source = previous && store.sourceForJob(String(previous.id));
327
- if (!source || !['article', 'pdf', 'media'].includes(source.kind))
328
- throw new StoreError('unavailable');
329
- const submission = store.submitSource({ ...source, kind: source.kind, intent: 'refresh', idempotencyKey: randomUUID() });
330
- jobId = submission.jobId;
331
- itemId = submission.itemId;
332
- if (!args.flags.wait) {
333
- emit(submission, `${itemId}\tqueued`);
334
- return 0;
335
- }
336
- }
337
- if (jobId) {
338
- const job = store.listJobs().find(j => j.id === jobId);
339
- if (job && job.state !== 'queued') {
340
- const code = job.state === 'cancelled' ? 'cancelled' : job.state === 'cleanup-pending' ? 'cleanup-required' : job.state === 'running' ? 'owner-busy' : errorCode({ code: job.reason }, 'unclassified');
341
- throw Object.assign(Error(code), { code });
342
- }
343
- }
344
- if (args.command === 'worker') {
345
- emit(await serveWorker(store, { signal: controller.signal, language: args.flags.language, forceLocal: args.flags['force-local'] === true, resolveCaptureRuntime: () => inspectCaptureRuntime(runtimeRoot, defaultCapturePins), resolveConnection: (realm) => { if (realm === 'public')
346
- return undefined; const connection = connections.find(c => c.realm === realm); if (!connection)
347
- throw Object.assign(Error('repair-auth'), { code: 'repair-auth' }); return connection.capture; } }, version));
348
- return 0;
349
- }
350
- if (args.command === 'import' && args.positional[0] === 'jungle') {
351
- // A dry run reads Jungle and the library only; applying reverifies every capture with the released verifier.
352
- const dryRun = args.flags.apply !== true, runtime = dryRun ? undefined : await inspectCaptureRuntime(runtimeRoot, defaultCapturePins).catch(error => { const code = errorCode(error, 'runtime-missing'); throw Object.assign(Error(code), { code }); });
353
- const report = await jungleAction(store, args.positional[1], { dryRun, runtime, limit: Number(args.flags.limit ?? 1000), offset: Number(args.flags.offset ?? 0), signal: controller.signal });
354
- emit({ report }, jungleText(report));
355
- return controller.signal.aborted ? 130 : report.counts.skipped['capture-failed'] || report.counts.skipped['capture-unsupported'] ? 3 : 0;
356
- }
357
- let runtime;
358
- try {
359
- runtime = await inspectCaptureRuntime(runtimeRoot, defaultCapturePins);
360
- }
361
- catch (error) {
362
- const code = errorCode(error, 'runtime-missing');
363
- throw Object.assign(Error(code), { code });
364
- }
365
- if (args.command === 'import') {
366
- const report = await importAction(store, args.positional[0], { runtime, dryRun: args.flags.apply !== true, limit: Number(args.flags.limit ?? 20), offset: Number(args.flags.offset ?? 0), signal: controller.signal });
367
- emit({ report });
368
- return controller.signal.aborted ? 130 : report.entries.some(entry => entry.status === 'failed' || entry.status === 'unsupported') ? 3 : 0;
369
- }
370
- let asrRuntime;
371
- try {
372
- asrRuntime = await inspectNative('asr', nativeRoot, version);
373
- }
374
- catch (error) {
375
- if (!['runtime-missing', 'unsupported'].includes(errorCode(error, 'integrity')))
376
- throw error;
377
- }
378
- const workerOptions = { asrRuntime, language: args.flags.language, forceLocal: args.flags['force-local'] === true, runtime, jobId, signal: controller.signal, allowPrivateNetwork: args.flags['allow-private-network'] === true, resolveConnection: (realm) => { if (realm === 'public')
379
- return undefined; const connection = connections.find(c => c.realm === realm); if (!connection)
380
- throw Object.assign(Error('repair-auth'), { code: 'repair-auth' }); return connection.capture; } };
381
- const results = await drainLibrary(store, workerOptions);
382
- if (jobId && !results.length && !controller.signal.aborted) {
383
- const job = store.listJobs().find(j => j.id === jobId);
384
- const code = job?.state === 'cancelled' ? 'cancelled' : job?.state === 'cleanup-pending' ? 'cleanup-required' : job?.state === 'running' ? 'owner-busy' : errorCode({ code: job?.reason }, 'unclassified');
385
- throw Object.assign(Error(code), { code });
386
- }
387
- let indexResults = [];
388
- if (args.command === 'run' && !controller.signal.aborted && store.listJobs().some(job => job.operation === 'index' && job.state === 'queued')) {
389
- const embedding = await inspectNative('embedding', nativeRoot, version);
390
- indexResults = await drainIndexQueue(store, { runtime: embedding, signal: controller.signal });
391
- const indexFailed = indexResults.find(result => !result.ok);
392
- if (indexFailed && !indexFailed.ok) {
393
- process.stderr.write(JSON.stringify(indexFailed) + '\n');
394
- return exitFor(indexFailed.code);
395
- }
396
- }
397
- const failed = results.find(result => !result.ok);
398
- if (failed && !failed.ok) {
399
- process.stderr.write(JSON.stringify(failed) + '\n');
400
- return exitFor(failed.code);
401
- }
402
- if (controller.signal.aborted)
403
- return 130;
404
- emit({ results, indexResults }, results.map(result => result.ok ? `${result.itemId}\t${result.status}` : '').join('\n') || 'Queue empty');
405
- return results.some(result => result.ok && result.status === 'partial') ? 3 : 0;
65
+ return await spec.handler({
66
+ ...context,
67
+ store,
68
+ captureRoot: join(store.root, 'runtime', 'capture', defaultCapturePins.id),
69
+ nativeRoot: join(store.root, 'runtime', 'models'),
70
+ });
406
71
  }
407
72
  catch (error) {
408
- const value = error?.code, code = value && codes.has(value) ? value : 'unclassified', issues = error?.issues;
409
- if (Array.isArray(issues))
410
- process.stderr.write(JSON.stringify({ issues }) + '\n');
411
- process.stderr.write(JSON.stringify(failure(requestId, code)) + '\n');
412
- return exitFor(code);
73
+ return report(error, requestId, process.env.SPONGE_DEBUG === '1' || (args ? args.debug : argv.includes('--debug')));
413
74
  }
414
75
  finally {
415
76
  store?.close();
@@ -1,9 +1,7 @@
1
1
  import type { LibraryStore } from '../store/index.js';
2
- import { restoreLibrary } from '../portability/backup.js';
3
2
  export declare function portabilityAction(store: LibraryStore, action: 'export' | 'backup' | 'gc', options: {
4
3
  itemId?: string;
5
4
  output?: string;
6
5
  dryRun?: boolean;
7
6
  report?: string;
8
7
  }): Promise<Record<string, unknown>>;
9
- export declare const restoreAction: typeof restoreLibrary;
@@ -1,34 +1,40 @@
1
1
  import { fstatSync, closeSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
- import { StoreError } from '../store/types.js';
5
4
  import { openRead, readExact, writePrivateExclusive } from '../portability/files.js';
6
- import { backupLibrary, restoreLibrary } from '../portability/backup.js';
5
+ import { backupLibrary } from '../portability/backup.js';
7
6
  import { exportItem } from '../portability/export.js';
8
7
  import { createGcReport, applyGcReport } from '../portability/gc.js';
8
+ import { SpongeError } from '../errors.js';
9
9
  export async function portabilityAction(store, action, options) {
10
10
  if (action === 'export') {
11
11
  if (!options.itemId || !options.output)
12
- throw new StoreError('invalid-input');
12
+ throw new SpongeError('invalid-input');
13
13
  return exportItem(store, options.itemId, options.output);
14
14
  }
15
15
  if (action === 'backup') {
16
16
  if (!options.output)
17
- throw new StoreError('invalid-input');
17
+ throw new SpongeError('invalid-input');
18
18
  return backupLibrary(store, options.output);
19
19
  }
20
20
  if (action !== 'gc' || Boolean(options.dryRun) === Boolean(options.report))
21
- throw new StoreError('invalid-input');
21
+ throw new SpongeError('invalid-input');
22
+ // A backup that crashed must not fence collection forever: expire fences whose holder is provably dead.
23
+ await store.backups.expireFences();
22
24
  if (options.dryRun) {
23
25
  const report = createGcReport(store), path = join(store.root, 'runtime', `gc-${randomUUID()}.json`);
24
26
  writePrivateExclusive(path, JSON.stringify(report) + '\n');
25
- return { report: path, candidates: report.candidates.length, bytes: report.candidates.reduce((n, c) => n + c.bytes, 0) };
27
+ return {
28
+ report: path,
29
+ candidates: report.candidates.length,
30
+ bytes: report.candidates.reduce((n, c) => n + c.bytes, 0),
31
+ };
26
32
  }
27
33
  const fd = openRead(options.report);
28
34
  try {
29
35
  const bytes = fstatSync(fd).size;
30
36
  if (bytes > 32 * 1024 ** 2)
31
- throw new StoreError('limit-exceeded');
37
+ throw new SpongeError('limit-exceeded');
32
38
  return applyGcReport(store, JSON.parse(readExact(fd, bytes).toString('utf8')));
33
39
  }
34
40
  finally {
@@ -36,4 +42,3 @@ export async function portabilityAction(store, action, options) {
36
42
  }
37
43
  }
38
44
  // Restore must dispatch before opening the normal library; it never targets the default root.
39
- export const restoreAction = restoreLibrary;
@@ -1,23 +1,27 @@
1
- import { StoreError } from '../store/index.js';
2
1
  import { recoverWorker } from '../worker/custody.js';
2
+ import { SpongeError } from '../errors.js';
3
3
  export async function queueAction(store, action, jobId) {
4
4
  if (action === 'status') {
5
5
  if (jobId)
6
- return { jobs: store.listJobs().filter(j => j.id === jobId) };
7
- return { jobs: store.listJobs(), owner: store.ownerSnapshot(), pendingCleanup: store.pendingCleanupCount() };
6
+ return { jobs: store.queue.list().filter((j) => j.id === jobId) };
7
+ return {
8
+ jobs: store.queue.list(),
9
+ owner: store.queue.ownerSnapshot(),
10
+ pendingCleanup: store.queue.pendingCleanup(),
11
+ };
8
12
  }
9
13
  if (action === 'recover') {
10
14
  if (jobId)
11
- throw new StoreError('invalid-input');
15
+ throw new SpongeError('invalid-input');
12
16
  return await recoverWorker(store);
13
17
  }
14
18
  if (!jobId)
15
- throw new StoreError('invalid-input');
19
+ throw new SpongeError('invalid-input');
16
20
  if (action === 'cancel')
17
- store.requestCancellation(jobId);
21
+ store.queue.cancel(jobId);
18
22
  else if (action === 'retry')
19
- store.retryJob(jobId, { automatic: false });
23
+ store.queue.retry(jobId, { automatic: false });
20
24
  else
21
- throw new StoreError('invalid-input');
25
+ throw new SpongeError('invalid-input');
22
26
  return { jobId, action };
23
27
  }
@@ -1,7 +1,5 @@
1
1
  import type { LibraryStore } from '../store/index.js';
2
- import { type RuntimeGcOptions, type RuntimeGcReport } from '../worker/runtime-gc.js';
3
- /** References of this installed Sponge: its capture pin and its catalog's model packages. */
4
- export declare function installedReferences(version: string): Promise<Omit<RuntimeGcOptions, 'dryRun'>>;
2
+ import { type RuntimeGcReport } from '../worker/runtime-gc.js';
5
3
  export declare function runtimeGcAction(store: LibraryStore, version: string, dryRun: boolean): Promise<RuntimeGcReport>;
6
4
  export declare function runtimeGcText(report: RuntimeGcReport): string;
7
5
  /** Setup collection follows a verified install and never fails that install. */
@@ -2,19 +2,26 @@ import { defaultCapturePins } from '../acquisition/index.js';
2
2
  import { loadNativeCatalog, nativeModelPackage } from '../models/catalog.js';
3
3
  import { modelSetupPlan } from '../models/artifacts.js';
4
4
  import { collectRuntimes } from '../worker/runtime-gc.js';
5
+ import { codeOf } from '../errors.js';
5
6
  /** References of this installed Sponge: its capture pin and its catalog's model packages. */
6
- export async function installedReferences(version) {
7
+ async function installedReferences(version) {
7
8
  let models = null;
8
9
  try {
9
10
  const catalog = await loadNativeCatalog(version);
10
- models = { fingerprints: ['asr', 'embedding'].map(kind => modelSetupPlan(nativeModelPackage(catalog, kind)).fingerprint) };
11
+ models = {
12
+ fingerprints: ['asr', 'embedding'].map((kind) => modelSetupPlan(nativeModelPackage(catalog, kind)).fingerprint),
13
+ };
14
+ }
15
+ catch {
16
+ /* Without the catalog no model directory is provably superseded. */
11
17
  }
12
- catch { /* Without the catalog no model directory is provably superseded. */ }
13
18
  return { captureIds: [defaultCapturePins.id], models };
14
19
  }
15
- export async function runtimeGcAction(store, version, dryRun) { return collectRuntimes(store, { ...await installedReferences(version), dryRun }); }
20
+ export async function runtimeGcAction(store, version, dryRun) {
21
+ return collectRuntimes(store, { ...(await installedReferences(version)), dryRun });
22
+ }
16
23
  export function runtimeGcText(report) {
17
- const lines = report.removed.map(e => `${report.dryRun ? 'would remove' : 'removed'}\t${e.kind}\t${e.name}\t${e.bytes}`);
24
+ const lines = report.removed.map((e) => `${report.dryRun ? 'would remove' : 'removed'}\t${e.kind}\t${e.name}\t${e.bytes}`);
18
25
  for (const e of report.kept)
19
26
  if (e.reason !== 'referenced')
20
27
  lines.push(`kept\t${e.kind}\t${e.name}\t${e.reason}`);
@@ -27,6 +34,6 @@ export async function setupRuntimeGc(store, version) {
27
34
  return await runtimeGcAction(store, version, false);
28
35
  }
29
36
  catch (error) {
30
- return { skipped: String(error?.code ?? 'unclassified') };
37
+ return { skipped: codeOf(error) };
31
38
  }
32
39
  }
@@ -1,4 +1,4 @@
1
1
  /** Neutralize terminal commands without changing redirected Markdown or stored content. */
2
2
  export function terminalSafe(value) {
3
- return value.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
3
+ return value.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
4
4
  }
@@ -4,6 +4,13 @@ import { type ServiceEnvironment } from '../service/launch-agent.js';
4
4
  export declare function workerAction(store: LibraryStore, action: 'install' | 'status' | 'uninstall', version: string, environment?: ServiceEnvironment, options?: {
5
5
  language?: 'en';
6
6
  }): Promise<Record<string, unknown>>;
7
+ /** Doctor's read-only view of the background worker: never installs, repairs or signals anything. */
8
+ export declare function workerDoctor(store: LibraryStore, version: string, environment?: ServiceEnvironment): Promise<{
9
+ worker: string;
10
+ drift?: unknown;
11
+ repair?: unknown;
12
+ lastExit?: unknown;
13
+ }>;
7
14
  type DrainOptions = Parameters<typeof drainLibrary>[1];
8
15
  export type ServiceDrainOptions = Omit<DrainOptions, 'runtime'> & {
9
16
  runtime?: DrainOptions['runtime'];