@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.
Files changed (39) hide show
  1. package/README.md +17 -16
  2. package/dist/src/acquisition/capture.d.ts +1 -4
  3. package/dist/src/acquisition/capture.js +12 -23
  4. package/dist/src/acquisition/clip-helper.js +14 -0
  5. package/dist/src/acquisition/errors.d.ts +2 -1
  6. package/dist/src/acquisition/errors.js +4 -33
  7. package/dist/src/acquisition/lock.d.ts +1 -1
  8. package/dist/src/acquisition/lock.js +1 -1
  9. package/dist/src/acquisition/pins.d.ts +1 -1
  10. package/dist/src/acquisition/pins.js +7 -13
  11. package/dist/src/acquisition/process.d.ts +1 -1
  12. package/dist/src/acquisition/process.js +1 -2
  13. package/dist/src/acquisition/runtime.d.ts +0 -4
  14. package/dist/src/acquisition/runtime.js +12 -18
  15. package/dist/src/app/source.d.ts +2 -0
  16. package/dist/src/app/source.js +18 -0
  17. package/dist/src/cli/commands.js +28 -18
  18. package/dist/src/cli/config.d.ts +2 -1
  19. package/dist/src/cli/config.js +8 -8
  20. package/dist/src/cli/runtimes.js +1 -1
  21. package/dist/src/cli/worker.js +13 -10
  22. package/dist/src/errors.d.ts +1 -0
  23. package/dist/src/errors.js +2 -0
  24. package/dist/src/media/capture.d.ts +6 -2
  25. package/dist/src/media/capture.js +33 -42
  26. package/dist/src/models/catalog.d.ts +4 -2
  27. package/dist/src/models/catalog.js +39 -20
  28. package/dist/src/models/runtime.d.ts +11 -3
  29. package/dist/src/models/runtime.js +18 -11
  30. package/dist/src/pdf/capture-helper.js +6 -5
  31. package/dist/src/pdf/capture.d.ts +0 -4
  32. package/dist/src/pdf/capture.js +2 -14
  33. package/dist/src/pdf/errors.js +1 -3
  34. package/dist/src/worker/basic.d.ts +1 -1
  35. package/dist/src/worker/basic.js +5 -3
  36. package/native/runtime-manifest.json +14 -6
  37. package/package.json +1 -1
  38. package/dist/src/media/capture-helper.js +0 -21
  39. /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 Ghostget; saved
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 Ghostget, Wordcell and Bun artifacts
38
- plus frozen dependencies. Read/search never download runtimes or contact source
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. Provider media acquisition requires yt-dlp; audio validation
63
- requires ffprobe from FFmpeg. Install these separately, for example with
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 contains references to existing Ghostget accounts,
208
- never cookies or tokens. A future setup phase adds connection-management commands.
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":"account","stateHome":"/absolute/ghostget-auth","accountId":"personal"}]}
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`. A public capture never silently
215
- selects your browser or account. For articles, attached browser connections use
216
- `kind: "cdp"` and an explicitly selected local `endpoint`; attached browsers remain
217
- open. PDF and media capture support selected Ghostget accounts, not CDP. Remote
218
- PDF acquisition requires public network addresses; `--allow-private-network` is
219
- not supported for PDF capture. Live authenticated remote-PDF qualification remains
220
- pending; failures never fall back to a different account or a public request.
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, upstreamFailure } from './errors.js';
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
- runtime.ghostget,
49
- 'clip',
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
- let selectedState;
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
- args.push('--cdp', request.connection.endpoint);
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, selectedState ?? join(home, 'state'));
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
- // Ghostget reports escaped browser custody separately from generic descendants.
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 = upstreamFailure(summary, requestId);
139
- if (upstream.code === 'cleanup-required')
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
- export declare function upstreamFailure(value: unknown, requestId: string): ErrorEnvelope;
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
- export function upstreamFailure(value, requestId) {
15
- const fallback = failure(requestId, 'unclassified');
16
- if (!value || typeof value !== 'object' || Array.isArray(value))
17
- return fallback;
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
  }