@hraness/sponge 0.1.0 → 0.2.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 (174) hide show
  1. package/README.md +163 -17
  2. package/bin/sponge.js +9 -2
  3. package/dist/contracts/index.d.ts +164 -0
  4. package/dist/contracts/index.js +6 -0
  5. package/dist/contracts/validate.d.ts +14 -0
  6. package/dist/contracts/validate.js +187 -0
  7. package/dist/migrations/001.d.ts +3 -0
  8. package/dist/migrations/001.js +33 -0
  9. package/dist/migrations/002.d.ts +3 -0
  10. package/dist/migrations/002.js +6 -0
  11. package/dist/migrations/003.d.ts +3 -0
  12. package/dist/migrations/003.js +10 -0
  13. package/dist/migrations/004.d.ts +3 -0
  14. package/dist/migrations/004.js +9 -0
  15. package/dist/src/acquisition/capture.d.ts +29 -0
  16. package/dist/src/acquisition/capture.js +160 -0
  17. package/dist/src/acquisition/cleanup.d.ts +6 -0
  18. package/dist/src/acquisition/cleanup.js +20 -0
  19. package/dist/src/acquisition/errors.d.ts +3 -0
  20. package/dist/src/acquisition/errors.js +20 -0
  21. package/dist/src/acquisition/files.d.ts +4 -0
  22. package/dist/src/acquisition/files.js +49 -0
  23. package/dist/src/acquisition/index.d.ts +6 -0
  24. package/dist/src/acquisition/index.js +4 -0
  25. package/dist/src/acquisition/lock.d.ts +1 -0
  26. package/dist/src/acquisition/lock.js +2 -0
  27. package/dist/src/acquisition/normalize.d.ts +11 -0
  28. package/dist/src/acquisition/normalize.js +62 -0
  29. package/dist/src/acquisition/pins.d.ts +3 -0
  30. package/dist/src/acquisition/pins.js +9 -0
  31. package/dist/src/acquisition/process.d.ts +16 -0
  32. package/dist/src/acquisition/process.js +63 -0
  33. package/dist/src/acquisition/runtime.d.ts +53 -0
  34. package/dist/src/acquisition/runtime.js +216 -0
  35. package/dist/src/acquisition/verify-helper.d.ts +1 -0
  36. package/dist/src/acquisition/verify-helper.js +19 -0
  37. package/dist/src/app/article.d.ts +23 -0
  38. package/dist/src/app/article.js +82 -0
  39. package/dist/src/app/bundle-files.d.ts +8 -0
  40. package/dist/src/app/bundle-files.js +66 -0
  41. package/dist/src/app/markdown.d.ts +6 -0
  42. package/dist/src/app/markdown.js +90 -0
  43. package/dist/src/app/source.d.ts +15 -0
  44. package/dist/src/app/source.js +40 -0
  45. package/dist/src/cli/arguments.d.ts +9 -0
  46. package/dist/src/cli/arguments.js +106 -0
  47. package/dist/src/cli/config.d.ts +8 -0
  48. package/dist/src/cli/config.js +45 -0
  49. package/dist/src/cli/import.d.ts +4 -0
  50. package/dist/src/cli/import.js +3 -0
  51. package/dist/src/cli/indexing.d.ts +7 -0
  52. package/dist/src/cli/indexing.js +21 -0
  53. package/dist/src/cli/main.d.ts +1 -0
  54. package/dist/src/cli/main.js +333 -0
  55. package/dist/src/cli/portability.d.ts +9 -0
  56. package/dist/src/cli/portability.js +39 -0
  57. package/dist/src/cli/queue.d.ts +2 -0
  58. package/dist/src/cli/queue.js +23 -0
  59. package/dist/src/cli/terminal.d.ts +2 -0
  60. package/dist/src/cli/terminal.js +4 -0
  61. package/dist/src/cli/worker.d.ts +16 -0
  62. package/dist/src/cli/worker.js +62 -0
  63. package/dist/src/importers/apply.d.ts +29 -0
  64. package/dist/src/importers/apply.js +85 -0
  65. package/dist/src/importers/custody.d.ts +6 -0
  66. package/dist/src/importers/custody.js +35 -0
  67. package/dist/src/importers/inspect.d.ts +21 -0
  68. package/dist/src/importers/inspect.js +97 -0
  69. package/dist/src/importers/manifest.d.ts +22 -0
  70. package/dist/src/importers/manifest.js +43 -0
  71. package/dist/src/importers/verify-helper.d.ts +1 -0
  72. package/dist/src/importers/verify-helper.js +27 -0
  73. package/dist/src/indexing/image.d.ts +10 -0
  74. package/dist/src/indexing/image.js +34 -0
  75. package/dist/src/indexing/index.d.ts +53 -0
  76. package/dist/src/indexing/index.js +123 -0
  77. package/dist/src/indexing/jobs.d.ts +14 -0
  78. package/dist/src/indexing/jobs.js +49 -0
  79. package/dist/src/indexing/runner.d.ts +23 -0
  80. package/dist/src/indexing/runner.js +114 -0
  81. package/dist/src/inference/asr/bridge.d.ts +1 -0
  82. package/dist/src/inference/asr/bridge.js +16 -0
  83. package/dist/src/inference/asr/protocol.d.ts +23 -0
  84. package/dist/src/inference/asr/protocol.js +60 -0
  85. package/dist/src/inference/asr/runtime.d.ts +29 -0
  86. package/dist/src/inference/asr/runtime.js +103 -0
  87. package/dist/src/inference/asr/windows.d.ts +9 -0
  88. package/dist/src/inference/asr/windows.js +41 -0
  89. package/dist/src/inference/embedding/index.d.ts +47 -0
  90. package/dist/src/inference/embedding/index.js +122 -0
  91. package/dist/src/media/capture-helper.d.ts +1 -0
  92. package/dist/src/media/capture-helper.js +16 -0
  93. package/dist/src/media/capture.d.ts +32 -0
  94. package/dist/src/media/capture.js +193 -0
  95. package/dist/src/media/handler.d.ts +22 -0
  96. package/dist/src/media/handler.js +82 -0
  97. package/dist/src/media/transcribe.d.ts +29 -0
  98. package/dist/src/media/transcribe.js +93 -0
  99. package/dist/src/metadata/article.d.ts +5 -0
  100. package/dist/src/metadata/article.js +51 -0
  101. package/dist/src/models/artifacts.d.ts +36 -0
  102. package/dist/src/models/artifacts.js +274 -0
  103. package/dist/src/models/catalog.d.ts +35 -0
  104. package/dist/src/models/catalog.js +71 -0
  105. package/dist/src/models/download.d.ts +9 -0
  106. package/dist/src/models/download.js +147 -0
  107. package/dist/src/models/runtime.d.ts +24 -0
  108. package/dist/src/models/runtime.js +37 -0
  109. package/dist/src/pdf/capture-helper.d.ts +1 -0
  110. package/dist/src/pdf/capture-helper.js +12 -0
  111. package/dist/src/pdf/capture.d.ts +29 -0
  112. package/dist/src/pdf/capture.js +156 -0
  113. package/dist/src/pdf/errors.d.ts +2 -0
  114. package/dist/src/pdf/errors.js +13 -0
  115. package/dist/src/pdf/handler.d.ts +24 -0
  116. package/dist/src/pdf/handler.js +84 -0
  117. package/dist/src/pdf/index.d.ts +4 -0
  118. package/dist/src/pdf/index.js +4 -0
  119. package/dist/src/pdf/input.d.ts +2 -0
  120. package/dist/src/pdf/input.js +46 -0
  121. package/dist/src/pdf/normalize.d.ts +10 -0
  122. package/dist/src/pdf/normalize.js +64 -0
  123. package/dist/src/pdf/source-identity.d.ts +4 -0
  124. package/dist/src/pdf/source-identity.js +12 -0
  125. package/dist/src/pdf/source.d.ts +6 -0
  126. package/dist/src/pdf/source.js +22 -0
  127. package/dist/src/pdf/verify-helper.d.ts +1 -0
  128. package/dist/src/pdf/verify-helper.js +17 -0
  129. package/dist/src/portability/backup.d.ts +23 -0
  130. package/dist/src/portability/backup.js +138 -0
  131. package/dist/src/portability/database.d.ts +14 -0
  132. package/dist/src/portability/database.js +78 -0
  133. package/dist/src/portability/export.d.ts +8 -0
  134. package/dist/src/portability/export.js +36 -0
  135. package/dist/src/portability/files.d.ts +33 -0
  136. package/dist/src/portability/files.js +78 -0
  137. package/dist/src/portability/gc.d.ts +21 -0
  138. package/dist/src/portability/gc.js +66 -0
  139. package/dist/src/search/extension.d.ts +3 -0
  140. package/dist/src/search/extension.js +17 -0
  141. package/dist/src/search/hybrid.d.ts +39 -0
  142. package/dist/src/search/hybrid.js +118 -0
  143. package/dist/src/search/query.d.ts +12 -0
  144. package/dist/src/search/query.js +47 -0
  145. package/dist/src/search/vector.d.ts +21 -0
  146. package/dist/src/search/vector.js +21 -0
  147. package/dist/src/service/launch-agent.d.ts +47 -0
  148. package/dist/src/service/launch-agent.js +179 -0
  149. package/dist/src/service/lifetime.d.ts +7 -0
  150. package/dist/src/service/lifetime.js +73 -0
  151. package/dist/src/service/loop.d.ts +21 -0
  152. package/dist/src/service/loop.js +94 -0
  153. package/dist/src/store/files.d.ts +12 -0
  154. package/dist/src/store/files.js +101 -0
  155. package/dist/src/store/index.d.ts +118 -0
  156. package/dist/src/store/index.js +727 -0
  157. package/dist/src/store/types.d.ts +90 -0
  158. package/dist/src/store/types.js +8 -0
  159. package/dist/src/worker/basic.d.ts +10 -0
  160. package/dist/src/worker/basic.js +27 -0
  161. package/dist/src/worker/custody.d.ts +13 -0
  162. package/dist/src/worker/custody.js +185 -0
  163. package/dist/src/worker/engine.d.ts +28 -0
  164. package/dist/src/worker/engine.js +78 -0
  165. package/dist/src/worker/identity.d.ts +7 -0
  166. package/dist/src/worker/identity.js +27 -0
  167. package/dist/src/worker/journal.d.ts +5 -0
  168. package/dist/src/worker/journal.js +30 -0
  169. package/dist/src/worker/supervisor-child.d.ts +1 -0
  170. package/dist/src/worker/supervisor-child.js +33 -0
  171. package/dist/src/worker/supervisor.d.ts +3 -0
  172. package/dist/src/worker/supervisor.js +47 -0
  173. package/native/runtime-manifest.json +40 -0
  174. package/package.json +36 -6
