@code-yeongyu/senpi 2026.10.23-1 → 2026.10.24-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 (49) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/dist/bundle/chunks/{chunk-TFFJEEHQ.js → chunk-3PTVLFBA.js} +1 -1
  3. package/dist/bundle/chunks/{chunk-52DQGTJR.js → chunk-CXED7XG5.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-TCWZBUBG.js → chunk-FKXOOQMF.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-7R3FUPMO.js → chunk-OS42QW66.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-NYYDXANA.js → chunk-QEHPPMVF.js} +2 -2
  7. package/dist/bundle/chunks/{chunk-TFULKHNU.js → chunk-T75A2MVF.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-E6IDMANN.js → chunk-XE753Y6U.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-RWRICRZB.js → chunk-ZSU4SPDQ.js} +80 -80
  10. package/dist/bundle/chunks/{cli-main-FVJRIDEL.js → cli-main-DQBJUCQS.js} +1 -1
  11. package/dist/bundle/chunks/{host-session-command-I7W5WSGN.js → host-session-command-MMYPIHCX.js} +1 -1
  12. package/dist/bundle/chunks/{interactive-mode-2GTQWY5I.js → interactive-mode-KIYNWS2N.js} +1 -1
  13. package/dist/bundle/chunks/{multi-session-host-4DDNG3S4.js → multi-session-host-X2XCJ7BZ.js} +4 -4
  14. package/dist/bundle/chunks/{package-manager-cli-VMY6B2WQ.js → package-manager-cli-UQMDWNHH.js} +1 -1
  15. package/dist/bundle/chunks/{rpc-mode-CN5HBSB3.js → rpc-mode-I7CRGV4X.js} +1 -1
  16. package/dist/bundle/chunks/{schedule-command-I3IYHTWW.js → schedule-command-6LF6KHSF.js} +1 -1
  17. package/dist/bundle/chunks/{session-control-endpoint-W3EL4JYJ.js → session-control-endpoint-VV6PUFSF.js} +1 -1
  18. package/dist/bundle/chunks/{session-picker-HES2YE6K.js → session-picker-QZZ3DJER.js} +1 -1
  19. package/dist/bundle/chunks/session-worker.js +143 -143
  20. package/dist/bundle/cli.js +2 -2
  21. package/dist/bundle/index.js +1 -1
  22. package/dist/bundle/rpc-entry.js +1 -1
  23. package/dist/bundle/runtime-manifest.json +1 -1
  24. package/dist/core/lifecycle-ledger.d.ts +3 -0
  25. package/dist/core/lifecycle-ledger.js +18 -1
  26. package/dist/core/lifecycle-session-binding.js +1 -0
  27. package/dist/core/session-holders.js +2 -1
  28. package/dist/core/session-lifecycle.d.ts +3 -0
  29. package/dist/core/session-lifecycle.js +11 -1
  30. package/dist/core/session-move/export.d.ts +3 -3
  31. package/dist/core/session-move/export.js +32 -27
  32. package/dist/core/session-move/extension-state.d.ts +5 -5
  33. package/dist/core/session-move/extension-state.js +25 -25
  34. package/dist/core/session-move/finish.d.ts +1 -1
  35. package/dist/core/session-move/finish.js +14 -13
  36. package/dist/core/session-move/import.js +37 -36
  37. package/dist/core/session-move/move-ledger.d.ts +3 -3
  38. package/dist/core/session-move/move-ledger.js +7 -6
  39. package/dist/core/session-move/requeue.js +1 -1
  40. package/dist/core/session-move/revoke.d.ts +1 -1
  41. package/dist/core/session-move/revoke.js +5 -5
  42. package/dist/core/session-move/session-lock.d.ts +4 -3
  43. package/dist/core/session-move/session-lock.js +23 -9
  44. package/dist/core/session-rebind.d.ts +1 -1
  45. package/dist/core/session-rebind.js +61 -31
  46. package/dist/modes/rpc/session-move-commands.js +16 -15
  47. package/dist/modes/rpc/session-release.js +3 -3
  48. package/docs/release-guide.md +1 -1
  49. package/package.json +6 -6
