@hraness/sponge 0.4.1 → 0.5.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 +17 -16
- package/dist/src/acquisition/capture.d.ts +1 -4
- package/dist/src/acquisition/capture.js +12 -23
- package/dist/src/acquisition/clip-helper.js +14 -0
- package/dist/src/acquisition/errors.d.ts +2 -1
- package/dist/src/acquisition/errors.js +4 -33
- package/dist/src/acquisition/lock.d.ts +1 -1
- package/dist/src/acquisition/lock.js +1 -1
- package/dist/src/acquisition/pins.d.ts +1 -1
- package/dist/src/acquisition/pins.js +7 -13
- package/dist/src/acquisition/process.d.ts +1 -1
- package/dist/src/acquisition/process.js +1 -2
- package/dist/src/acquisition/runtime.d.ts +0 -4
- package/dist/src/acquisition/runtime.js +12 -18
- package/dist/src/app/source.d.ts +2 -0
- package/dist/src/app/source.js +18 -0
- package/dist/src/cli/commands.js +28 -18
- package/dist/src/cli/config.d.ts +2 -1
- package/dist/src/cli/config.js +8 -8
- package/dist/src/cli/runtimes.js +1 -1
- package/dist/src/cli/worker.js +13 -10
- package/dist/src/errors.d.ts +1 -0
- package/dist/src/errors.js +2 -0
- package/dist/src/media/capture.d.ts +6 -2
- package/dist/src/media/capture.js +33 -42
- package/dist/src/models/catalog.d.ts +4 -2
- package/dist/src/models/catalog.js +39 -20
- package/dist/src/models/runtime.d.ts +11 -3
- package/dist/src/models/runtime.js +18 -11
- package/dist/src/pdf/capture-helper.js +6 -5
- package/dist/src/pdf/capture.d.ts +0 -4
- package/dist/src/pdf/capture.js +2 -14
- package/dist/src/pdf/errors.js +1 -3
- package/dist/src/worker/basic.d.ts +1 -1
- package/dist/src/worker/basic.js +5 -3
- package/native/runtime-manifest.json +14 -6
- package/package.json +1 -1
- package/dist/src/media/capture-helper.js +0 -21
- /package/dist/src/{media/capture-helper.d.ts → acquisition/clip-helper.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sponge
|
|
2
2
|
|
|
3
|
-
Sponge is a local library for articles, images, PDFs and media transcripts. Capturing runs on your machine through
|
|
3
|
+
Sponge is a local library for articles, images, PDFs and media transcripts. Capturing runs on your machine through Wordcell and Sponge's media helper; saved
|
|
4
4
|
content stays in a private SQLite database and adjacent hashed asset files.
|
|
5
5
|
|
|
6
6
|
Version 0.2.0 introduces the local-library CLI; earlier 0.1.x commands are inactive.
|
|
@@ -34,8 +34,9 @@ revision and preserves the old one if capture fails. `--json` provides structure
|
|
|
34
34
|
output. Errors are categorical JSON on stderr; exit codes are 0 success,
|
|
35
35
|
1 failure, 2 invalid input, 3 partial content, 4 action required, and 130 cancelled.
|
|
36
36
|
|
|
37
|
-
Capture setup explicitly downloads pinned
|
|
38
|
-
|
|
37
|
+
Capture setup explicitly downloads pinned Wordcell and Bun artifacts plus frozen
|
|
38
|
+
dependencies, and on macOS 26 on Apple Silicon Sponge's media helper from the
|
|
39
|
+
Sponge release. Read/search never download runtimes or contact source
|
|
39
40
|
websites. Setup reports disk requirements, a monitored 2 GiB temporary installation
|
|
40
41
|
limit, a 1 GiB retained runtime limit and a 30-minute dependency installation
|
|
41
42
|
deadline. Temporary dependency caches are removed
|
|
@@ -59,8 +60,8 @@ node bin/sponge.js add https://example.com/episode --kind media --wait
|
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
PDF capture requires Poppler (`pdfinfo` and `pdftohtml`). Tesseract enables
|
|
62
|
-
optional local OCR.
|
|
63
|
-
|
|
63
|
+
optional local OCR. Media capture runs Sponge's native media helper, which uses
|
|
64
|
+
yt-dlp and FFmpeg (ffprobe) from your PATH or Homebrew. Install these separately, for example with
|
|
64
65
|
`brew install poppler tesseract yt-dlp ffmpeg` on macOS. Capture setup does not
|
|
65
66
|
install system tools.
|
|
66
67
|
|
|
@@ -86,6 +87,7 @@ that changes them reports `runtime-missing` until setup runs again.
|
|
|
86
87
|
```sh
|
|
87
88
|
sponge setup --asr
|
|
88
89
|
sponge add https://example.com/episode --kind media --wait --language en
|
|
90
|
+
sponge add https://youtu.be/VIDEO_ID --wait # YouTube, Vimeo, SoundCloud and audio or video files are media without --kind
|
|
89
91
|
sponge refresh ITEM --wait --language en --force-local
|
|
90
92
|
sponge setup --embedding
|
|
91
93
|
sponge index create
|
|
@@ -204,21 +206,20 @@ HTTPS. See [the decision record](docs/decisions/publish-feed.md).
|
|
|
204
206
|
### Selected authenticated connections
|
|
205
207
|
|
|
206
208
|
The initial development interface accepts private `connections.json` in the
|
|
207
|
-
library (mode `0600`). It
|
|
208
|
-
never cookies or tokens.
|
|
209
|
+
library (mode `0600`). It names a signed-in browser you attached over the Chrome
|
|
210
|
+
DevTools Protocol on loopback, never cookies or tokens.
|
|
209
211
|
|
|
210
212
|
```json
|
|
211
|
-
{"schemaVersion":1,"connections":[{"id":"personal","kind":"
|
|
213
|
+
{"schemaVersion":1,"connections":[{"id":"personal","kind":"cdp","endpoint":"http://127.0.0.1:9222"}]}
|
|
212
214
|
```
|
|
213
215
|
|
|
214
|
-
Use `add URL --connection personal --wait
|
|
215
|
-
selects your browser
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
Changing connection identity creates a distinct private source realm.
|
|
216
|
+
Use `add URL --connection personal --wait` for an article. A public capture never
|
|
217
|
+
silently selects your browser, and an attached browser stays open. PDF and media
|
|
218
|
+
capture are public only. Remote PDF acquisition requires public network addresses;
|
|
219
|
+
`--allow-private-network` is not supported for PDF capture. Saved GhostGet account
|
|
220
|
+
connections from earlier versions are retired: their items keep their realm, and a
|
|
221
|
+
capture through one fails with reason `connection-retired`. Changing connection
|
|
222
|
+
identity creates a distinct private source realm.
|
|
222
223
|
|
|
223
224
|
## Design and validation
|
|
224
225
|
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import type { CaptureResult, ErrorEnvelope } from '../../contracts/index.js';
|
|
2
2
|
import { runOwned } from './process.js';
|
|
3
3
|
import { type CaptureRuntime } from './runtime.js';
|
|
4
|
+
/** A signed-in browser the user attached over CDP on loopback. */
|
|
4
5
|
export type CaptureConnection = {
|
|
5
|
-
kind: 'account';
|
|
6
|
-
stateHome: string;
|
|
7
|
-
accountId: string;
|
|
8
|
-
} | {
|
|
9
6
|
kind: 'cdp';
|
|
10
7
|
endpoint: string;
|
|
11
8
|
};
|
|
@@ -6,7 +6,7 @@ import { privateRoot, physicalBytes } from './files.js';
|
|
|
6
6
|
import { runOwned, isolatedEnvironment } from './process.js';
|
|
7
7
|
import { inspectCaptureRuntime } from './runtime.js';
|
|
8
8
|
import { settleCaptureRoot } from './cleanup.js';
|
|
9
|
-
import { failure,
|
|
9
|
+
import { failure, clipFailure } from './errors.js';
|
|
10
10
|
import { normalizeBundle, object } from './normalize.js';
|
|
11
11
|
import { SpongeError, publicCode, codeOf } from '../errors.js';
|
|
12
12
|
export async function captureArticle(request) {
|
|
@@ -44,9 +44,10 @@ export async function captureArticle(request) {
|
|
|
44
44
|
const deadline = request.deadlineMs ?? limits.captureDeadlineMs;
|
|
45
45
|
if (!Number.isSafeInteger(deadline) || deadline < 100 || deadline > limits.captureDeadlineMs)
|
|
46
46
|
return failure(requestId, 'invalid-input');
|
|
47
|
+
const clipHelper = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './clip-helper.ts' : './clip-helper.js', import.meta.url));
|
|
47
48
|
const args = [
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
clipHelper,
|
|
50
|
+
runtime.root,
|
|
50
51
|
request.sourceUrl,
|
|
51
52
|
'article',
|
|
52
53
|
'--media',
|
|
@@ -64,28 +65,18 @@ export async function captureArticle(request) {
|
|
|
64
65
|
];
|
|
65
66
|
if (request.allowPrivateNetwork)
|
|
66
67
|
args.push('--allow-private-network');
|
|
67
|
-
|
|
68
|
-
if (request.connection?.kind === 'account') {
|
|
69
|
-
if (!/^[a-zA-Z0-9_.-]{1,128}$/.test(request.connection.accountId))
|
|
70
|
-
return failure(requestId, 'invalid-input');
|
|
71
|
-
try {
|
|
72
|
-
selectedState = await privateRoot(request.connection.stateHome);
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return failure(requestId, 'repair-auth');
|
|
76
|
-
}
|
|
77
|
-
args.push('--auth', request.connection.accountId, '--mode', 'http');
|
|
78
|
-
}
|
|
79
|
-
else if (request.connection?.kind === 'cdp') {
|
|
68
|
+
if (request.connection) {
|
|
80
69
|
if (!/^(?:[1-9][0-9]{0,4}|(?:http|ws):\/\/127\.0\.0\.1:[1-9][0-9]{0,4}(?:\/[^\s]*)?)$/.test(request.connection.endpoint))
|
|
81
70
|
return failure(requestId, 'invalid-input');
|
|
82
|
-
|
|
71
|
+
// Wordcell takes the loopback DevTools port alone.
|
|
72
|
+
const endpoint = request.connection.endpoint;
|
|
73
|
+
args.push('--cdp', /^[0-9]+$/.test(endpoint) ? endpoint : new URL(endpoint).port);
|
|
83
74
|
}
|
|
84
75
|
else
|
|
85
76
|
args.push('--mode', 'http');
|
|
86
77
|
const root = await realpath(await mkdtemp(join(base, 'capture-'))), home = join(root, 'home'), output = join(root, 'bundles');
|
|
87
78
|
args.push('--output', output);
|
|
88
|
-
const env = isolatedEnvironment(home, home
|
|
79
|
+
const env = isolatedEnvironment(home, home);
|
|
89
80
|
const controller = new AbortController();
|
|
90
81
|
const abort = () => controller.abort();
|
|
91
82
|
signal?.addEventListener('abort', abort, { once: true });
|
|
@@ -127,7 +118,7 @@ export async function captureArticle(request) {
|
|
|
127
118
|
graceMs: 5000,
|
|
128
119
|
});
|
|
129
120
|
cleanup = result.cleanup;
|
|
130
|
-
//
|
|
121
|
+
// An attached browser can start helpers outside the owned process group.
|
|
131
122
|
let summary;
|
|
132
123
|
try {
|
|
133
124
|
summary = JSON.parse(result.stdout);
|
|
@@ -135,10 +126,8 @@ export async function captureArticle(request) {
|
|
|
135
126
|
catch {
|
|
136
127
|
summary = null;
|
|
137
128
|
}
|
|
138
|
-
const upstream =
|
|
139
|
-
if (
|
|
140
|
-
cleanup = 'pending';
|
|
141
|
-
if (cleanup === 'settled' && request.connection?.kind === 'cdp') {
|
|
129
|
+
const upstream = clipFailure(summary, requestId);
|
|
130
|
+
if (cleanup === 'settled' && request.connection) {
|
|
142
131
|
cleanup = 'pending';
|
|
143
132
|
const census = await (request.runProcess ?? runOwned)('/bin/ps', ['-axo', 'command'], {
|
|
144
133
|
env,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Executed only by the pinned Bun runtime. The import is Wordcell's public clip entry point.
|
|
2
|
+
const runtime = process.argv[2];
|
|
3
|
+
try {
|
|
4
|
+
const { main } = await import(import.meta.resolve('@hraness/wordcell/clip/cli', `${runtime}/package.json`));
|
|
5
|
+
process.exitCode = await main(process.argv.slice(3), process.env, {
|
|
6
|
+
stdout: (value) => process.stdout.write(value),
|
|
7
|
+
stderr: (value) => process.stderr.write(value),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
process.stdout.write(JSON.stringify({ ok: false }));
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { ErrorCode, ErrorEnvelope } from '../../contracts/index.js';
|
|
2
2
|
export declare const failure: (requestId: string, code: ErrorCode, phase?: ErrorEnvelope["phase"], retryable?: boolean) => ErrorEnvelope;
|
|
3
|
-
|
|
3
|
+
/** Wordcell's clip answers with a JSON summary; prose is never classified. A summary that is not success means the source gave no usable capture. */
|
|
4
|
+
export declare function clipFailure(value: unknown, requestId: string): ErrorEnvelope;
|
|
@@ -1,35 +1,6 @@
|
|
|
1
|
-
const mappings = {
|
|
2
|
-
unknown: 'unclassified',
|
|
3
|
-
'permission-required': 'grant-permission',
|
|
4
|
-
'auth-required': 'repair-auth',
|
|
5
|
-
'invalid-input': 'invalid-input',
|
|
6
|
-
cancelled: 'cancelled',
|
|
7
|
-
timeout: 'unavailable',
|
|
8
|
-
transient: 'unavailable',
|
|
9
|
-
unsupported: 'unsupported',
|
|
10
|
-
'cleanup-pending': 'cleanup-required',
|
|
11
|
-
'rate-limited': 'rate-limited',
|
|
12
|
-
};
|
|
13
1
|
export const failure = (requestId, code, phase = 'acquisition', retryable = false) => ({ schemaVersion: 1, requestId, ok: false, code, phase, retryable });
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const outer = value;
|
|
19
|
-
if (outer.ok !== false)
|
|
20
|
-
return fallback;
|
|
21
|
-
const raw = outer.captureError;
|
|
22
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
23
|
-
return fallback;
|
|
24
|
-
const typed = raw;
|
|
25
|
-
if (Object.keys(typed).some((k) => !['schemaVersion', 'code', 'retryable'].includes(k)) ||
|
|
26
|
-
typed.schemaVersion !== 1 ||
|
|
27
|
-
typeof typed.code !== 'string' ||
|
|
28
|
-
!Object.hasOwn(mappings, typed.code) ||
|
|
29
|
-
typeof typed.retryable !== 'boolean')
|
|
30
|
-
return fallback;
|
|
31
|
-
const code = mappings[typed.code];
|
|
32
|
-
if (typed.retryable && !['timeout', 'transient', 'rate-limited'].includes(typed.code))
|
|
33
|
-
return fallback;
|
|
34
|
-
return failure(requestId, code, code === 'cleanup-required' ? 'cleanup' : 'acquisition', typed.retryable);
|
|
2
|
+
/** Wordcell's clip answers with a JSON summary; prose is never classified. A summary that is not success means the source gave no usable capture. */
|
|
3
|
+
export function clipFailure(value, requestId) {
|
|
4
|
+
const answered = !!value && typeof value === 'object' && !Array.isArray(value) && 'ok' in value;
|
|
5
|
+
return failure(requestId, answered ? 'unavailable' : 'unclassified');
|
|
35
6
|
}
|