@@ -0,0 +1,106 @@
1
+ import { isAbsolute } from 'node:path';
2
+ import { InputError } from '../app/source.js';
3
+ const specifications = {
4
+ init: { count: 0, flags: {} },
5
+ add: { count: 1, flags: { connection: 'value', kind: 'value', wait: 'boolean', language: 'value', 'force-local': 'boolean', 'allow-private-network': 'boolean' } },
6
+ list: { count: 0, flags: {} },
7
+ asset: { count: 2, flags: {} },
8
+ show: { count: 1, flags: { format: 'value' } },
9
+ delete: { count: 1, flags: {} },
10
+ refresh: { count: 1, flags: { wait: 'boolean', language: 'value', 'force-local': 'boolean', 'allow-private-network': 'boolean' } },
11
+ search: { count: 1, flags: { mode: 'value', limit: 'value', profile: 'value' } },
12
+ run: { count: 0, flags: { drain: 'boolean', language: 'value', 'force-local': 'boolean', 'allow-private-network': 'boolean' } },
13
+ setup: { count: 0, flags: { capture: 'boolean', asr: 'boolean', embedding: 'boolean' } },
14
+ doctor: { count: 0, flags: {} },
15
+ queue: { count: -1, flags: {} },
16
+ export: { count: 1, flags: { output: 'value' } },
17
+ backup: { count: 0, flags: { output: 'value' } },
18
+ restore: { count: 1, flags: { into: 'value' } },
19
+ gc: { count: 0, flags: { 'dry-run': 'boolean', apply: 'value' } },
20
+ worker: { count: 1, flags: { language: 'value', 'force-local': 'boolean' } },
21
+ index: { count: -1, flags: { profile: 'value' } },
22
+ import: { count: 1, flags: { apply: 'boolean', 'dry-run': 'boolean', limit: 'value', offset: 'value' } },
23
+ };
24
+ /** No shell grammar, implicit option coercion, duplicate flags, or unbounded argv. */
25
+ export function parseArguments(argv) {
26
+ if (argv.length > 32 || argv.some(arg => Buffer.byteLength(arg) > 8192 || /[\u0000-\u001f\u007f]/u.test(arg)))
27
+ throw new InputError();
28
+ if (argv.length === 0 || argv.length === 1 && ['--help', '-h'].includes(argv[0]))
29
+ return { command: 'help', positional: [], flags: {}, json: false };
30
+ if (argv.length === 1 && argv[0] === '--version')
31
+ return { command: 'version', positional: [], flags: {}, json: false };
32
+ // Global flags are valid before or after the command.
33
+ let command;
34
+ const flags = Object.create(null);
35
+ const positional = [];
36
+ const allFlags = { library: 'value', json: 'boolean' };
37
+ for (const spec of Object.values(specifications))
38
+ Object.assign(allFlags, spec.flags);
39
+ let literal = false;
40
+ for (let i = 0; i < argv.length; i++) {
41
+ const arg = argv[i];
42
+ if (arg === '--' && !literal) {
43
+ literal = true;
44
+ continue;
45
+ }
46
+ if (arg.startsWith('--') && !literal) {
47
+ const key = arg.slice(2);
48
+ const kind = (command && specifications[command]?.flags[key]) ?? allFlags[key];
49
+ if (!kind || Object.hasOwn(flags, key))
50
+ throw new InputError();
51
+ if (kind === 'boolean')
52
+ flags[key] = true;
53
+ else {
54
+ const value = argv[++i];
55
+ if (!value || value.startsWith('--'))
56
+ throw new InputError();
57
+ flags[key] = value;
58
+ }
59
+ }
60
+ else if (!command)
61
+ command = arg;
62
+ else
63
+ positional.push(arg);
64
+ }
65
+ const spec = command && specifications[command];
66
+ if (!command || !spec || (command === 'queue' ? !(['status', 'recover'].includes(positional[0]) && positional.length === 1 || ['cancel', 'retry'].includes(positional[0]) && positional.length === 2) : command === 'index' ? !(['create', 'run'].includes(positional[0]) && positional.length === 1 || ['status', 'rollback'].includes(positional[0]) && positional.length === 2) : positional.length !== spec.count))
67
+ throw new InputError();
68
+ if (Object.keys(flags).some(key => !['library', 'json'].includes(key) && !Object.hasOwn(spec.flags, key)))
69
+ throw new InputError();
70
+ const library = flags.library;
71
+ if (library !== undefined && (typeof library !== 'string' || !isAbsolute(library)))
72
+ throw new InputError();
73
+ if (flags.kind && !['article', 'pdf', 'media'].includes(String(flags.kind)))
74
+ throw new InputError();
75
+ if (flags.format && !['markdown', 'json'].includes(String(flags.format)))
76
+ throw new InputError();
77
+ if (flags.mode && !['lexical', 'hybrid'].includes(String(flags.mode)))
78
+ throw new InputError();
79
+ if (flags.limit && (!/^[1-9]\d{0,2}$/u.test(String(flags.limit)) || Number(flags.limit) > 100))
80
+ throw new InputError();
81
+ if (command === 'run' && !flags.drain || command === 'setup' && [flags.capture, flags.asr, flags.embedding].filter(Boolean).length !== 1)
82
+ throw new InputError();
83
+ if (flags['allow-private-network'] && command !== 'run' && !flags.wait)
84
+ throw new InputError();
85
+ if (['export', 'backup'].includes(command) && (typeof flags.output !== 'string' || !isAbsolute(flags.output)))
86
+ throw new InputError();
87
+ if (command === 'restore' && (typeof flags.into !== 'string' || !isAbsolute(flags.into) || flags.library !== undefined || !isAbsolute(positional[0])))
88
+ throw new InputError();
89
+ if (command === 'gc' && (Boolean(flags['dry-run']) === Boolean(flags.apply) || flags.apply !== undefined && !isAbsolute(String(flags.apply))))
90
+ throw new InputError();
91
+ if (command === 'worker' && !['install', 'status', 'uninstall', 'serve'].includes(positional[0]))
92
+ throw new InputError();
93
+ if (command === 'import' && (!isAbsolute(positional[0]) || flags.apply && flags['dry-run'] || flags.offset !== undefined && !/^(0|[1-9][0-9]{0,5})$/.test(String(flags.offset))))
94
+ throw new InputError();
95
+ if (flags.language !== undefined && flags.language !== 'en')
96
+ throw new InputError();
97
+ if (flags['force-local'] && command === 'worker' && positional[0] === 'install')
98
+ throw new InputError();
99
+ if (flags['force-local'] && !flags.language)
100
+ throw new InputError();
101
+ if ((flags.language || flags['force-local']) && (['add', 'refresh'].includes(command) && !flags.wait || command === 'worker' && !['serve', 'install'].includes(positional[0])))
102
+ throw new InputError();
103
+ if (flags.profile && (!(command === 'search' || command === 'index' && positional[0] === 'create') || !/^[a-zA-Z0-9_-]{1,128}$/.test(String(flags.profile))))
104
+ throw new InputError();
105
+ return { command, positional, flags, library: library, json: flags.json === true || flags.format === 'json' };
106
+ }
@@ -0,0 +1,8 @@
1
+ import type { CaptureConnection } from '../acquisition/index.js';
2
+ export interface Connection {
3
+ id: string;
4
+ realm: string;
5
+ capture: CaptureConnection;
6
+ }
7
+ /** Local references only. Tokens, cookies and passwords never belong in this file. */
8
+ export declare function readConnections(root: string): Promise<Connection[]>;
@@ -0,0 +1,45 @@
1
+ import { lstat } from 'node:fs/promises';
2
+ import { readConfined } from '../acquisition/files.js';
3
+ import { join } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { InputError } from '../app/source.js';
6
+ /** Local references only. Tokens, cookies and passwords never belong in this file. */
7
+ export async function readConnections(root) {
8
+ const path = join(root, 'connections.json');
9
+ let bytes;
10
+ try {
11
+ const stat = await lstat(path);
12
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 65536 || (stat.mode & 0o077) !== 0)
13
+ throw new InputError();
14
+ bytes = await readConfined(root, 'connections.json', 65536);
15
+ }
16
+ catch (error) {
17
+ if (error.code === 'ENOENT')
18
+ return [];
19
+ throw error;
20
+ }
21
+ let config;
22
+ try {
23
+ config = JSON.parse(bytes.toString('utf8'));
24
+ }
25
+ catch {
26
+ throw new InputError();
27
+ }
28
+ if (config?.schemaVersion !== 1 || !Array.isArray(config.connections) || config.connections.length > 100)
29
+ throw new InputError();
30
+ const ids = new Set();
31
+ return config.connections.map((entry) => {
32
+ if (!entry || typeof entry.id !== 'string' || !/^[a-zA-Z0-9_-]{1,64}$/.test(entry.id) || ids.has(entry.id))
33
+ throw new InputError();
34
+ ids.add(entry.id);
35
+ let capture;
36
+ if (entry.kind === 'account' && typeof entry.stateHome === 'string' && entry.stateHome.startsWith('/') && typeof entry.accountId === 'string')
37
+ capture = { kind: 'account', stateHome: entry.stateHome, accountId: entry.accountId };
38
+ else if (entry.kind === 'cdp' && typeof entry.endpoint === 'string')
39
+ capture = { kind: 'cdp', endpoint: entry.endpoint };
40
+ else
41
+ throw new InputError();
42
+ const realm = 'connection:' + createHash('sha256').update(JSON.stringify([entry.id, capture])).digest('hex');
43
+ return { id: entry.id, realm, capture };
44
+ });
45
+ }
@@ -0,0 +1,4 @@
1
+ import type { LibraryStore } from '../store/index.js';
2
+ import { type ImportOptions, type ImportReport } from '../importers/apply.js';
3
+ /** A source manifest is explicit; default execution produces a private dry-run report. */
4
+ export declare function importAction(store: LibraryStore, path: string, options: ImportOptions): Promise<ImportReport>;
@@ -0,0 +1,3 @@
1
+ import { importManifest } from '../importers/apply.js';
2
+ /** A source manifest is explicit; default execution produces a private dry-run report. */
3
+ export function importAction(store, path, options) { return importManifest(store, path, options); }
@@ -0,0 +1,7 @@
1
+ import type { LibraryStore } from '../store/index.js';
2
+ import { type EmbeddingRuntime } from '../inference/embedding/index.js';
3
+ export declare function indexAction(store: LibraryStore, action: string, id: string | undefined, options?: {
4
+ runtime?: EmbeddingRuntime;
5
+ profile?: string;
6
+ signal?: AbortSignal;
7
+ }): Promise<Record<string, unknown>>;
@@ -0,0 +1,21 @@
1
+ import { embeddingIdentity, embeddingFingerprint } from '../inference/embedding/index.js';
2
+ import { drainIndexQueue } from '../indexing/runner.js';
3
+ import { StoreError } from '../store/index.js';
4
+ import { NativeCatalogError } from '../models/catalog.js';
5
+ export async function indexAction(store, action, id, options = {}) {
6
+ if (action === 'status')
7
+ return { generation: store.indexStatus(id) };
8
+ if (action === 'rollback') {
9
+ const generation = store.indexStatus(id);
10
+ if (!['active', 'retired'].includes(generation.state))
11
+ throw new StoreError('invalid-input');
12
+ return { generation: store.activateIndex(id) };
13
+ }
14
+ if (!options.runtime)
15
+ throw new NativeCatalogError('runtime-missing');
16
+ if (action === 'create') {
17
+ const runtime = options.runtime, generation = store.createIndexGeneration({ profileId: options.profile ?? 'default', fingerprint: embeddingFingerprint(runtime.helperSha256), identity: { ...embeddingIdentity, helperSha256: runtime.helperSha256 }, dimensions: embeddingIdentity.dimensions });
18
+ return { generation, job: store.submitIndexJob(generation.id) };
19
+ }
20
+ return { results: await drainIndexQueue(store, { runtime: options.runtime, signal: options.signal }) };
21
+ }
@@ -0,0 +1 @@
1
+ export declare function main(argv: string[], version: string): Promise<number>;
@@ -0,0 +1,333 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { lstat } from 'node:fs/promises';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { terminalSafe } from './terminal.js';
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 { createQueryEmbedder } from '../search/query.js';
19
+ import { indexAction } from './indexing.js';
20
+ import { importAction } from './import.js';
21
+ import { drainLibrary } from '../worker/basic.js';
22
+ import { failure } from '../acquisition/errors.js';
23
+ const help = `sponge — a local article library
24
+
25
+ init Create a private library
26
+ setup --capture|--asr|--embedding Download one pinned runtime
27
+ add URL_OR_PDF [--kind article|pdf|media] [--connection ID] [--wait]
28
+ run --drain Process queued articles sequentially
29
+ queue status Show jobs and their states
30
+ queue cancel JOB Cancel a queued or active job
31
+ queue retry JOB Retry a failed job after resolving its cause
32
+ queue recover Reconcile a stopped worker after cleanup
33
+ list List saved and queued items
34
+ show ITEM [--format markdown|json]
35
+ asset ITEM SHA Write a verified asset to stdout
36
+ search QUERY [--mode hybrid|lexical] [--profile default] [--limit 20]
37
+ refresh ITEM [--wait] Capture a new revision
38
+ delete ITEM Hide an item and remove it from search
39
+ index create [--profile default] Queue a new embedding generation
40
+ index status GENERATION Inspect generation coverage
41
+ index run Drain queued embedding jobs
42
+ index rollback GENERATION Reactivate a compatible complete generation
43
+ doctor Check library and runtime states
44
+ export ITEM --output DIR Export Markdown and local assets
45
+ backup --output FILE Save a verified offline backup
46
+ restore FILE --into NEW_DIR Restore into a new private library
47
+ gc --dry-run Write a report of unreferenced blobs
48
+ gc --apply REPORT Recheck and remove reported blobs
49
+ worker install [--language en] Enable the optional background worker
50
+ worker status Inspect the background worker
51
+ worker uninstall Stop it and preserve the library
52
+ import MANIFEST [--apply] Preview or import existing Wordcell captures
53
+
54
+ Global flags: --library /absolute/path --json
55
+ Use -- before an option-like search query. Capture setup is explicit.
56
+ Media: --language en enables local English ASR; --force-local bypasses captions.
57
+ These flags apply to --wait, run --drain, or worker serve.
58
+ worker install --language en saves the English ASR choice for background work.
59
+ For trusted local sources, use --allow-private-network with --wait or run.
60
+ `;
61
+ 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']);
62
+ function errorCode(error, fallback) { const code = error?.code; return code && codes.has(code) ? code : fallback; }
63
+ 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; }
64
+ export async function main(argv, version) {
65
+ const requestId = randomUUID();
66
+ let store;
67
+ const controller = new AbortController();
68
+ const cancel = () => controller.abort();
69
+ process.on('SIGINT', cancel);
70
+ process.on('SIGTERM', cancel);
71
+ try {
72
+ 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); };
73
+ if (args.command === 'help') {
74
+ process.stdout.write(help);
75
+ return 0;
76
+ }
77
+ if (args.command === 'version') {
78
+ process.stdout.write(version + '\n');
79
+ return 0;
80
+ }
81
+ if (args.command === 'restore') {
82
+ emit(await restoreAction(args.positional[0], String(args.flags.into)));
83
+ return 0;
84
+ }
85
+ const root = args.library ?? join(homedir(), '.local', 'share', 'sponge');
86
+ if (args.command !== 'init') {
87
+ try {
88
+ await lstat(join(root, '.sponge-library'));
89
+ }
90
+ catch (error) {
91
+ if (error.code === 'ENOENT')
92
+ throw new StoreError('unavailable');
93
+ throw error;
94
+ }
95
+ }
96
+ store = await openLibrary(root);
97
+ if (args.command === 'init') {
98
+ emit({ library: store.root }, `Library: ${store.root}`);
99
+ return 0;
100
+ }
101
+ if (args.command === 'worker' && args.positional[0] !== 'serve') {
102
+ emit(await workerAction(store, args.positional[0], version, undefined, { language: args.flags.language }));
103
+ return 0;
104
+ }
105
+ if (args.command === 'export' || args.command === 'backup' || args.command === 'gc') {
106
+ emit(await portabilityAction(store, args.command, { itemId: args.positional[0], output: args.flags.output, dryRun: args.flags['dry-run'] === true, report: args.flags.apply }));
107
+ return 0;
108
+ }
109
+ const runtimeRoot = join(store.root, 'runtime', 'capture', defaultCapturePins.id);
110
+ const nativeRoot = join(store.root, 'runtime', 'models');
111
+ if (args.command === 'setup' && !args.flags.capture) {
112
+ const kind = args.flags.asr ? 'asr' : 'embedding', plan = await nativeSetupPlan(kind, version);
113
+ process.stderr.write(JSON.stringify({ setup: kind, ...plan }) + '\n');
114
+ try {
115
+ await provisionNative(kind, nativeRoot, version, controller.signal);
116
+ }
117
+ catch (error) {
118
+ if (error.code === 'repair-auth' && !args.json)
119
+ process.stderr.write(nativeDownloadAuthGuidance + '\n');
120
+ throw error;
121
+ }
122
+ emit({ [kind]: 'ready' });
123
+ return 0;
124
+ }
125
+ if (args.command === 'setup') {
126
+ const plan = captureSetupPlan(defaultCapturePins);
127
+ 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. ${plan.requiredFreeBytes} free bytes required.\n`);
128
+ await provisionCapture({ runtimeRoot: join(store.root, 'runtime', 'capture'), pins: defaultCapturePins, signal: controller.signal });
129
+ emit({ capture: 'ready' }, 'Capture ready');
130
+ return 0;
131
+ }
132
+ if (args.command === 'list') {
133
+ const items = store.list();
134
+ emit({ items }, items.map(item => `${item.itemId}\t${item.title}`).join('\n'));
135
+ return 0;
136
+ }
137
+ if (args.command === 'show') {
138
+ const item = store.read(args.positional[0]);
139
+ if (!item)
140
+ throw new StoreError('unavailable');
141
+ emit({ item }, item.markdown);
142
+ return item.status === 'partial' ? 3 : 0;
143
+ }
144
+ if (args.command === 'asset') {
145
+ const item = store.read(args.positional[0]), asset = item?.assets.find(a => a.sha256 === args.positional[1]);
146
+ if (!asset)
147
+ throw new StoreError('unavailable');
148
+ const bytes = store.readBlob(asset);
149
+ if (args.json)
150
+ emit({ asset });
151
+ else {
152
+ if (process.stdout.isTTY)
153
+ throw new InputError();
154
+ process.stdout.write(bytes);
155
+ }
156
+ return 0;
157
+ }
158
+ if (args.command === 'index') {
159
+ const action = args.positional[0], runtime = ['create', 'run'].includes(action) ? await inspectNative('embedding', nativeRoot, version) : undefined;
160
+ const result = await indexAction(store, action, args.positional[1], { runtime, profile: args.flags.profile, signal: controller.signal });
161
+ emit(result);
162
+ const failed = result.results?.find(r => !r.ok);
163
+ return failed ? exitFor(failed.code) : 0;
164
+ }
165
+ if (args.command === 'search') {
166
+ if (args.flags.mode === 'lexical') {
167
+ const items = store.search(args.positional[0], Number(args.flags.limit ?? 20));
168
+ emit({ items }, items.map(item => `${item.itemId}\t${item.title}`).join('\n'));
169
+ return 0;
170
+ }
171
+ let embedding;
172
+ try {
173
+ embedding = await inspectNative('embedding', nativeRoot, version);
174
+ }
175
+ catch { /* Hybrid explicitly reports semantic unavailability and preserves lexical results. */ }
176
+ 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) } : {}) });
177
+ 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'));
178
+ return controller.signal.aborted ? 130 : 0;
179
+ }
180
+ if (args.command === 'delete') {
181
+ store.delete(args.positional[0]);
182
+ emit({ itemId: args.positional[0], deleted: true }, 'Deleted');
183
+ return 0;
184
+ }
185
+ if (args.command === 'queue') {
186
+ const result = await queueAction(store, args.positional[0], args.positional[1]);
187
+ emit(result);
188
+ return 0;
189
+ }
190
+ if (args.command === 'doctor') {
191
+ let capture = 'ready';
192
+ try {
193
+ await inspectCaptureRuntime(runtimeRoot, defaultCapturePins);
194
+ }
195
+ catch (error) {
196
+ capture = errorCode(error, 'runtime-missing');
197
+ }
198
+ const states = {};
199
+ for (const kind of ['asr', 'embedding']) {
200
+ try {
201
+ await inspectNative(kind, nativeRoot, version);
202
+ states[kind] = 'ready';
203
+ }
204
+ catch (error) {
205
+ states[kind] = errorCode(error, 'runtime-missing');
206
+ }
207
+ }
208
+ const cleanup = store.pendingCleanupCount();
209
+ emit({ library: 'ready', capture, ...states, pendingCleanup: cleanup }, `Library: ready\nCapture: ${capture}\nASR: ${states.asr}\nEmbedding: ${states.embedding}\nPending cleanup: ${cleanup}`);
210
+ return cleanup || capture !== 'ready' ? 4 : 0;
211
+ }
212
+ if (args.command === 'run' && !hasDueAcquisition(store)) {
213
+ if (store.ownerSnapshot())
214
+ throw new StoreError('owner-busy');
215
+ if (store.pendingCleanupCount())
216
+ throw Object.assign(Error('cleanup-required'), { code: 'cleanup-required' });
217
+ if (!store.listJobs().some(job => job.operation === 'index' && job.state === 'queued')) {
218
+ emit({ results: [], indexResults: [] }, 'Queue empty');
219
+ return 0;
220
+ }
221
+ const runtime = await inspectNative('embedding', nativeRoot, version), indexResults = await drainIndexQueue(store, { runtime, signal: controller.signal });
222
+ emit({ results: [], indexResults });
223
+ const failed = indexResults.find(result => !result.ok);
224
+ return failed && !failed.ok ? exitFor(failed.code) : controller.signal.aborted ? 130 : 0;
225
+ }
226
+ const connections = await readConnections(store.root);
227
+ let jobId, itemId;
228
+ if (args.command === 'add') {
229
+ const connection = args.flags.connection ? connections.find(c => c.id === args.flags.connection) : undefined;
230
+ if (args.flags.connection && !connection)
231
+ throw new InputError();
232
+ const input = args.positional[0], kind = (args.flags.kind ?? (/\.pdf(?:[?#]|$)/i.test(input) || input.startsWith('file:') ? 'pdf' : 'article'));
233
+ const source = (kind === 'pdf' ? pdfSource : kind === 'media' ? mediaSource : articleSource)(input, connection?.realm ?? 'public');
234
+ const submission = store.submitSource({ kind, ...source, idempotencyKey: 'save:' + source.sourceKey });
235
+ jobId = submission.jobId;
236
+ itemId = submission.itemId;
237
+ const job = store.listJobs().find(j => j.id === jobId);
238
+ if (!args.flags.wait) {
239
+ emit({ ...submission, state: job?.state ?? 'queued' }, `${itemId}\t${job?.state ?? 'queued'}`);
240
+ return 0;
241
+ }
242
+ if (job?.state === 'succeeded') {
243
+ const item = store.read(itemId);
244
+ emit({ itemId, jobId, status: item.status }, `${itemId}\t${item.status}`);
245
+ return item.status === 'partial' ? 3 : 0;
246
+ }
247
+ }
248
+ else if (args.command === 'refresh') {
249
+ const previous = store.listJobs().find(j => j.item_id === args.positional[0]), source = previous && store.sourceForJob(String(previous.id));
250
+ if (!source || !['article', 'pdf', 'media'].includes(source.kind))
251
+ throw new StoreError('unavailable');
252
+ const submission = store.submitSource({ ...source, kind: source.kind, intent: 'refresh', idempotencyKey: randomUUID() });
253
+ jobId = submission.jobId;
254
+ itemId = submission.itemId;
255
+ if (!args.flags.wait) {
256
+ emit(submission, `${itemId}\tqueued`);
257
+ return 0;
258
+ }
259
+ }
260
+ if (jobId) {
261
+ const job = store.listJobs().find(j => j.id === jobId);
262
+ if (job && job.state !== 'queued') {
263
+ const code = job.state === 'cancelled' ? 'cancelled' : job.state === 'cleanup-pending' ? 'cleanup-required' : job.state === 'running' ? 'owner-busy' : errorCode({ code: job.reason }, 'unclassified');
264
+ throw Object.assign(Error(code), { code });
265
+ }
266
+ }
267
+ if (args.command === 'worker') {
268
+ 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')
269
+ return undefined; const connection = connections.find(c => c.realm === realm); if (!connection)
270
+ throw Object.assign(Error('repair-auth'), { code: 'repair-auth' }); return connection.capture; } }, version));
271
+ return 0;
272
+ }
273
+ let runtime;
274
+ try {
275
+ runtime = await inspectCaptureRuntime(runtimeRoot, defaultCapturePins);
276
+ }
277
+ catch (error) {
278
+ const code = errorCode(error, 'runtime-missing');
279
+ throw Object.assign(Error(code), { code });
280
+ }
281
+ if (args.command === 'import') {
282
+ 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 });
283
+ emit({ report });
284
+ return controller.signal.aborted ? 130 : report.entries.some(entry => entry.status === 'failed' || entry.status === 'unsupported') ? 3 : 0;
285
+ }
286
+ let asrRuntime;
287
+ try {
288
+ asrRuntime = await inspectNative('asr', nativeRoot, version);
289
+ }
290
+ catch (error) {
291
+ if (!['runtime-missing', 'unsupported'].includes(errorCode(error, 'integrity')))
292
+ throw error;
293
+ }
294
+ 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')
295
+ return undefined; const connection = connections.find(c => c.realm === realm); if (!connection)
296
+ throw Object.assign(Error('repair-auth'), { code: 'repair-auth' }); return connection.capture; } };
297
+ const results = await drainLibrary(store, workerOptions);
298
+ if (jobId && !results.length && !controller.signal.aborted) {
299
+ const job = store.listJobs().find(j => j.id === jobId);
300
+ const code = job?.state === 'cancelled' ? 'cancelled' : job?.state === 'cleanup-pending' ? 'cleanup-required' : job?.state === 'running' ? 'owner-busy' : errorCode({ code: job?.reason }, 'unclassified');
301
+ throw Object.assign(Error(code), { code });
302
+ }
303
+ let indexResults = [];
304
+ if (args.command === 'run' && !controller.signal.aborted && store.listJobs().some(job => job.operation === 'index' && job.state === 'queued')) {
305
+ const embedding = await inspectNative('embedding', nativeRoot, version);
306
+ indexResults = await drainIndexQueue(store, { runtime: embedding, signal: controller.signal });
307
+ const indexFailed = indexResults.find(result => !result.ok);
308
+ if (indexFailed && !indexFailed.ok) {
309
+ process.stderr.write(JSON.stringify(indexFailed) + '\n');
310
+ return exitFor(indexFailed.code);
311
+ }
312
+ }
313
+ const failed = results.find(result => !result.ok);
314
+ if (failed && !failed.ok) {
315
+ process.stderr.write(JSON.stringify(failed) + '\n');
316
+ return exitFor(failed.code);
317
+ }
318
+ if (controller.signal.aborted)
319
+ return 130;
320
+ emit({ results, indexResults }, results.map(result => result.ok ? `${result.itemId}\t${result.status}` : '').join('\n') || 'Queue empty');
321
+ return results.some(result => result.ok && result.status === 'partial') ? 3 : 0;
322
+ }
323
+ catch (error) {
324
+ const value = error?.code, code = value && codes.has(value) ? value : 'unclassified';
325
+ process.stderr.write(JSON.stringify(failure(requestId, code)) + '\n');
326
+ return exitFor(code);
327
+ }
328
+ finally {
329
+ store?.close();
330
+ process.off('SIGINT', cancel);
331
+ process.off('SIGTERM', cancel);
332
+ }
333
+ }
@@ -0,0 +1,9 @@
1
+ import type { LibraryStore } from '../store/index.js';
2
+ import { restoreLibrary } from '../portability/backup.js';
3
+ export declare function portabilityAction(store: LibraryStore, action: 'export' | 'backup' | 'gc', options: {
4
+ itemId?: string;
5
+ output?: string;
6
+ dryRun?: boolean;
7
+ report?: string;
8
+ }): Promise<Record<string, unknown>>;
9
+ export declare const restoreAction: typeof restoreLibrary;
@@ -0,0 +1,39 @@
1
+ import { fstatSync, closeSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { StoreError } from '../store/types.js';
5
+ import { openRead, readExact, writePrivateExclusive } from '../portability/files.js';
6
+ import { backupLibrary, restoreLibrary } from '../portability/backup.js';
7
+ import { exportItem } from '../portability/export.js';
8
+ import { createGcReport, applyGcReport } from '../portability/gc.js';
9
+ export async function portabilityAction(store, action, options) {
10
+ if (action === 'export') {
11
+ if (!options.itemId || !options.output)
12
+ throw new StoreError('invalid-input');
13
+ return exportItem(store, options.itemId, options.output);
14
+ }
15
+ if (action === 'backup') {
16
+ if (!options.output)
17
+ throw new StoreError('invalid-input');
18
+ return backupLibrary(store, options.output);
19
+ }
20
+ if (action !== 'gc' || Boolean(options.dryRun) === Boolean(options.report))
21
+ throw new StoreError('invalid-input');
22
+ if (options.dryRun) {
23
+ const report = createGcReport(store), path = join(store.root, 'runtime', `gc-${randomUUID()}.json`);
24
+ writePrivateExclusive(path, JSON.stringify(report) + '\n');
25
+ return { report: path, candidates: report.candidates.length, bytes: report.candidates.reduce((n, c) => n + c.bytes, 0) };
26
+ }
27
+ const fd = openRead(options.report);
28
+ try {
29
+ const bytes = fstatSync(fd).size;
30
+ if (bytes > 32 * 1024 ** 2)
31
+ throw new StoreError('limit-exceeded');
32
+ return applyGcReport(store, JSON.parse(readExact(fd, bytes).toString('utf8')));
33
+ }
34
+ finally {
35
+ closeSync(fd);
36
+ }
37
+ }
38
+ // Restore must dispatch before opening the normal library; it never targets the default root.
39
+ export const restoreAction = restoreLibrary;
@@ -0,0 +1,2 @@
1
+ import type { LibraryStore } from '../store/index.js';
2
+ export declare function queueAction(store: LibraryStore, action: 'status' | 'cancel' | 'retry' | 'recover', jobId?: string): Promise<Record<string, unknown>>;
@@ -0,0 +1,23 @@
1
+ import { StoreError } from '../store/index.js';
2
+ import { recoverWorker } from '../worker/custody.js';
3
+ export async function queueAction(store, action, jobId) {
4
+ if (action === 'status') {
5
+ if (jobId)
6
+ return { jobs: store.listJobs().filter(j => j.id === jobId) };
7
+ return { jobs: store.listJobs(), owner: store.ownerSnapshot(), pendingCleanup: store.pendingCleanupCount() };
8
+ }
9
+ if (action === 'recover') {
10
+ if (jobId)
11
+ throw new StoreError('invalid-input');
12
+ return await recoverWorker(store);
13
+ }
14
+ if (!jobId)
15
+ throw new StoreError('invalid-input');
16
+ if (action === 'cancel')
17
+ store.requestCancellation(jobId);
18
+ else if (action === 'retry')
19
+ store.retryJob(jobId, { automatic: false });
20
+ else
21
+ throw new StoreError('invalid-input');
22
+ return { jobId, action };
23
+ }
@@ -0,0 +1,2 @@
1
+ /** Neutralize terminal commands without changing redirected Markdown or stored content. */
2
+ export declare function terminalSafe(value: string): string;
@@ -0,0 +1,4 @@
1
+ /** Neutralize terminal commands without changing redirected Markdown or stored content. */
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'));
4
+ }
@@ -0,0 +1,16 @@
1
+ import type { LibraryStore } from '../store/index.js';
2
+ import { drainLibrary } from '../worker/basic.js';
3
+ import { type ServiceEnvironment } from '../service/launch-agent.js';
4
+ export declare function workerAction(store: LibraryStore, action: 'install' | 'status' | 'uninstall', version: string, environment?: ServiceEnvironment, options?: {
5
+ language?: 'en';
6
+ }): Promise<Record<string, unknown>>;
7
+ type DrainOptions = Parameters<typeof drainLibrary>[1];
8
+ export type ServiceDrainOptions = Omit<DrainOptions, 'runtime'> & {
9
+ runtime?: DrainOptions['runtime'];
10
+ resolveCaptureRuntime?: () => Promise<DrainOptions['runtime']>;
11
+ };
12
+ /** Dispatch only work that claimNext can admit; delayed retries retain their due time. */
13
+ export declare function hasDueAcquisition(store: LibraryStore): boolean;
14
+ export declare function drainServiceQueues(store: LibraryStore, options: ServiceDrainOptions, version: string, signal: AbortSignal): Promise<readonly unknown[]>;
15
+ export declare function serveWorker(store: LibraryStore, options: ServiceDrainOptions, version: string, environment?: ServiceEnvironment): Promise<Record<string, unknown>>;
16
+ export {};