@@ -1,14 +1,14 @@
1
- import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
2
- import { readFile, writeFile } from "node:fs/promises";
1
+ import { cp, mkdir, open, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
2
  import { basename, dirname, join } from "node:path";
4
3
  import { normalizePath, resolvePath } from "../utils/paths.js";
5
4
  import { compareRepositoryIdentities, parseRepositoryIdentity, REPOSITORY_IDENTITY_ENTRY_TYPE, readRepositoryIdentity, } from "./repository-identity.js";
6
5
  import { withSessionMoveLock } from "./session-holders.js";
7
- import { getDefaultSessionDir, loadEntriesFromFile } from "./session-manager.js";
6
+ import { getDefaultSessionDir, loadEntriesFromFileAsync } from "./session-manager.js";
8
7
  import { encodedSessionId } from "./session-sidecar-store.js";
9
- export function readRecordedRepositoryIdentity(sessionFile) {
8
+ import { pathExists } from "./tools/path-utils.js";
9
+ export async function readRecordedRepositoryIdentity(sessionFile) {
10
10
  let recorded;
11
- for (const entry of loadEntriesFromFile(sessionFile)) {
11
+ for (const entry of await loadEntriesFromFileAsync(sessionFile)) {
12
12
  if (entry.type === "custom" && entry.customType === REPOSITORY_IDENTITY_ENTRY_TYPE) {
13
13
  recorded = parseRepositoryIdentity(entry.data) ?? recorded;
14
14
  }
@@ -23,12 +23,12 @@ export function readRecordedRepositoryIdentity(sessionFile) {
23
23
  export async function classifySessionRepository(sessionFile, sessionCwd, cwd, read = readRepositoryIdentity) {
24
24
  const [current, live] = await Promise.all([
25
25
  read(cwd),
26
- existsSync(sessionCwd) ? read(sessionCwd) : Promise.resolve(undefined),
26
+ pathExists(sessionCwd).then((exists) => (exists ? read(sessionCwd) : undefined)),
27
27
  ]);
28
- return compareRepositoryIdentities(live ?? readRecordedRepositoryIdentity(sessionFile), current);
28
+ return compareRepositoryIdentities(live ?? (await readRecordedRepositoryIdentity(sessionFile)), current);
29
29
  }
30
30
  export async function readSessionCwd(sessionFile) {
31
- const { cwd } = (await readHeaderLine(resolvePath(sessionFile))).header;
31
+ const { cwd } = await readHeader(resolvePath(sessionFile));
32
32
  return typeof cwd === "string" && cwd !== "" ? cwd : undefined;
33
33
  }
34
34
  /**
@@ -46,7 +46,7 @@ export async function rebindSessionFile(sourcePath, targetCwd, sessionDir, optio
46
46
  const cwd = resolvePath(targetCwd);
47
47
  const targetDir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
48
48
  const target = join(targetDir, basename(source));
49
- if (target !== source && !existsSync(source) && existsSync(target))
49
+ if (target !== source && (await movedAlready(source, target)))
50
50
  return target;
51
51
  const header = await readHeaderBeforeLock(source, target);
52
52
  if (header === undefined)
@@ -60,36 +60,66 @@ export async function rebindSessionFile(sourcePath, targetCwd, sessionDir, optio
60
60
  // missing source next to an existing target means the move is already done (`undefined`).
61
61
  async function readHeaderBeforeLock(source, target) {
62
62
  try {
63
- return (await readHeaderLine(source)).header;
63
+ return await readHeader(source);
64
64
  }
65
65
  catch (error) {
66
- if (error.code === "ENOENT" && target !== source && existsSync(target))
66
+ if (error.code === "ENOENT" && target !== source && (await pathExists(target)))
67
67
  return undefined;
68
68
  throw error;
69
69
  }
70
70
  }
71
71
  async function moveSessionFile(source, target, cwd) {
72
- if (target !== source && !existsSync(source) && existsSync(target))
72
+ if (target !== source && (await movedAlready(source, target)))
73
73
  return target;
74
74
  const { header, rest } = await readHeaderLine(source);
75
- if (target !== source && existsSync(target)) {
75
+ if (target !== source && (await pathExists(target))) {
76
76
  throw new Error(`Cannot rebind session: ${target} already exists`);
77
77
  }
78
78
  const targetDir = dirname(target);
79
- mkdirSync(targetDir, { recursive: true });
79
+ await mkdir(targetDir, { recursive: true });
80
80
  const temp = join(targetDir, `.${basename(source)}.rebind-${process.pid}.tmp`);
81
81
  await writeFile(temp, `${JSON.stringify({ ...header, cwd })}\n${rest}`, { flag: "wx" });
82
82
  if (target !== source)
83
- moveSessionSidecars(dirname(source), targetDir, header.id);
84
- renameSync(temp, target);
83
+ await moveSessionSidecars(dirname(source), targetDir, header.id);
84
+ await rename(temp, target);
85
85
  if (target !== source)
86
- rmSync(source, { force: true });
86
+ await rm(source, { force: true });
87
87
  return target;
88
88
  }
89
+ /** A concurrent rebind writes the target before it removes the source: no source next to a target means done. */
90
+ async function movedAlready(source, target) {
91
+ return !(await pathExists(source)) && (await pathExists(target));
92
+ }
89
93
  async function readHeaderLine(sessionFile) {
90
94
  const content = await readFile(sessionFile, "utf8");
91
95
  const newline = content.indexOf("\n");
92
96
  const firstLine = newline === -1 ? content : content.slice(0, newline);
97
+ return { header: parseHeader(firstLine, sessionFile), rest: newline === -1 ? "" : content.slice(newline + 1) };
98
+ }
99
+ const HEADER_CHUNK_BYTES = 64 * 1024;
100
+ /** The header alone: reads up to the first newline, never the transcript after it. */
101
+ async function readHeader(sessionFile) {
102
+ const handle = await open(sessionFile, "r");
103
+ try {
104
+ const chunks = [];
105
+ const buffer = Buffer.allocUnsafe(HEADER_CHUNK_BYTES);
106
+ while (true) {
107
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
108
+ if (bytesRead === 0)
109
+ break;
110
+ const chunk = buffer.subarray(0, bytesRead);
111
+ const newline = chunk.indexOf(0x0a);
112
+ chunks.push(Buffer.from(newline === -1 ? chunk : chunk.subarray(0, newline)));
113
+ if (newline !== -1)
114
+ break;
115
+ }
116
+ return parseHeader(Buffer.concat(chunks).toString("utf8"), sessionFile);
117
+ }
118
+ finally {
119
+ await handle.close();
120
+ }
121
+ }
122
+ function parseHeader(firstLine, sessionFile) {
93
123
  let parsed;
94
124
  try {
95
125
  parsed = JSON.parse(firstLine);
@@ -100,7 +130,7 @@ async function readHeaderLine(sessionFile) {
100
130
  if (!isSessionHeader(parsed)) {
101
131
  throw new Error(`Cannot rebind session: ${sessionFile} does not start with a session header`);
102
132
  }
103
- return { header: parsed, rest: newline === -1 ? "" : content.slice(newline + 1) };
133
+ return parsed;
104
134
  }
105
135
  function isSessionHeader(value) {
106
136
  return (typeof value === "object" &&
@@ -110,34 +140,34 @@ function isSessionHeader(value) {
110
140
  }
111
141
  // Per-session extension state lives at `<sessionDir>/extensions/<extension>/<encoded id>.json`
112
142
  // (goal, loop, terminal monitors); without it a rebound session would lose its goal and monitors.
113
- function moveSessionSidecars(fromDir, toDir, sessionId) {
143
+ async function moveSessionSidecars(fromDir, toDir, sessionId) {
114
144
  const extensionsDir = join(fromDir, "extensions");
115
145
  const sidecarName = `${encodedSessionId(sessionId)}.json`;
116
- if (existsSync(extensionsDir)) {
117
- for (const extension of readdirSync(extensionsDir, { withFileTypes: true })) {
146
+ if (await pathExists(extensionsDir)) {
147
+ for (const extension of await readdir(extensionsDir, { withFileTypes: true })) {
118
148
  if (!extension.isDirectory())
119
149
  continue;
120
150
  const from = join(extensionsDir, extension.name, sidecarName);
121
- if (existsSync(from))
122
- movePath(from, join(toDir, "extensions", extension.name, sidecarName));
151
+ if (await pathExists(from))
152
+ await movePath(from, join(toDir, "extensions", extension.name, sidecarName));
123
153
  }
124
154
  }
125
155
  const blobs = join(fromDir, "resident-blobs", sessionId);
126
- if (existsSync(blobs))
127
- movePath(blobs, join(toDir, "resident-blobs", sessionId));
156
+ if (await pathExists(blobs))
157
+ await movePath(blobs, join(toDir, "resident-blobs", sessionId));
128
158
  }
129
- function movePath(from, to) {
130
- if (existsSync(to))
159
+ async function movePath(from, to) {
160
+ if (await pathExists(to))
131
161
  return;
132
- mkdirSync(dirname(to), { recursive: true });
162
+ await mkdir(dirname(to), { recursive: true });
133
163
  try {
134
- renameSync(from, to);
164
+ await rename(from, to);
135
165
  }
136
166
  catch (error) {
137
167
  if (error.code !== "EXDEV")
138
168
  throw error;
139
- cpSync(from, to, { recursive: true });
140
- rmSync(from, { recursive: true, force: true });
169
+ await cp(from, to, { recursive: true });
170
+ await rm(from, { recursive: true, force: true });
141
171
  }
142
172
  }
143
173
  //# sourceMappingURL=session-rebind.js.map
@@ -4,7 +4,7 @@
4
4
  * outgoing bundles and the session folder for a working directory) and maps refusals to wire codes.
5
5
  */
6
6
  import { isAbsolute, join } from "node:path";
7
- import { getDefaultSessionDir, loadEntriesFromFile } from "../../core/session-manager.js";
7
+ import { getDefaultSessionDir, loadEntriesFromFileAsync } from "../../core/session-manager.js";
8
8
  import { parseMoveId } from "../../core/session-move/bundle.js";
9
9
  import { exportSessionForMove } from "../../core/session-move/export.js";
10
10
  import { sealedOutgoingStateDir } from "../../core/session-move/extension-state.js";
@@ -13,7 +13,7 @@ import { answerMoveStatus, importMovedSession } from "../../core/session-move/im
13
13
  import { MoveLedger, parseMoveStatus } from "../../core/session-move/move-ledger.js";
14
14
  import { headerSessionId, sessionOwnership } from "../../core/session-move/ownership.js";
15
15
  import { revokeImportedMove } from "../../core/session-move/revoke.js";
16
- import { realSessionPath } from "../../core/session-move/session-lock.js";
16
+ import { orderedRealPath, realSessionPath } from "../../core/session-move/session-lock.js";
17
17
  import { normalizePath, resolvePath } from "../../utils/paths.js";
18
18
  import { RPC_ERROR_INVALID_MOVE_REQUEST, RPC_ERROR_MOVE_FAILED, RPC_ERROR_MOVE_NOT_RELEASED, RPC_ERROR_SESSION_OPEN_HERE, } from "./session-move-wire.js";
19
19
  /** Refusal codes the move core throws (`error.code`), passed through to the wire as they are. */
@@ -113,7 +113,7 @@ async function run(parsed, port, ledger, outgoing) {
113
113
  case "status":
114
114
  return answerMoveStatus(ledger, parsed.moveId, parsed.sessionPath);
115
115
  case "export": {
116
- const sessionFile = realSessionPath(parsed.sessionPath);
116
+ const sessionFile = await orderedRealPath(parsed.sessionPath);
117
117
  const bundleDir = outgoing(parsed.moveId);
118
118
  const manifest = await exportSessionForMove({
119
119
  sessionFile,
@@ -122,9 +122,9 @@ async function run(parsed, port, ledger, outgoing) {
122
122
  droppedUserMessages: parsed.dropped,
123
123
  bundleDir,
124
124
  extensionStateDir: () => sealedOutgoingStateDir(sessionFile, parsed.moveId),
125
- guard: () => {
126
- refuseOpenHere(port, sessionFile, "This session is open on this host. Release it with release_session reason move and this moveId first.");
127
- refuseUnlessSealedFor(sessionFile, parsed.moveId);
125
+ guard: async () => {
126
+ await refuseOpenHere(port, sessionFile, "This session is open on this host. Release it with release_session reason move and this moveId first.");
127
+ await refuseUnlessSealedFor(sessionFile, parsed.moveId);
128
128
  },
129
129
  // Again inside the cross-process lock: a finish from another generation may have reclaimed it meanwhile.
130
130
  lockedGuard: () => refuseUnlessSealedFor(sessionFile, parsed.moveId),
@@ -146,13 +146,13 @@ async function run(parsed, port, ledger, outgoing) {
146
146
  sessionDir,
147
147
  from: parsed.from,
148
148
  });
149
- const sessionId = headerSessionId(loadEntriesFromFile(sessionFile));
149
+ const sessionId = headerSessionId(await loadEntriesFromFileAsync(sessionFile));
150
150
  if (sessionId === undefined)
151
151
  throw new Error(`The imported session at ${sessionFile} has no id in its header.`);
152
152
  return { session_id: sessionId, session_path: sessionFile };
153
153
  }
154
154
  case "finish": {
155
- const sessionFile = realSessionPath(parsed.sessionPath);
155
+ const sessionFile = await orderedRealPath(parsed.sessionPath);
156
156
  await finishMove({
157
157
  sessionFile,
158
158
  bundleDir: outgoing(parsed.moveId),
@@ -163,7 +163,7 @@ async function run(parsed, port, ledger, outgoing) {
163
163
  return ownershipOf(sessionFile);
164
164
  }
165
165
  case "revoke": {
166
- const sessionFile = realSessionPath(parsed.sessionPath);
166
+ const sessionFile = await orderedRealPath(parsed.sessionPath);
167
167
  await revokeImportedMove({
168
168
  sessionFile,
169
169
  moveId: parsed.moveId,
@@ -174,8 +174,8 @@ async function run(parsed, port, ledger, outgoing) {
174
174
  }
175
175
  }
176
176
  }
177
- function ownershipOf(sessionFile) {
178
- const ownership = sessionOwnership(loadEntriesFromFile(sessionFile));
177
+ async function ownershipOf(sessionFile) {
178
+ const ownership = sessionOwnership(await loadEntriesFromFileAsync(sessionFile));
179
179
  return { ownership: ownership.writable ? "writable" : ownership.state };
180
180
  }
181
181
  class MoveRefusal extends Error {
@@ -185,13 +185,14 @@ class MoveRefusal extends Error {
185
185
  this.code = code;
186
186
  }
187
187
  }
188
- function refuseUnlessSealedFor(sessionFile, moveId) {
189
- const ownership = sessionOwnership(loadEntriesFromFile(sessionFile));
188
+ async function refuseUnlessSealedFor(sessionFile, moveId) {
189
+ const ownership = sessionOwnership(await loadEntriesFromFileAsync(sessionFile));
190
190
  if (ownership.writable || ownership.state !== "moving" || ownership.moveId !== moveId)
191
191
  throw new MoveRefusal(RPC_ERROR_MOVE_NOT_RELEASED, "This session file is not sealed for this move. Release it with release_session reason move and this moveId first; that also hands back its queued input.");
192
192
  }
193
- function refuseOpenHere(port, sessionFile, detail) {
194
- if (port.hostedPaths().some((path) => realSessionPath(path) === sessionFile))
193
+ async function refuseOpenHere(port, sessionFile, detail) {
194
+ const hosted = await Promise.all(port.hostedPaths().map((path) => realSessionPath(path)));
195
+ if (hosted.includes(sessionFile))
195
196
  throw new MoveRefusal(RPC_ERROR_SESSION_OPEN_HERE, detail);
196
197
  }
197
198
  function answer(id, data) {
@@ -21,7 +21,7 @@
21
21
  * Also refused while clients are attached (`attached`, unless `force`) and for a session this host
22
22
  * cannot hand over (`release_unsupported`: a worker isolate owns the runtime, or there is no file).
23
23
  */
24
- import { rmSync } from "node:fs";
24
+ import { rm } from "node:fs/promises";
25
25
  import { isMoveId, isPlainId, parseMoveId } from "../../core/session-move/bundle.js";
26
26
  import { prepareOutgoingState, STATE_ATTEMPT_FIELD } from "../../core/session-move/extension-state.js";
27
27
  import { SessionMovedAwayError } from "../../core/session-move/ownership.js";
@@ -79,7 +79,7 @@ export async function releaseSession(port, command) {
79
79
  // Each handler runs under the session_shutdown handler budget: a hung one fails the release instead of holding it.
80
80
  if (command.reason === "move") {
81
81
  const moveId = parseMoveId(command.moveId);
82
- stagedState = prepareOutgoingState(first.sessionPath, moveId);
82
+ stagedState = await prepareOutgoingState(first.sessionPath, moveId);
83
83
  const [failed] = await first.session.extensionRunner.emitSessionMove({
84
84
  type: "session_move_export",
85
85
  moveId,
@@ -113,7 +113,7 @@ export async function releaseSession(port, command) {
113
113
  // A move refused before its seal is abandoned: what its export staged is never bundled, and a retry stages
114
114
  // afresh. Once the seal names this attempt, its folder is the move's state and stays for export.
115
115
  if (stagedState !== undefined && !sealed)
116
- rmSync(stagedState.dir, { recursive: true, force: true });
116
+ await rm(stagedState.dir, { recursive: true, force: true });
117
117
  }
118
118
  }
119
119
  }
@@ -618,7 +618,7 @@ For the next release:
618
618
 
619
619
  1. Start from a clean, current `main`.
620
620
  2. Audit every commit since the latest tag.
621
- 3. Update all affected package `[Unreleased]` sections.
621
+ 3. Record affected-package changelog entries in `changes/unreleased/<pr>.md`; the release cut compiles them before stamping.
622
622
  4. Duplicate user-facing AI, agent, and TUI changes into the coding-agent changelog.
623
623
  5. Query registry-verifiable plugin dependencies for newer exact versions.
624
624
  6. Preserve vendored snapshots when no authoritative upstream source is available.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.10.23-1",
3
+ "version": "2026.10.24-1",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -68,9 +68,9 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "@earendil-works/chord": "0.99.1",
71
- "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.23-1",
72
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.23-1",
73
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.23-1",
71
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.24-1",
72
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.24-1",
73
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.24-1",
74
74
  "@silvia-odwyer/photon-node": "0.3.4",
75
75
  "chalk": "6.0.0",
76
76
  "cross-spawn": "7.0.6",
@@ -89,8 +89,8 @@
89
89
  "yaml": "2.9.1",
90
90
  "@anthropic-ai/claude-agent-sdk": "0.3.306",
91
91
  "@anthropic-ai/sdk": "0.127.0",
92
- "@code-yeongyu/senpi-codemode": "2026.10.23-1",
93
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.23-1",
92
+ "@code-yeongyu/senpi-codemode": "2026.10.24-1",
93
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.24-1",
94
94
  "@modelcontextprotocol/sdk": "1.32.1",
95
95
  "@mozilla/readability": "0.6.0",
96
96
  "linkedom": "0.18.13",