@deslop/stash 0.1.1081 → 0.1.1083

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 CHANGED
@@ -19,7 +19,7 @@ The CLI saves the entry through the server's RPC, waits until the server has rea
19
19
  - **Media.** The server runs the `yt-dlp` on `PATH` and downloads the Whisper model into `$XDG_CACHE_HOME/deslop/media` on first use. Published captions win; Whisper `small.en` takes about a quarter of the audio's length, up to two hours of audio. TikTok, X, YouTube and Instagram links go through `yt-dlp`, also when their page could not be read.
20
20
  - **Proxy.** When `STASH_PROXY` holds an `http://` proxy URL, every request for a source exits through it: `yt-dlp`, pages, APIs, preview images and the reader. Without it, the server reads with its own address. `{session}` in the URL becomes a new random value for each `yt-dlp` run and each request, so a rotating residential proxy keeps one exit address while a run downloads. A request that a site blocks, or that the exit drops, retries twice from new exit addresses. On `dev` it is the owner's [DataImpulse](https://app.dataimpulse.com) residential plan "stash": pay-as-you-go traffic that never expires, with `__sessid.{session}` after the login.
21
21
  - **Sources.** Web pages become Markdown with their headings, lists and link targets. GitHub repositories give their README, issues and pull requests their comments, X posts their long-form articles, and TikTok photo posts every slide. A page that holds no text, such as a JavaScript app or a PDF, goes to the [Jina Reader](https://jina.ai/reader/), which also takes blocked pages; its bot-check pages count as failures.
22
- - **Storage.** SQLite at `stash.db` in `STASH_DATA_DIR`, one row per note, without migrations: a change to its shape can drop it. Previews and uploaded files sit beside it in `images/` and `files/`.
22
+ - **Storage.** SQLite at `stash.db` in `STASH_DATA_DIR`, one row per note holding its JSON. The notes on `dev` are production data: a change to the note's shape ships with a migration that converts the stored rows, never a reset. Previews and uploaded files sit beside it in `images/` and `files/`.
23
23
  - **Sync.** `notes.watch` sends every note's list fields once, then only the notes that changed, at most every 400 ms. `notes.get` loads the long texts and `notes.search` searches transcripts and pages on the server.
24
24
  - **Routes.** The app uses the JSON WebSocket RPC at `/api/rpc` and loads previews from `/api/images/<id>`. The share extension posts `{id, text}` to `/api/capture`. Photos and videos arrive as the raw body of `/api/files/<id>`.
25
25
  - **Organizer.** Claude reads each entry as a developer would and writes the notes the owner reads instead of the source: a plain title of at most 32 characters, a one-sentence summary, one to three tags, search topics, short Markdown notes of at most 10 lines (facts, steps, bold names and links, with headings only for long sources), and the words in the images. The preview, the photo post's slides and 12 evenly spaced video stills, up to 768 pixels wide, go with it. A short link resolves before saving, so another share of the same post finds the saved note.
@@ -39,10 +39,11 @@ On main, `deploy.yaml` compares `apps/stash`, `packages` and the lockfile with t
39
39
 
40
40
  ## Simulator
41
41
 
42
- CI builds the app for the iOS Simulator once per native fingerprint, as the artifact `stash-simulator-<fingerprint>` of the branch's CI run. The app holds the JavaScript of the commit that built it, so replace its bundle with the branch's own before you install it. `device_open` returns the `agent-device` command and its flags.
42
+ The Simulator workflow builds the app for the iOS Simulator once per native fingerprint, as the artifact `stash-simulator-<fingerprint>`; it runs only for a push that changes `apps/stash`, `packages` or the lockfile, so a branch without one reuses the artifact another branch built. The app holds the JavaScript of the commit that built it, so replace its bundle with the branch's own before you install it. `device_open` returns the `agent-device` command and its flags.
43
43
 
44
44
  ```bash
45
- gh run download <run id> --name stash-simulator-<fingerprint> --dir /tmp/stash-sim && tar -xzf /tmp/stash-sim/simulator.tar.gz -C /tmp/stash-sim
45
+ run=$(gh api "repos/MP281X/deslop/actions/artifacts?name=stash-simulator-<fingerprint>" --jq '.artifacts[0].workflow_run.id')
46
+ gh run download $run --name stash-simulator-<fingerprint> --dir /tmp/stash-sim && tar -xzf /tmp/stash-sim/simulator.tar.gz -C /tmp/stash-sim
46
47
  APP_VARIANT=development vp -C apps/stash exec expo export:embed --platform ios --dev false --bundle-output /tmp/stash-sim/stashdev.app/main.jsbundle --assets-dest /tmp/stash-sim/stashdev.app
47
48
  <agent-device> install xyz.mp281x.stash.dev /tmp/stash-sim/stashdev.app <flags>
48
49
  <agent-device> open xyz.mp281x.stash.dev <flags>
package/dist/cli.js CHANGED
@@ -7542,7 +7542,7 @@ const runWith = (command, config) => {
7542
7542
  };
7543
7543
  //#endregion
7544
7544
  //#region package.json
7545
- var version = "0.1.1081";
7545
+ var version = "0.1.1083";
7546
7546
  //#endregion
7547
7547
  //#region src/main.cli.ts
7548
7548
  function section(title, text) {
package/dist/server.js CHANGED
@@ -44281,7 +44281,7 @@ var Media = class Media extends Service()("@deslop/stash/services/media/service/
44281
44281
  path.join(work, "frame-%02d.jpg")
44282
44282
  ]);
44283
44283
  if (sampled.exitCode !== 0) return yield* MediaError.make({ message: `ffmpeg cannot sample the video: ${sampled.stderr}` });
44284
- return yield* forEach(sort(filter$4(yield* fs.readDirectory(work), startsWith("frame-")), String$3), (file) => fs.readFile(path.join(work, file)));
44284
+ return yield* forEach(sort(filter$4(yield* fs.readDirectory(work), startsWith("frame-")), String$3), (file) => fs.readFile(path.join(work, file)), { concurrency: "unbounded" });
44285
44285
  });
44286
44286
  return Media.of({
44287
44287
  fileFrames: fn$1("Media.fileFrames")(function* (file) {
@@ -91169,7 +91169,7 @@ const xThread = fnUntraced(function* (url) {
91169
91169
  const quote = fromNullishOr(status.quote);
91170
91170
  const media = flatMap$1([status, ...toArray$1(quote)], (post) => post.media?.all ?? []);
91171
91171
  const replies = conversation.replies ?? [];
91172
- const linked = yield* forEach(pipe(status.raw_text?.facets ?? [], filterMap((facet) => fromOption$1(pipe(fromNullishOr(facet.replacement), flatMap$2(match$2(/(?:x|twitter)\.com\/[^/]+\/status\/(\d+)/u)), flatMap$2((match) => get$6(match, 1))), () => void 0)), filter$4((linkedId) => linkedId !== status.id && !exists(quote, (post) => post.id === linkedId)), dedupe, take$1(5)), (linkedId) => option$1(map$1(decode$1(FxStatus, new URL(`https://api.fxtwitter.com/2/status/${linkedId}`)), (found) => found.status)));
91172
+ const linked = yield* forEach(pipe(status.raw_text?.facets ?? [], filterMap((facet) => fromOption$1(pipe(fromNullishOr(facet.replacement), flatMap$2(match$2(/(?:x|twitter)\.com\/[^/]+\/status\/(\d+)/u)), flatMap$2((match) => get$6(match, 1))), () => void 0)), filter$4((linkedId) => linkedId !== status.id && !exists(quote, (post) => post.id === linkedId)), dedupe, take$1(5)), (linkedId) => option$1(map$1(decode$1(FxStatus, new URL(`https://api.fxtwitter.com/2/status/${linkedId}`)), (found) => found.status)), { concurrency: "unbounded" });
91173
91173
  return Extraction.make({
91174
91174
  author: status.author.name,
91175
91175
  image: map$4(head(media), (item) => item.thumbnail_url ?? item.url),
@@ -91229,7 +91229,7 @@ const issue = fnUntraced(function* (url, repository, number) {
91229
91229
  });
91230
91230
  const repository = fnUntraced(function* (url, name) {
91231
91231
  const found = yield* decode$1(GitHubRepo, new URL(`https://api.github.com/repos/${name}`));
91232
- const readme = yield* pipe(decode$1(Struct({ download_url: String$1 }), new URL(`https://api.github.com/repos/${name}/readme`)), flatMap((file) => follow(new URL(file.download_url), api, 0)), map$1((file) => file.body), orElseSucceed(() => ""));
91232
+ const readme = yield* pipe(decode$1(Struct({ download_url: String$1 }), new URL(`https://api.github.com/repos/${name}/readme`)), flatMap((file) => follow(new URL(file.download_url), api, 0)), map$1((file) => file.body), option$1);
91233
91233
  return Extraction.make({
91234
91234
  author: found.owner.login,
91235
91235
  image: some$1(`https://opengraph.githubassets.com/1/${name}`),
@@ -91240,7 +91240,7 @@ const repository = fnUntraced(function* (url, name) {
91240
91240
  onSome: (topics) => `Topics: ${join$1(topics, ", ")}`
91241
91241
  })),
91242
91242
  found.homepage ?? "",
91243
- readme
91243
+ getOrElse(readme, () => "")
91244
91244
  ], filter$4(isNonEmpty$1), join$1("\n\n")),
91245
91245
  title: found.full_name,
91246
91246
  url: canonical(url)
@@ -102110,13 +102110,13 @@ const openStore = fnUntraced(function* (path) {
102110
102110
  const upsert = database.prepare("insert into notes (id, created_at, json) values (?, ?, ?) on conflict (id) do update set created_at = excluded.created_at, json = excluded.json");
102111
102111
  const remove = database.prepare("delete from notes where id = ?");
102112
102112
  return {
102113
- load: pipe(try_(() => database.prepare("select json from notes order by created_at desc").all()), flatMap(forEach((row) => decodeUnknownEffect(NoteJson)(row["json"]))), failed),
102113
+ load: pipe(try_(() => database.prepare("select json from notes order by created_at desc").all()), flatMap(forEach((row) => decodeUnknownEffect(NoteJson)(row["json"]), { concurrency: "unbounded" })), failed),
102114
102114
  save: fnUntraced(function* (previous, next) {
102115
102115
  const changes = diff(previous, next);
102116
102116
  const rows = yield* pipe(forEach(changes.upserted, (note) => map$1(encodeEffect(NoteJson)(note), (json) => ({
102117
102117
  json,
102118
102118
  note
102119
- }))), failed);
102119
+ })), { concurrency: "unbounded" }), failed);
102120
102120
  yield* pipe(try_(() => {
102121
102121
  database.exec("begin");
102122
102122
  try {
@@ -102212,7 +102212,7 @@ var Notes = class Notes extends Service()("@deslop/stash/services/notes/service/
102212
102212
  const pictures = [...yield* pipe(forEach(toArray$1(fromNullishOr(note.image)), (type) => map$1(fs.readFile(path.join(images, id)), (bytes) => ({
102213
102213
  bytes,
102214
102214
  type
102215
- }))), optional("The preview could not be read", empty$11)), ...stills];
102215
+ })), { concurrency: "unbounded" }), optional("The preview could not be read", empty$11)), ...stills];
102216
102216
  if (isEmpty$2(note.content) && isEmpty$2(note.transcript ?? "") && isEmpty$2(commentFor(note.text)) && isReadonlyArrayEmpty(pictures)) return yield* updateNote(id, (item) => ({
102217
102217
  ...item,
102218
102218
  issue: match(isEmpty$2(item.issue), {
@@ -102246,8 +102246,11 @@ var Notes = class Notes extends Service()("@deslop/stash/services/notes/service/
102246
102246
  }));
102247
102247
  }
102248
102248
  const keepImages = fnUntraced(function* (note, page) {
102249
- yield* pipe(forEach(toArray$1(filter$5(page.image, () => note.image === void 0)), (image) => flatMap(sources.image(image), (bytes) => storeImage(note.id, bytes)), { discard: true }), skip("The preview image was not kept"));
102250
- const slides = yield* forEach(page.slides ?? [], (image) => pipe(sources.image(image), asSome, optional("A slide was not read", none)));
102249
+ yield* pipe(forEach(toArray$1(filter$5(page.image, () => note.image === void 0)), (image) => flatMap(sources.image(image), (bytes) => storeImage(note.id, bytes)), {
102250
+ concurrency: "unbounded",
102251
+ discard: true
102252
+ }), skip("The preview image was not kept"));
102253
+ const slides = yield* forEach(page.slides ?? [], (image) => pipe(sources.image(image), asSome, optional("A slide was not read", none)), { concurrency: 1 });
102251
102254
  return getSomes(slides);
102252
102255
  });
102253
102256
  const organizeLink = fnUntraced(function* (note, link) {
@@ -102338,7 +102341,10 @@ var Notes = class Notes extends Service()("@deslop/stash/services/notes/service/
102338
102341
  onTrue: () => void_
102339
102342
  })), (effect) => run$1(runs, note.id, effect), asVoid);
102340
102343
  }
102341
- yield* forEach(filter$4(yield* get$1(state), (note) => note.status === "organizing"), start, { discard: true });
102344
+ yield* forEach(filter$4(yield* get$1(state), (note) => note.status === "organizing"), start, {
102345
+ concurrency: 1,
102346
+ discard: true
102347
+ });
102342
102348
  const missing = mapError(() => NotesError.make({ message: "This note no longer exists." }));
102343
102349
  const receive = fnUntraced(function* (input) {
102344
102350
  const stored = path.join(match(isVideo(input.type), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deslop/stash",
3
- "version": "0.1.1081",
3
+ "version": "0.1.1083",
4
4
  "description": "A personal inbox for links and notes: the server, the iOS app, and a CLI that saves to it.",
5
5
  "repository": {
6
6
  "type": "git",