@hraness/sponge 0.2.6 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -9
- package/bin/sponge.js +8 -7
- package/dist/contracts/index.js +24 -6
- package/dist/contracts/validate.js +106 -29
- package/dist/migrations/008.d.ts +3 -0
- package/dist/migrations/008.js +5 -0
- package/dist/migrations/009.d.ts +3 -0
- package/dist/migrations/009.js +8 -0
- package/dist/migrations/010.d.ts +3 -0
- package/dist/migrations/010.js +11 -0
- package/dist/migrations/index.d.ts +15 -0
- package/dist/migrations/index.js +39 -0
- package/dist/src/acquisition/capture.js +87 -28
- package/dist/src/acquisition/cleanup.d.ts +0 -4
- package/dist/src/acquisition/cleanup.js +3 -5
- package/dist/src/acquisition/errors.js +17 -2
- package/dist/src/acquisition/files.d.ts +0 -1
- package/dist/src/acquisition/files.js +14 -11
- package/dist/src/acquisition/index.d.ts +2 -3
- package/dist/src/acquisition/index.js +1 -2
- package/dist/src/acquisition/lock.js +1 -1
- package/dist/src/acquisition/normalize.js +57 -18
- package/dist/src/acquisition/pins.js +22 -4
- package/dist/src/acquisition/process.js +67 -29
- package/dist/src/acquisition/runtime.d.ts +5 -7
- package/dist/src/acquisition/runtime.js +129 -68
- package/dist/src/acquisition/verify-helper.js +10 -3
- package/dist/src/annotations/quote.js +40 -12
- package/dist/src/annotations/store.d.ts +0 -1
- package/dist/src/annotations/store.js +66 -14
- package/dist/src/app/article.d.ts +2 -1
- package/dist/src/app/article.js +66 -70
- package/dist/src/app/bundle-files.d.ts +0 -4
- package/dist/src/app/bundle-files.js +11 -14
- package/dist/src/app/markdown.js +18 -6
- package/dist/src/app/source.d.ts +0 -4
- package/dist/src/app/source.js +18 -14
- package/dist/src/cli/arguments.d.ts +2 -8
- package/dist/src/cli/arguments.js +67 -80
- package/dist/src/cli/commands.d.ts +42 -0
- package/dist/src/cli/commands.js +795 -0
- package/dist/src/cli/config.js +12 -9
- package/dist/src/cli/digest.d.ts +1 -1
- package/dist/src/cli/digest.js +38 -16
- package/dist/src/cli/import.js +25 -4
- package/dist/src/cli/indexing.d.ts +2 -0
- package/dist/src/cli/indexing.js +26 -9
- package/dist/src/cli/main.js +47 -386
- package/dist/src/cli/portability.d.ts +0 -2
- package/dist/src/cli/portability.js +13 -8
- package/dist/src/cli/queue.js +12 -8
- package/dist/src/cli/runtimes.d.ts +1 -3
- package/dist/src/cli/runtimes.js +13 -6
- package/dist/src/cli/terminal.js +1 -1
- package/dist/src/cli/worker.d.ts +7 -0
- package/dist/src/cli/worker.js +54 -15
- package/dist/src/derivations/kinds.d.ts +34 -0
- package/dist/src/derivations/kinds.js +14 -0
- package/dist/src/derivations/types.d.ts +48 -0
- package/dist/src/derivations/types.js +1 -0
- package/dist/src/digest/derivation.d.ts +25 -0
- package/dist/src/digest/derivation.js +124 -0
- package/dist/src/digest/editorial.d.ts +9 -46
- package/dist/src/digest/editorial.js +57 -49
- package/dist/src/digest/endpoint.js +63 -27
- package/dist/src/digest/pipeline.d.ts +49 -28
- package/dist/src/digest/pipeline.js +369 -102
- package/dist/src/digest/schema.d.ts +11 -25
- package/dist/src/digest/schema.js +86 -18
- package/dist/src/errors.d.ts +46 -0
- package/dist/src/errors.js +117 -0
- package/dist/src/identity/query-policy.d.ts +0 -5
- package/dist/src/identity/query-policy.js +61 -9
- package/dist/src/identity/uri.d.ts +6 -5
- package/dist/src/identity/uri.js +37 -18
- package/dist/src/identity/visibility.d.ts +11 -1
- package/dist/src/identity/visibility.js +81 -17
- package/dist/src/importers/apply.js +104 -36
- package/dist/src/importers/custody.js +27 -9
- package/dist/src/importers/inspect.js +125 -38
- package/dist/src/importers/jungle-digest.d.ts +3 -6
- package/dist/src/importers/jungle-digest.js +196 -69
- package/dist/src/importers/jungle-store.d.ts +19 -6
- package/dist/src/importers/jungle-store.js +124 -28
- package/dist/src/importers/jungle.d.ts +14 -14
- package/dist/src/importers/jungle.js +142 -31
- package/dist/src/importers/manifest.js +57 -21
- package/dist/src/importers/verify-helper.js +18 -4
- package/dist/src/indexing/image.js +29 -10
- package/dist/src/indexing/index.d.ts +37 -6
- package/dist/src/indexing/index.js +360 -124
- package/dist/src/indexing/jobs.js +156 -47
- package/dist/src/indexing/prune-run.d.ts +2 -0
- package/dist/src/indexing/prune-run.js +69 -26
- package/dist/src/indexing/prune.d.ts +12 -3
- package/dist/src/indexing/prune.js +85 -35
- package/dist/src/indexing/runner.d.ts +2 -1
- package/dist/src/indexing/runner.js +83 -40
- package/dist/src/indexing/upgrade.d.ts +9 -0
- package/dist/src/indexing/upgrade.js +79 -0
- package/dist/src/inference/asr/bridge.js +6 -2
- package/dist/src/inference/asr/protocol.js +109 -19
- package/dist/src/inference/asr/runtime.js +58 -21
- package/dist/src/inference/asr/windows.js +9 -5
- package/dist/src/inference/embedding/index.d.ts +2 -6
- package/dist/src/inference/embedding/index.js +89 -29
- package/dist/src/ingest/attempt.d.ts +29 -0
- package/dist/src/ingest/attempt.js +55 -0
- package/dist/src/media/capture-helper.js +7 -2
- package/dist/src/media/capture.d.ts +2 -1
- package/dist/src/media/capture.js +124 -43
- package/dist/src/media/handler.d.ts +4 -2
- package/dist/src/media/handler.js +88 -70
- package/dist/src/media/transcribe.js +63 -21
- package/dist/src/metadata/article.js +49 -17
- package/dist/src/models/artifacts.d.ts +10 -8
- package/dist/src/models/artifacts.js +237 -106
- package/dist/src/models/catalog.d.ts +2 -5
- package/dist/src/models/catalog.js +94 -25
- package/dist/src/models/download.js +55 -15
- package/dist/src/models/runtime.d.ts +0 -2
- package/dist/src/models/runtime.js +69 -33
- package/dist/src/pdf/capture-helper.js +3 -1
- package/dist/src/pdf/capture.d.ts +2 -1
- package/dist/src/pdf/capture.js +95 -27
- package/dist/src/pdf/errors.js +12 -1
- package/dist/src/pdf/handler.d.ts +2 -1
- package/dist/src/pdf/handler.js +70 -71
- package/dist/src/pdf/index.d.ts +1 -3
- package/dist/src/pdf/index.js +0 -2
- package/dist/src/pdf/input.js +12 -7
- package/dist/src/pdf/normalize.js +80 -21
- package/dist/src/pdf/source-identity.js +3 -2
- package/dist/src/pdf/source.js +12 -8
- package/dist/src/pdf/verify-helper.js +15 -3
- package/dist/src/portability/backup.d.ts +2 -9
- package/dist/src/portability/backup.js +106 -22
- package/dist/src/portability/database.js +51 -17
- package/dist/src/portability/export.js +35 -9
- package/dist/src/portability/files.d.ts +1 -4
- package/dist/src/portability/files.js +127 -67
- package/dist/src/portability/gc.js +65 -22
- package/dist/src/publish/component.js +1 -1
- package/dist/src/publish/feed.d.ts +2 -2
- package/dist/src/publish/feed.js +75 -29
- package/dist/src/publish/identity.d.ts +5 -1
- package/dist/src/publish/identity.js +31 -11
- package/dist/src/publish/index.d.ts +2 -3
- package/dist/src/publish/index.js +34 -21
- package/dist/src/publish/projection.d.ts +2 -9
- package/dist/src/publish/projection.js +21 -14
- package/dist/src/search/extension.js +3 -1
- package/dist/src/search/hybrid.d.ts +3 -8
- package/dist/src/search/hybrid.js +45 -11
- package/dist/src/search/lexical.js +18 -4
- package/dist/src/search/query.js +56 -25
- package/dist/src/search/vector.js +37 -10
- package/dist/src/service/launch-agent.d.ts +5 -3
- package/dist/src/service/launch-agent.js +272 -83
- package/dist/src/service/lifetime.js +47 -14
- package/dist/src/service/loop.d.ts +3 -3
- package/dist/src/service/loop.js +47 -19
- package/dist/src/store/annotations.d.ts +32 -0
- package/dist/src/store/annotations.js +44 -0
- package/dist/src/store/backups.d.ts +18 -0
- package/dist/src/store/backups.js +110 -0
- package/dist/src/store/blobs.d.ts +24 -0
- package/dist/src/store/blobs.js +139 -0
- package/dist/src/store/changes.d.ts +5 -12
- package/dist/src/store/changes.js +108 -56
- package/dist/src/store/context.d.ts +15 -0
- package/dist/src/store/context.js +19 -0
- package/dist/src/store/derivations.d.ts +42 -0
- package/dist/src/store/derivations.js +104 -0
- package/dist/src/store/files.d.ts +0 -2
- package/dist/src/store/files.js +19 -24
- package/dist/src/store/ids.d.ts +30 -0
- package/dist/src/store/ids.js +34 -0
- package/dist/src/store/imports.d.ts +26 -0
- package/dist/src/store/imports.js +57 -0
- package/dist/src/store/index.d.ts +3 -186
- package/dist/src/store/index.js +2 -809
- package/dist/src/store/lease.d.ts +20 -0
- package/dist/src/store/lease.js +62 -0
- package/dist/src/store/library.d.ts +75 -0
- package/dist/src/store/library.js +174 -0
- package/dist/src/store/open.d.ts +7 -0
- package/dist/src/store/open.js +183 -0
- package/dist/src/store/queue.d.ts +65 -0
- package/dist/src/store/queue.js +327 -0
- package/dist/src/store/revisions.d.ts +12 -0
- package/dist/src/store/revisions.js +189 -0
- package/dist/src/store/search-index.d.ts +63 -0
- package/dist/src/store/search-index.js +190 -0
- package/dist/src/store/sync-ready.d.ts +6 -0
- package/dist/src/store/sync-ready.js +121 -0
- package/dist/src/store/types.d.ts +1 -5
- package/dist/src/store/types.js +1 -8
- package/dist/src/util/canonical.d.ts +2 -0
- package/dist/src/util/canonical.js +17 -0
- package/dist/src/util/exact.d.ts +2 -0
- package/dist/src/util/exact.js +9 -0
- package/dist/src/util/fsync.d.ts +4 -0
- package/dist/src/util/fsync.js +22 -0
- package/dist/src/util/hash.d.ts +4 -0
- package/dist/src/util/hash.js +8 -0
- package/dist/src/worker/basic.d.ts +0 -1
- package/dist/src/worker/basic.js +23 -7
- package/dist/src/worker/custody.d.ts +4 -1
- package/dist/src/worker/custody.js +111 -38
- package/dist/src/worker/engine.d.ts +2 -1
- package/dist/src/worker/engine.js +42 -20
- package/dist/src/worker/identity.js +13 -5
- package/dist/src/worker/journal.d.ts +1 -1
- package/dist/src/worker/journal.js +54 -24
- package/dist/src/worker/lock.d.ts +18 -0
- package/dist/src/worker/lock.js +196 -0
- package/dist/src/worker/runtime-gc.d.ts +4 -3
- package/dist/src/worker/runtime-gc.js +68 -41
- package/dist/src/worker/supervisor-child.js +19 -6
- package/dist/src/worker/supervisor.js +41 -8
- package/native/runtime-manifest.json +4 -4
- package/package.json +10 -2
- package/dist/src/digest/store.d.ts +0 -57
- package/dist/src/digest/store.js +0 -79
- package/dist/src/store/foundation.d.ts +0 -7
- package/dist/src/store/foundation.js +0 -7
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import { join, isAbsolute } from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { readConnections } from './config.js';
|
|
4
|
+
import { pdfSource } from '../pdf/index.js';
|
|
5
|
+
import { articleSource, mediaSource } from '../app/source.js';
|
|
6
|
+
import { defaultCapturePins, inspectCaptureRuntime, provisionCapture, captureSetupPlan } from '../acquisition/index.js';
|
|
7
|
+
import { queueAction } from './queue.js';
|
|
8
|
+
import { portabilityAction } from './portability.js';
|
|
9
|
+
import { restoreLibrary } from '../portability/backup.js';
|
|
10
|
+
import { workerAction, serveWorker, hasDueAcquisition, workerDoctor } from './worker.js';
|
|
11
|
+
import { nativeSetupPlan, provisionNative, inspectNative } from '../models/runtime.js';
|
|
12
|
+
import { nativeDownloadAuthGuidance } from '../models/download.js';
|
|
13
|
+
import { drainIndexQueue } from '../indexing/runner.js';
|
|
14
|
+
import { indexAction, overviewText } from './indexing.js';
|
|
15
|
+
import { pruneText } from '../indexing/prune-run.js';
|
|
16
|
+
import { importAction, jungleAction, jungleText } from './import.js';
|
|
17
|
+
import { digestText, reviewFromSpec, reviewSpec } from './digest.js';
|
|
18
|
+
import { connectEndpoint } from '../digest/endpoint.js';
|
|
19
|
+
import { createDigest } from '../digest/pipeline.js';
|
|
20
|
+
import { createQueryEmbedder, createTextEmbedder } from '../search/query.js';
|
|
21
|
+
import { runtimeGcAction, runtimeGcText, setupRuntimeGc } from './runtimes.js';
|
|
22
|
+
import { drainLibrary } from '../worker/basic.js';
|
|
23
|
+
import { minAge, publishSite } from '../publish/index.js';
|
|
24
|
+
import { SpongeError, codeOf, publicCode, exitFor, isSpongeCode } from '../errors.js';
|
|
25
|
+
const abs = (v) => typeof v === 'string' && isAbsolute(v);
|
|
26
|
+
const media = { language: 'value', 'force-local': 'boolean', 'allow-private-network': 'boolean' };
|
|
27
|
+
const lines = (items) => items.map((item) => `${item.itemId}\t${item.title}`);
|
|
28
|
+
/** A required capture runtime, as the categorical code of its inspection failure. */
|
|
29
|
+
async function captureRuntime(c) {
|
|
30
|
+
try {
|
|
31
|
+
return await inspectCaptureRuntime(c.captureRoot, defaultCapturePins);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new SpongeError(publicCode(error, 'runtime-missing'), { cause: error });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** The failure of a job that did not run here: its state, or its recorded reason. */
|
|
38
|
+
function jobFailure(store, jobId) {
|
|
39
|
+
const job = store.queue.list().find((j) => j.id === jobId), reason = job?.reason;
|
|
40
|
+
return new SpongeError(job?.state === 'cancelled'
|
|
41
|
+
? 'cancelled'
|
|
42
|
+
: job?.state === 'cleanup-pending'
|
|
43
|
+
? 'cleanup-required'
|
|
44
|
+
: job?.state === 'running'
|
|
45
|
+
? 'owner-busy'
|
|
46
|
+
: isSpongeCode(reason)
|
|
47
|
+
? reason
|
|
48
|
+
: 'unclassified');
|
|
49
|
+
}
|
|
50
|
+
const resolveConnection = (connections) => (realm) => {
|
|
51
|
+
if (realm === 'public')
|
|
52
|
+
return undefined;
|
|
53
|
+
const connection = connections.find((c) => c.realm === realm);
|
|
54
|
+
if (!connection)
|
|
55
|
+
throw new SpongeError('repair-auth');
|
|
56
|
+
return connection.capture;
|
|
57
|
+
};
|
|
58
|
+
/** add/refresh with --wait and run --drain: drain acquisition (one job when given), then queued indexes for run. */
|
|
59
|
+
async function drain(c, jobId, connections) {
|
|
60
|
+
const { store, args, signal } = c;
|
|
61
|
+
connections ??= await readConnections(store.root);
|
|
62
|
+
if (jobId) {
|
|
63
|
+
const job = store.queue.list().find((j) => j.id === jobId);
|
|
64
|
+
if (job && job.state !== 'queued')
|
|
65
|
+
throw jobFailure(store, jobId);
|
|
66
|
+
}
|
|
67
|
+
const runtime = await captureRuntime(c);
|
|
68
|
+
let asrRuntime;
|
|
69
|
+
try {
|
|
70
|
+
asrRuntime = await inspectNative('asr', c.nativeRoot, c.version);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (!['runtime-missing', 'unsupported'].includes(publicCode(error, 'integrity')))
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
const results = await drainLibrary(store, {
|
|
77
|
+
asrRuntime,
|
|
78
|
+
language: args.flags.language,
|
|
79
|
+
forceLocal: args.flags['force-local'] === true,
|
|
80
|
+
runtime,
|
|
81
|
+
jobId,
|
|
82
|
+
signal,
|
|
83
|
+
allowPrivateNetwork: args.flags['allow-private-network'] === true,
|
|
84
|
+
resolveConnection: resolveConnection(connections),
|
|
85
|
+
});
|
|
86
|
+
if (jobId && !results.length && !signal.aborted)
|
|
87
|
+
throw jobFailure(store, jobId);
|
|
88
|
+
let indexResults = [];
|
|
89
|
+
if (args.command === 'run' &&
|
|
90
|
+
!signal.aborted &&
|
|
91
|
+
store.queue.list().some((job) => job.operation === 'index' && job.state === 'queued')) {
|
|
92
|
+
const embedding = await inspectNative('embedding', c.nativeRoot, c.version);
|
|
93
|
+
indexResults = await drainIndexQueue(store, { runtime: embedding, signal });
|
|
94
|
+
const indexFailed = indexResults.find((result) => !result.ok);
|
|
95
|
+
if (indexFailed && !indexFailed.ok) {
|
|
96
|
+
process.stderr.write(JSON.stringify(indexFailed) + '\n');
|
|
97
|
+
return exitFor(indexFailed.code);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const failed = results.find((result) => !result.ok);
|
|
101
|
+
if (failed && !failed.ok) {
|
|
102
|
+
process.stderr.write(JSON.stringify(failed) + '\n');
|
|
103
|
+
return exitFor(failed.code);
|
|
104
|
+
}
|
|
105
|
+
if (signal.aborted)
|
|
106
|
+
return 130;
|
|
107
|
+
c.emit({ results, indexResults }, results.map((result) => (result.ok ? `${result.itemId}\t${result.status}` : '')).join('\n') || 'Queue empty');
|
|
108
|
+
return results.some((result) => result.ok && result.status === 'partial') ? 3 : 0;
|
|
109
|
+
}
|
|
110
|
+
export const commands = [
|
|
111
|
+
{
|
|
112
|
+
name: 'init',
|
|
113
|
+
summary: 'Create a private library',
|
|
114
|
+
usage: [['init', 'Create a private library']],
|
|
115
|
+
positional: 0,
|
|
116
|
+
flags: {},
|
|
117
|
+
library: 'create',
|
|
118
|
+
handler: (c) => {
|
|
119
|
+
c.emit({ library: c.store.root }, `Library: ${c.store.root}`);
|
|
120
|
+
return 0;
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'setup',
|
|
125
|
+
summary: 'Download one pinned runtime',
|
|
126
|
+
usage: [['setup --capture|--asr|--embedding', 'Download one pinned runtime']],
|
|
127
|
+
positional: 0,
|
|
128
|
+
flags: { capture: 'boolean', asr: 'boolean', embedding: 'boolean' },
|
|
129
|
+
library: 'open',
|
|
130
|
+
check: (_, f) => [f.capture, f.asr, f.embedding].filter(Boolean).length === 1,
|
|
131
|
+
handler: async (c) => {
|
|
132
|
+
if (!c.args.flags.capture) {
|
|
133
|
+
const kind = c.args.flags.asr ? 'asr' : 'embedding', plan = await nativeSetupPlan(kind, c.version);
|
|
134
|
+
process.stderr.write(JSON.stringify({ setup: kind, ...plan }) + '\n');
|
|
135
|
+
try {
|
|
136
|
+
await provisionNative(kind, c.nativeRoot, c.version, c.signal);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (codeOf(error) === 'repair-auth' && !c.args.json)
|
|
140
|
+
process.stderr.write(nativeDownloadAuthGuidance + '\n');
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
const gc = await setupRuntimeGc(c.store, c.version);
|
|
144
|
+
c.emit({ [kind]: 'ready', gc });
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
const plan = captureSetupPlan(defaultCapturePins);
|
|
148
|
+
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`);
|
|
149
|
+
await provisionCapture({
|
|
150
|
+
runtimeRoot: join(c.store.root, 'runtime', 'capture'),
|
|
151
|
+
pins: defaultCapturePins,
|
|
152
|
+
signal: c.signal,
|
|
153
|
+
});
|
|
154
|
+
const gc = await setupRuntimeGc(c.store, c.version);
|
|
155
|
+
c.emit({ capture: 'ready', gc }, 'skipped' in gc ? `Capture ready\nRuntime GC skipped: ${gc.skipped}` : `Capture ready\n${runtimeGcText(gc)}`);
|
|
156
|
+
return 0;
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'add',
|
|
161
|
+
summary: 'Save an article, PDF or media source',
|
|
162
|
+
usage: [['add URL_OR_PDF [--kind article|pdf|media] [--connection ID] [--wait]', '']],
|
|
163
|
+
positional: 1,
|
|
164
|
+
flags: { connection: 'value', kind: 'value', wait: 'boolean', ...media },
|
|
165
|
+
library: 'open',
|
|
166
|
+
handler: async (c) => {
|
|
167
|
+
const { store, args } = c, connections = await readConnections(store.root);
|
|
168
|
+
const connection = args.flags.connection ? connections.find((x) => x.id === args.flags.connection) : undefined;
|
|
169
|
+
if (args.flags.connection && !connection)
|
|
170
|
+
throw new SpongeError('invalid-input');
|
|
171
|
+
const input = args.positional[0], kind = (args.flags.kind ??
|
|
172
|
+
(/\.pdf(?:[?#]|$)/i.test(input) || input.startsWith('file:') ? 'pdf' : 'article'));
|
|
173
|
+
const source = (kind === 'pdf' ? pdfSource : kind === 'media' ? mediaSource : articleSource)(input, connection?.realm ?? 'public');
|
|
174
|
+
const submission = store.queue.submit({ kind, ...source, idempotencyKey: 'save:' + source.sourceKey }), { jobId, itemId } = submission;
|
|
175
|
+
const job = store.queue.list().find((j) => j.id === jobId);
|
|
176
|
+
if (!args.flags.wait) {
|
|
177
|
+
c.emit({ ...submission, state: job?.state ?? 'queued' }, `${itemId}\t${job?.state ?? 'queued'}`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
if (job?.state === 'succeeded') {
|
|
181
|
+
const item = store.read(itemId);
|
|
182
|
+
c.emit({ itemId, jobId, status: item.status }, `${itemId}\t${item.status}`);
|
|
183
|
+
return item.status === 'partial' ? 3 : 0;
|
|
184
|
+
}
|
|
185
|
+
return drain(c, jobId, connections);
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'run',
|
|
190
|
+
summary: 'Process queued work sequentially',
|
|
191
|
+
usage: [['run --drain', 'Process queued articles sequentially']],
|
|
192
|
+
positional: 0,
|
|
193
|
+
flags: { drain: 'boolean', ...media },
|
|
194
|
+
library: 'open',
|
|
195
|
+
check: (_, f) => f.drain === true,
|
|
196
|
+
handler: async (c) => {
|
|
197
|
+
const { store, signal } = c;
|
|
198
|
+
if (hasDueAcquisition(store))
|
|
199
|
+
return drain(c);
|
|
200
|
+
if (store.queue.ownerSnapshot())
|
|
201
|
+
throw new SpongeError('owner-busy');
|
|
202
|
+
if (store.queue.pendingCleanup())
|
|
203
|
+
throw new SpongeError('cleanup-required');
|
|
204
|
+
if (!store.queue.list().some((job) => job.operation === 'index' && job.state === 'queued')) {
|
|
205
|
+
c.emit({ results: [], indexResults: [] }, 'Queue empty');
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
const runtime = await inspectNative('embedding', c.nativeRoot, c.version), indexResults = await drainIndexQueue(store, { runtime, signal });
|
|
209
|
+
c.emit({ results: [], indexResults });
|
|
210
|
+
const failed = indexResults.find((result) => !result.ok);
|
|
211
|
+
return failed && !failed.ok ? exitFor(failed.code) : signal.aborted ? 130 : 0;
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'queue',
|
|
216
|
+
summary: 'Inspect and manage jobs',
|
|
217
|
+
usage: [
|
|
218
|
+
['queue status', 'Show jobs and their states'],
|
|
219
|
+
['queue cancel JOB', 'Cancel a queued or active job'],
|
|
220
|
+
['queue retry JOB', 'Retry a failed job after resolving its cause'],
|
|
221
|
+
['queue recover', 'Reconcile a stopped worker after cleanup'],
|
|
222
|
+
],
|
|
223
|
+
positional: (p) => (['status', 'recover'].includes(p[0]) && p.length === 1) ||
|
|
224
|
+
(['cancel', 'retry'].includes(p[0]) && p.length === 2),
|
|
225
|
+
flags: {},
|
|
226
|
+
library: 'open',
|
|
227
|
+
handler: async (c) => {
|
|
228
|
+
c.emit(await queueAction(c.store, c.args.positional[0], c.args.positional[1]));
|
|
229
|
+
return 0;
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'list',
|
|
234
|
+
summary: 'List saved and queued items',
|
|
235
|
+
usage: [['list', 'List saved and queued items']],
|
|
236
|
+
positional: 0,
|
|
237
|
+
flags: {},
|
|
238
|
+
library: 'open',
|
|
239
|
+
handler: (c) => {
|
|
240
|
+
const items = c.store.list();
|
|
241
|
+
c.emit({ items }, lines(items).join('\n'));
|
|
242
|
+
return 0;
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'show',
|
|
247
|
+
summary: 'Show an item as Markdown or JSON',
|
|
248
|
+
usage: [['show ITEM [--format markdown|json]', '']],
|
|
249
|
+
positional: 1,
|
|
250
|
+
flags: { format: 'value' },
|
|
251
|
+
library: 'open',
|
|
252
|
+
handler: (c) => {
|
|
253
|
+
const { store } = c, item = store.read(c.args.positional[0]);
|
|
254
|
+
if (!item)
|
|
255
|
+
throw new SpongeError('unavailable');
|
|
256
|
+
c.emit({
|
|
257
|
+
item,
|
|
258
|
+
identity: store.identity(item.itemId),
|
|
259
|
+
annotations: store.annotations.list(item.itemId),
|
|
260
|
+
digest: store.derivations.current('digest', item.itemId) ?? null,
|
|
261
|
+
editorial: store.derivations.current('editorial', item.itemId) ?? null,
|
|
262
|
+
}, item.markdown);
|
|
263
|
+
return item.status === 'partial' ? 3 : 0;
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'asset',
|
|
268
|
+
summary: 'Write a verified asset to stdout',
|
|
269
|
+
usage: [['asset ITEM SHA', 'Write a verified asset to stdout']],
|
|
270
|
+
positional: 2,
|
|
271
|
+
flags: {},
|
|
272
|
+
library: 'open',
|
|
273
|
+
handler: (c) => {
|
|
274
|
+
const item = c.store.read(c.args.positional[0]), asset = item?.assets.find((a) => a.sha256 === c.args.positional[1]);
|
|
275
|
+
if (!asset)
|
|
276
|
+
throw new SpongeError('unavailable');
|
|
277
|
+
const bytes = c.store.blobs.read(asset);
|
|
278
|
+
if (c.args.json)
|
|
279
|
+
c.emit({ asset });
|
|
280
|
+
else {
|
|
281
|
+
if (process.stdout.isTTY)
|
|
282
|
+
throw new SpongeError('invalid-input');
|
|
283
|
+
c.write(bytes);
|
|
284
|
+
}
|
|
285
|
+
return 0;
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'search',
|
|
290
|
+
summary: 'Search saved items',
|
|
291
|
+
usage: [['search QUERY [--mode hybrid|lexical] [--profile default] [--limit 20]', '']],
|
|
292
|
+
positional: 1,
|
|
293
|
+
flags: { mode: 'value', limit: 'value', profile: 'value' },
|
|
294
|
+
library: 'open',
|
|
295
|
+
handler: async (c) => {
|
|
296
|
+
const { store, args, signal } = c;
|
|
297
|
+
if (args.flags.mode === 'lexical') {
|
|
298
|
+
const items = store.search(args.positional[0], Number(args.flags.limit ?? 20));
|
|
299
|
+
c.emit({ items }, lines(items).join('\n'));
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
let embedding;
|
|
303
|
+
try {
|
|
304
|
+
embedding = await inspectNative('embedding', c.nativeRoot, c.version);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
/* Hybrid explicitly reports semantic unavailability and preserves lexical results. */
|
|
308
|
+
}
|
|
309
|
+
const result = await store.index.search(args.positional[0], {
|
|
310
|
+
profileId: args.flags.profile,
|
|
311
|
+
limit: Number(args.flags.limit ?? 20),
|
|
312
|
+
signal,
|
|
313
|
+
...(embedding ? { embed: createQueryEmbedder(store, embedding) } : {}),
|
|
314
|
+
});
|
|
315
|
+
c.emit({ items: result.hits, warnings: result.warnings, generation: result.generation }, [...lines(result.hits), ...result.warnings.map((warning) => `Warning: ${warning}`)].join('\n'));
|
|
316
|
+
return signal.aborted ? 130 : 0;
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'refresh',
|
|
321
|
+
summary: 'Capture a new revision',
|
|
322
|
+
usage: [['refresh ITEM [--wait]', 'Capture a new revision']],
|
|
323
|
+
positional: 1,
|
|
324
|
+
flags: { wait: 'boolean', ...media },
|
|
325
|
+
library: 'open',
|
|
326
|
+
handler: async (c) => {
|
|
327
|
+
const { store, args } = c, connections = await readConnections(store.root), previous = store.queue.list().find((j) => j.item_id === args.positional[0]), source = previous && store.queue.source(String(previous.id));
|
|
328
|
+
if (!source || !['article', 'pdf', 'media'].includes(source.kind))
|
|
329
|
+
throw new SpongeError('unavailable');
|
|
330
|
+
const submission = store.queue.submit({
|
|
331
|
+
...source,
|
|
332
|
+
kind: source.kind,
|
|
333
|
+
intent: 'refresh',
|
|
334
|
+
idempotencyKey: randomUUID(),
|
|
335
|
+
});
|
|
336
|
+
if (!args.flags.wait) {
|
|
337
|
+
c.emit(submission, `${submission.itemId}\tqueued`);
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
return drain(c, submission.jobId, connections);
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: 'delete',
|
|
345
|
+
summary: 'Hide an item and remove it from search',
|
|
346
|
+
usage: [['delete ITEM', 'Hide an item and remove it from search']],
|
|
347
|
+
positional: 1,
|
|
348
|
+
flags: {},
|
|
349
|
+
library: 'open',
|
|
350
|
+
handler: (c) => {
|
|
351
|
+
c.store.delete(c.args.positional[0]);
|
|
352
|
+
c.emit({ itemId: c.args.positional[0], deleted: true }, 'Deleted');
|
|
353
|
+
return 0;
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: 'index',
|
|
358
|
+
summary: 'Manage embedding generations',
|
|
359
|
+
usage: [
|
|
360
|
+
['index create [--profile default]', 'Queue a new embedding generation'],
|
|
361
|
+
['index status [GENERATION] [--profile default]', 'Active (or given) generation coverage and pending inputs'],
|
|
362
|
+
['index run', 'Drain queued embedding jobs'],
|
|
363
|
+
['index rollback GENERATION', 'Reactivate a compatible complete generation'],
|
|
364
|
+
['index prune [--keep 1] [--dry-run] [--vacuum]', 'Delete vectors of retired generations'],
|
|
365
|
+
],
|
|
366
|
+
positional: (p) => (['create', 'run', 'prune'].includes(p[0]) && p.length === 1) ||
|
|
367
|
+
(p[0] === 'status' && p.length <= 2) ||
|
|
368
|
+
(p[0] === 'rollback' && p.length === 2),
|
|
369
|
+
flags: { profile: 'value', keep: 'value', 'dry-run': 'boolean', vacuum: 'boolean' },
|
|
370
|
+
library: 'open',
|
|
371
|
+
check: (p, f) => p[0] === 'prune' || (f.keep === undefined && !f['dry-run'] && !f.vacuum),
|
|
372
|
+
handler: async (c) => {
|
|
373
|
+
const { args } = c, action = args.positional[0], runtime = ['create', 'run'].includes(action)
|
|
374
|
+
? await inspectNative('embedding', c.nativeRoot, c.version)
|
|
375
|
+
: undefined;
|
|
376
|
+
const result = await indexAction(c.store, action, args.positional[1], {
|
|
377
|
+
runtime,
|
|
378
|
+
profile: args.flags.profile,
|
|
379
|
+
signal: c.signal,
|
|
380
|
+
...(action === 'prune'
|
|
381
|
+
? {
|
|
382
|
+
keep: args.flags.keep === undefined ? undefined : Number(args.flags.keep),
|
|
383
|
+
dryRun: args.flags['dry-run'] === true,
|
|
384
|
+
vacuum: args.flags.vacuum === true,
|
|
385
|
+
}
|
|
386
|
+
: {}),
|
|
387
|
+
});
|
|
388
|
+
c.emit(result, result.prune
|
|
389
|
+
? pruneText(result.prune)
|
|
390
|
+
: action === 'status' && !args.positional[1]
|
|
391
|
+
? overviewText(result)
|
|
392
|
+
: undefined);
|
|
393
|
+
const failed = result.results?.find((r) => !r.ok);
|
|
394
|
+
return failed ? exitFor(failed.code) : 0;
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: 'doctor',
|
|
399
|
+
summary: 'Check library and runtime states',
|
|
400
|
+
usage: [['doctor', 'Check library and runtime states']],
|
|
401
|
+
positional: 0,
|
|
402
|
+
flags: {},
|
|
403
|
+
library: 'open',
|
|
404
|
+
handler: async (c) => {
|
|
405
|
+
const { store } = c;
|
|
406
|
+
let capture = 'ready';
|
|
407
|
+
try {
|
|
408
|
+
await inspectCaptureRuntime(c.captureRoot, defaultCapturePins);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
capture = publicCode(error, 'runtime-missing');
|
|
412
|
+
}
|
|
413
|
+
const states = {};
|
|
414
|
+
for (const kind of ['asr', 'embedding']) {
|
|
415
|
+
try {
|
|
416
|
+
await inspectNative(kind, c.nativeRoot, c.version);
|
|
417
|
+
states[kind] = 'ready';
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
states[kind] = publicCode(error, 'runtime-missing');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const cleanup = store.queue.pendingCleanup(), worker = await workerDoctor(store, c.version), workerBroken = !['ready', 'not-installed', 'unsupported'].includes(worker.worker);
|
|
424
|
+
c.emit({ library: 'ready', capture, ...states, pendingCleanup: cleanup, worker }, `Library: ready\nCapture: ${capture}\nASR: ${states.asr}\nEmbedding: ${states.embedding}\nPending cleanup: ${cleanup}\nWorker: ${worker.worker}${Array.isArray(worker.drift) ? ` (${worker.drift.join(', ')})` : ''}${worker.repair ? `\n ${worker.repair}` : ''}`);
|
|
425
|
+
return cleanup || capture !== 'ready' || workerBroken ? 4 : 0;
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'export',
|
|
430
|
+
summary: 'Export Markdown and local assets',
|
|
431
|
+
usage: [['export ITEM --output DIR', 'Export Markdown and local assets']],
|
|
432
|
+
positional: 1,
|
|
433
|
+
flags: { output: 'value' },
|
|
434
|
+
library: 'open',
|
|
435
|
+
check: (_, f) => abs(f.output),
|
|
436
|
+
handler: async (c) => {
|
|
437
|
+
c.emit(await portabilityAction(c.store, 'export', {
|
|
438
|
+
itemId: c.args.positional[0],
|
|
439
|
+
output: c.args.flags.output,
|
|
440
|
+
}));
|
|
441
|
+
return 0;
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: 'backup',
|
|
446
|
+
summary: 'Save a verified offline backup',
|
|
447
|
+
usage: [['backup --output FILE', 'Save a verified offline backup']],
|
|
448
|
+
positional: 0,
|
|
449
|
+
flags: { output: 'value' },
|
|
450
|
+
library: 'open',
|
|
451
|
+
check: (_, f) => abs(f.output),
|
|
452
|
+
handler: async (c) => {
|
|
453
|
+
c.emit(await portabilityAction(c.store, 'backup', { output: c.args.flags.output }));
|
|
454
|
+
return 0;
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: 'restore',
|
|
459
|
+
summary: 'Restore into a new private library',
|
|
460
|
+
usage: [['restore FILE --into NEW_DIR', 'Restore into a new private library']],
|
|
461
|
+
positional: 1,
|
|
462
|
+
flags: { into: 'value' },
|
|
463
|
+
library: 'none',
|
|
464
|
+
check: (p, f) => abs(f.into) && f.library === undefined && abs(p[0]),
|
|
465
|
+
handler: async (c) => {
|
|
466
|
+
c.emit(await restoreLibrary(c.args.positional[0], String(c.args.flags.into)));
|
|
467
|
+
return 0;
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: 'gc',
|
|
472
|
+
summary: 'Collect unreferenced blobs and superseded runtimes',
|
|
473
|
+
usage: [
|
|
474
|
+
['gc --dry-run', 'Write a report of unreferenced blobs'],
|
|
475
|
+
['gc --apply REPORT', 'Recheck and remove reported blobs'],
|
|
476
|
+
['gc --runtimes [--dry-run]', 'Remove superseded runtimes, models and worker journals'],
|
|
477
|
+
],
|
|
478
|
+
positional: 0,
|
|
479
|
+
flags: { 'dry-run': 'boolean', apply: 'value', runtimes: 'boolean' },
|
|
480
|
+
library: 'open',
|
|
481
|
+
check: (_, f) => f.runtimes
|
|
482
|
+
? f.apply === undefined
|
|
483
|
+
: Boolean(f['dry-run']) !== Boolean(f.apply) && (f.apply === undefined || abs(f.apply)),
|
|
484
|
+
handler: async (c) => {
|
|
485
|
+
if (c.args.flags.runtimes) {
|
|
486
|
+
const report = await runtimeGcAction(c.store, c.version, c.args.flags['dry-run'] === true);
|
|
487
|
+
c.emit(report, runtimeGcText(report));
|
|
488
|
+
return 0;
|
|
489
|
+
}
|
|
490
|
+
c.emit(await portabilityAction(c.store, 'gc', {
|
|
491
|
+
dryRun: c.args.flags['dry-run'] === true,
|
|
492
|
+
report: c.args.flags.apply,
|
|
493
|
+
}));
|
|
494
|
+
return 0;
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: 'worker',
|
|
499
|
+
summary: 'Manage the optional background worker',
|
|
500
|
+
usage: [
|
|
501
|
+
['worker install [--language en]', 'Enable the optional background worker'],
|
|
502
|
+
['worker status', 'Inspect the background worker'],
|
|
503
|
+
['worker uninstall', 'Stop it and preserve the library'],
|
|
504
|
+
],
|
|
505
|
+
positional: 1,
|
|
506
|
+
flags: { language: 'value', 'force-local': 'boolean' },
|
|
507
|
+
library: 'open',
|
|
508
|
+
check: (p) => ['install', 'status', 'uninstall', 'serve'].includes(p[0]),
|
|
509
|
+
handler: async (c) => {
|
|
510
|
+
const { store, args } = c;
|
|
511
|
+
if (args.positional[0] !== 'serve') {
|
|
512
|
+
c.emit(await workerAction(store, args.positional[0], c.version, undefined, {
|
|
513
|
+
language: args.flags.language,
|
|
514
|
+
}));
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
const connections = await readConnections(store.root);
|
|
518
|
+
c.emit(await serveWorker(store, {
|
|
519
|
+
signal: c.signal,
|
|
520
|
+
language: args.flags.language,
|
|
521
|
+
forceLocal: args.flags['force-local'] === true,
|
|
522
|
+
resolveCaptureRuntime: () => inspectCaptureRuntime(c.captureRoot, defaultCapturePins),
|
|
523
|
+
resolveConnection: resolveConnection(connections),
|
|
524
|
+
}, c.version));
|
|
525
|
+
return 0;
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: 'import',
|
|
530
|
+
summary: 'Import existing captures',
|
|
531
|
+
usage: [
|
|
532
|
+
['import MANIFEST [--apply]', 'Preview or import existing Wordcell captures'],
|
|
533
|
+
['import jungle DIR [--apply]', 'Preview or import Jungle Reading notes, their captures and published state'],
|
|
534
|
+
],
|
|
535
|
+
positional: (p) => p.length === 1 || (p[0] === 'jungle' && p.length === 2),
|
|
536
|
+
flags: { apply: 'boolean', 'dry-run': 'boolean', limit: 'value', offset: 'value' },
|
|
537
|
+
library: 'open',
|
|
538
|
+
check: (p, f) => abs(p.at(-1)) &&
|
|
539
|
+
!(f.apply && f['dry-run']) &&
|
|
540
|
+
(f.offset === undefined || /^(0|[1-9][0-9]{0,5})$/.test(String(f.offset))),
|
|
541
|
+
handler: async (c) => {
|
|
542
|
+
const { store, args, signal } = c, dryRun = args.flags.apply !== true, offset = Number(args.flags.offset ?? 0);
|
|
543
|
+
if (args.positional[0] === 'jungle' && args.positional.length === 2) {
|
|
544
|
+
// A dry run reads Jungle and the library only; applying reverifies every capture with the released verifier.
|
|
545
|
+
const runtime = dryRun ? undefined : await captureRuntime(c);
|
|
546
|
+
const report = await jungleAction(store, args.positional[1], {
|
|
547
|
+
dryRun,
|
|
548
|
+
runtime,
|
|
549
|
+
limit: Number(args.flags.limit ?? 1000),
|
|
550
|
+
offset,
|
|
551
|
+
signal,
|
|
552
|
+
});
|
|
553
|
+
c.emit({ report }, jungleText(report));
|
|
554
|
+
return signal.aborted
|
|
555
|
+
? 130
|
|
556
|
+
: report.counts.skipped['capture-failed'] || report.counts.skipped['capture-unsupported']
|
|
557
|
+
? 3
|
|
558
|
+
: 0;
|
|
559
|
+
}
|
|
560
|
+
const runtime = await captureRuntime(c), report = await importAction(store, args.positional[0], {
|
|
561
|
+
runtime,
|
|
562
|
+
dryRun,
|
|
563
|
+
limit: Number(args.flags.limit ?? 20),
|
|
564
|
+
offset,
|
|
565
|
+
signal,
|
|
566
|
+
});
|
|
567
|
+
c.emit({ report });
|
|
568
|
+
return signal.aborted
|
|
569
|
+
? 130
|
|
570
|
+
: report.entries.some((entry) => entry.status === 'failed' || entry.status === 'unsupported')
|
|
571
|
+
? 3
|
|
572
|
+
: 0;
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: 'visibility',
|
|
577
|
+
summary: 'Override or restore the default visibility',
|
|
578
|
+
usage: [['visibility ITEM public|private|default', 'Override or restore the default']],
|
|
579
|
+
positional: 2,
|
|
580
|
+
flags: {},
|
|
581
|
+
library: 'open',
|
|
582
|
+
check: (p) => ['public', 'private', 'default'].includes(p[1]),
|
|
583
|
+
handler: (c) => {
|
|
584
|
+
const [itemId, value] = c.args.positional, state = c.store.setVisibility(itemId, value === 'default' ? null : value);
|
|
585
|
+
c.emit({ itemId, visibility: state }, `${state.effective}${state.override ? ' (set)' : ' (default)'}`);
|
|
586
|
+
return 0;
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
name: 'title',
|
|
591
|
+
summary: "Set or reset an item's title",
|
|
592
|
+
usage: [
|
|
593
|
+
['title set ITEM TEXT', 'Use TEXT as the title; refresh and import never change it'],
|
|
594
|
+
['title reset ITEM', 'Restore the default: the captured title (a local scan takes its import note title)'],
|
|
595
|
+
],
|
|
596
|
+
positional: (p) => (p[0] === 'set' && p.length === 3) || (p[0] === 'reset' && p.length === 2),
|
|
597
|
+
flags: {},
|
|
598
|
+
library: 'open',
|
|
599
|
+
handler: (c) => {
|
|
600
|
+
const [action, itemId, value] = c.args.positional, state = c.store.setTitle(itemId, action === 'set' ? value : null);
|
|
601
|
+
c.emit({ itemId, title: state }, `${state.title}${state.override !== null ? ' (set)' : ' (default)'}`);
|
|
602
|
+
return 0;
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: 'highlight',
|
|
607
|
+
summary: 'Anchor a verbatim quote',
|
|
608
|
+
usage: [['highlight ITEM TEXT [--note BODY] [--occurrence N]', 'Anchor a verbatim quote']],
|
|
609
|
+
positional: 2,
|
|
610
|
+
flags: { note: 'value', occurrence: 'value' },
|
|
611
|
+
library: 'open',
|
|
612
|
+
handler: (c) => {
|
|
613
|
+
const { flags, positional } = c.args, annotation = c.store.annotations.addHighlight({
|
|
614
|
+
itemId: positional[0],
|
|
615
|
+
quote: positional[1],
|
|
616
|
+
...(typeof flags.note === 'string' ? { note: flags.note } : {}),
|
|
617
|
+
...(flags.occurrence !== undefined ? { occurrence: Number(flags.occurrence) } : {}),
|
|
618
|
+
});
|
|
619
|
+
c.emit({ annotation }, annotation.id);
|
|
620
|
+
return 0;
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
name: 'comment',
|
|
625
|
+
summary: 'Comment on an item or reply',
|
|
626
|
+
usage: [['comment ITEM BODY [--on ANNOTATION]', 'Comment on an item or reply']],
|
|
627
|
+
positional: 2,
|
|
628
|
+
flags: { on: 'value' },
|
|
629
|
+
library: 'open',
|
|
630
|
+
handler: (c) => {
|
|
631
|
+
const { flags, positional } = c.args, annotation = c.store.annotations.addComment({
|
|
632
|
+
itemId: positional[0],
|
|
633
|
+
body: positional[1],
|
|
634
|
+
...(typeof flags.on === 'string' ? { parentId: flags.on } : {}),
|
|
635
|
+
});
|
|
636
|
+
c.emit({ annotation }, annotation.id);
|
|
637
|
+
return 0;
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: 'publish',
|
|
642
|
+
summary: 'Show or write the public reading list',
|
|
643
|
+
usage: [
|
|
644
|
+
['publish --dry-run [--min-age-days N]', 'Show the public reading-list entries'],
|
|
645
|
+
[
|
|
646
|
+
'publish --output DIR [--title T] [--base-url https://...] [--min-age-days N]',
|
|
647
|
+
'Write a signed static feed site with <sponge-list>',
|
|
648
|
+
],
|
|
649
|
+
],
|
|
650
|
+
positional: 0,
|
|
651
|
+
flags: { 'dry-run': 'boolean', output: 'value', 'min-age-days': 'value', title: 'value', 'base-url': 'value' },
|
|
652
|
+
library: 'open',
|
|
653
|
+
check: (_, f) => Boolean(f['dry-run']) !== Boolean(f.output) &&
|
|
654
|
+
(f.output === undefined || abs(f.output)) &&
|
|
655
|
+
((f.title === undefined && f['base-url'] === undefined) || Boolean(f.output)) &&
|
|
656
|
+
(f['min-age-days'] === undefined || /^(0|[1-9][0-9]{0,3})$/.test(String(f['min-age-days']))) &&
|
|
657
|
+
Number(f['min-age-days'] ?? 0) <= 3650 &&
|
|
658
|
+
(f.title === undefined || String(f.title).length <= 200) &&
|
|
659
|
+
(f['base-url'] === undefined || /^https:\/\//.test(String(f['base-url']))),
|
|
660
|
+
handler: (c) => {
|
|
661
|
+
const { store, args } = c, minAgeDays = Number(args.flags['min-age-days'] ?? 0);
|
|
662
|
+
if (typeof args.flags.output === 'string') {
|
|
663
|
+
const result = publishSite(store, {
|
|
664
|
+
output: args.flags.output,
|
|
665
|
+
minAgeDays,
|
|
666
|
+
...(typeof args.flags.title === 'string' ? { title: args.flags.title } : {}),
|
|
667
|
+
...(typeof args.flags['base-url'] === 'string' ? { baseUrl: args.flags['base-url'] } : {}),
|
|
668
|
+
});
|
|
669
|
+
c.emit(result, `Published ${result.entries} entr${result.entries === 1 ? 'y' : 'ies'} to ${result.output}`);
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
const all = store.publicProjection(), projection = { ...all, entries: minAge(all.entries, minAgeDays) };
|
|
673
|
+
c.emit({
|
|
674
|
+
dryRun: true,
|
|
675
|
+
...projection,
|
|
676
|
+
...(minAgeDays ? { held: all.entries.length - projection.entries.length } : {}),
|
|
677
|
+
}, [
|
|
678
|
+
...projection.entries.map((entry) => `${entry.uri}\t${entry.title}`),
|
|
679
|
+
...(projection.skipped.length
|
|
680
|
+
? [`Skipped ${projection.skipped.length} public item(s) without a public link`]
|
|
681
|
+
: []),
|
|
682
|
+
].join('\n') || 'Nothing public');
|
|
683
|
+
return 0;
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: 'digest',
|
|
688
|
+
summary: 'Draft or show an item digest',
|
|
689
|
+
usage: [
|
|
690
|
+
[
|
|
691
|
+
'digest ITEM --endpoint http://127.0.0.1:PORT --model NAME [--check-support]',
|
|
692
|
+
'Draft a digest with a local model server you started',
|
|
693
|
+
],
|
|
694
|
+
['digest ITEM', 'Show the current digest and its review state'],
|
|
695
|
+
],
|
|
696
|
+
positional: 1,
|
|
697
|
+
flags: { endpoint: 'value', model: 'value', 'check-support': 'boolean' },
|
|
698
|
+
library: 'open',
|
|
699
|
+
check: (_, f) => Boolean(f.endpoint) === Boolean(f.model) &&
|
|
700
|
+
!(f['check-support'] && !f.endpoint) &&
|
|
701
|
+
(f.model === undefined || /^[\w.:/@+-]{1,256}$/.test(String(f.model))),
|
|
702
|
+
handler: async (c) => {
|
|
703
|
+
const { store, args, signal } = c;
|
|
704
|
+
if (!args.flags.endpoint) {
|
|
705
|
+
const digest = store.derivations.current('digest', args.positional[0]);
|
|
706
|
+
if (!digest)
|
|
707
|
+
throw new SpongeError('unavailable');
|
|
708
|
+
c.emit({ digest }, digestText(digest));
|
|
709
|
+
return 0;
|
|
710
|
+
}
|
|
711
|
+
const model = await connectEndpoint({
|
|
712
|
+
endpoint: String(args.flags.endpoint),
|
|
713
|
+
model: String(args.flags.model),
|
|
714
|
+
signal,
|
|
715
|
+
});
|
|
716
|
+
const embed = args.flags['check-support']
|
|
717
|
+
? createTextEmbedder(store, await inspectNative('embedding', c.nativeRoot, c.version))
|
|
718
|
+
: undefined;
|
|
719
|
+
const result = await createDigest(store, args.positional[0], model, { signal, ...(embed ? { embed } : {}) });
|
|
720
|
+
c.emit({ digest: result.derivation, duplicate: result.duplicate, ...(result.trace ? { trace: result.trace } : {}) }, digestText(result.derivation));
|
|
721
|
+
return 0;
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
name: 'review',
|
|
726
|
+
summary: 'Accept or reject digest fields',
|
|
727
|
+
usage: [['review DIGEST --accept SPEC --reject SPEC', 'Review fields: all, gist, idea:N, quote:N.M']],
|
|
728
|
+
positional: 1,
|
|
729
|
+
flags: { accept: 'value', reject: 'value' },
|
|
730
|
+
library: 'open',
|
|
731
|
+
check: (_, f) => !(f.accept === undefined && f.reject === undefined) &&
|
|
732
|
+
[f.accept, f.reject].every((v) => v === undefined || reviewSpec.test(String(v))),
|
|
733
|
+
handler: (c) => {
|
|
734
|
+
const { store, args } = c, prior = store.derivations.read('digest', args.positional[0]);
|
|
735
|
+
if (!prior)
|
|
736
|
+
throw new SpongeError('unavailable');
|
|
737
|
+
const digest = store.derivations.review('digest', prior.id, reviewFromSpec(prior, args.flags.accept, args.flags.reject));
|
|
738
|
+
c.emit({ digest }, digestText(digest));
|
|
739
|
+
return 0;
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
name: 'help',
|
|
744
|
+
summary: 'Show help for all commands or one',
|
|
745
|
+
usage: [['help [COMMAND]', 'Show help for all commands or one']],
|
|
746
|
+
positional: (p) => p.length <= 1,
|
|
747
|
+
flags: {},
|
|
748
|
+
library: 'none',
|
|
749
|
+
check: (p) => !p.length || commands.some((command) => command.name === p[0]),
|
|
750
|
+
handler: (c) => {
|
|
751
|
+
c.write(c.args.positional[0] ? commandHelp(c.args.positional[0]) : help());
|
|
752
|
+
return 0;
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: 'version',
|
|
757
|
+
summary: 'Print the version',
|
|
758
|
+
usage: [['--version', 'Print the version']],
|
|
759
|
+
positional: 0,
|
|
760
|
+
flags: {},
|
|
761
|
+
library: 'none',
|
|
762
|
+
handler: (c) => {
|
|
763
|
+
c.write(c.version + '\n');
|
|
764
|
+
return 0;
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
];
|
|
768
|
+
export const command = (name) => commands.find((c) => c.name === name);
|
|
769
|
+
const row = ([synopsis, description]) => !description
|
|
770
|
+
? ` ${synopsis}\n`
|
|
771
|
+
: synopsis.length < 30
|
|
772
|
+
? ` ${synopsis.padEnd(30)}${description}\n`
|
|
773
|
+
: synopsis.length < 60
|
|
774
|
+
? ` ${synopsis} ${description}\n`
|
|
775
|
+
: ` ${synopsis}\n${' '.repeat(32)}${description}\n`;
|
|
776
|
+
const notes = `Global flags: --library /absolute/path --json --debug (or SPONGE_DEBUG=1: print error causes to stderr)
|
|
777
|
+
Items are public by default only when captured from a public source.
|
|
778
|
+
Use -- before an option-like search query. Capture setup is explicit.
|
|
779
|
+
Digest endpoints must be literal loopback HTTP (127.0.0.1 or [::1]); Sponge never starts a model server.
|
|
780
|
+
Media: --language en enables local English ASR; --force-local bypasses captions.
|
|
781
|
+
These flags apply to --wait, run --drain, or worker serve.
|
|
782
|
+
worker install --language en saves the English ASR choice for background work.
|
|
783
|
+
For trusted local sources, use --allow-private-network with --wait or run.
|
|
784
|
+
`;
|
|
785
|
+
export function help() {
|
|
786
|
+
return `sponge — a local article library\n\n${commands.flatMap((c) => c.usage.map(row)).join('')}\n${notes}`;
|
|
787
|
+
}
|
|
788
|
+
/** One command's synopsis lines; also the hint printed for its usage errors. */
|
|
789
|
+
export const usageOf = (name) => command(name)
|
|
790
|
+
.usage.map(([synopsis]) => `sponge ${synopsis}`)
|
|
791
|
+
.join('\n');
|
|
792
|
+
export function commandHelp(name) {
|
|
793
|
+
const c = command(name), flags = Object.keys(c.flags);
|
|
794
|
+
return `sponge ${c.name} — ${c.summary}\n\nUsage:\n${c.usage.map(row).join('').replace(/^ /gm, ' sponge ')}${flags.length ? `\nFlags: ${flags.map((f) => '--' + f).join(' ')}\n` : ''}\nGlobal flags: --library /absolute/path --json --debug\n`;
|
|
795
|
+
}
|