codetrellis 0.2.0 → 0.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codetrellis",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CodeTrellis for agents and CI: run it headless, keep agents on track, check changes against your rules, and download the desktop app.",
5
5
  "keywords": [
6
6
  "codetrellis",
@@ -33,9 +33,7 @@
33
33
  "express": "^5.0.0",
34
34
  "fast-json-patch": "^3.1.1",
35
35
  "ignore": "^7.0.5",
36
- "mammoth": "^1.12.3",
37
36
  "node-pty": "^1.2.0-beta.13",
38
- "pdfjs-dist": "^6.3.289",
39
37
  "web-tree-sitter": "^0.27.0",
40
38
  "werift": "^0.24.4",
41
39
  "ws": "^8.18.0",
@@ -0,0 +1,249 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var cli_install_exports = {};
29
+ __export(cli_install_exports, {
30
+ LAUNCHER_MARK: () => LAUNCHER_MARK,
31
+ appImageLauncher: () => appImageLauncher,
32
+ applyCliInstall: () => applyCliInstall,
33
+ planCliInstall: () => planCliInstall,
34
+ refreshCliInstall: () => refreshCliInstall,
35
+ removeCliInstall: () => removeCliInstall,
36
+ thisCliMachine: () => thisCliMachine
37
+ });
38
+ module.exports = __toCommonJS(cli_install_exports);
39
+ var import_node_child_process = require("node:child_process");
40
+ var import_node_fs = __toESM(require("node:fs"));
41
+ var import_node_path = __toESM(require("node:path"));
42
+ var import_command = require("../mcp/connector/command");
43
+ const LAUNCHER_MARK = "Written by the CodeTrellis app";
44
+ const join = (m, ...p) => m.platform === "win32" ? import_node_path.default.win32.join(...p) : import_node_path.default.posix.join(...p);
45
+ function onPathOf(m, dir) {
46
+ const list = (m.env.PATH ?? "").split(m.platform === "win32" ? ";" : ":").map((d) => d.replace(/[\\/]+$/, ""));
47
+ return list.some((d) => m.platform === "win32" ? d.toLowerCase() === dir.toLowerCase() : d === dir);
48
+ }
49
+ function appImageLauncher(appImage) {
50
+ const q = `'${appImage.replace(/'/g, `'\\''`)}'`;
51
+ return [
52
+ "#!/bin/sh",
53
+ `# codetrellis: ${LAUNCHER_MARK} for the AppImage below; it rewrites this`,
54
+ "# file if the AppImage moves. Runs the CLI the app carries, in Node mode.",
55
+ // --no-sandbox last, after --: the AppImage's AppRun adds it as the first
56
+ // argument unless it is already there, and in Node mode that is a "bad
57
+ // option". As the connector's AppImage command does; app.cjs drops it.
58
+ `ELECTRON_RUN_AS_NODE=1 exec ${q} -e "require(require('path').join(process.resourcesPath, 'cli', 'bin', 'app.cjs'))" -- "$@" --no-sandbox`,
59
+ ""
60
+ ].join("\n");
61
+ }
62
+ function readLink(p) {
63
+ try {
64
+ return import_node_fs.default.readlinkSync(p);
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+ function readText(p) {
70
+ try {
71
+ return import_node_fs.default.readFileSync(p, "utf8");
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+ function present(p) {
77
+ try {
78
+ import_node_fs.default.lstatSync(p);
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+ function oursAt(p) {
85
+ const link = readLink(p);
86
+ if (link) return /[\\/]cli[\\/]bin[\\/]codetrellis$/.test(link);
87
+ return (readText(p) ?? "").includes(LAUNCHER_MARK);
88
+ }
89
+ function planCliInstall(m) {
90
+ if (!(0, import_command.isPackagedElectron)({ execPath: m.execPath, electronVersion: m.electronVersion }) || !m.resourcesPath) {
91
+ return { ok: false, reason: "Only the installed app can add the codetrellis command. From a checkout, run npm link; anywhere, npm install -g codetrellis." };
92
+ }
93
+ if (m.platform === "win32" && m.env.PORTABLE_EXECUTABLE_FILE) {
94
+ return { ok: false, reason: "The portable build unpacks to a temporary folder each time it starts, so a command on the PATH would stop working. Use the installer, or npm install -g codetrellis." };
95
+ }
96
+ const binDir = join(m, m.resourcesPath, "cli", "bin");
97
+ if (m.platform === "win32") {
98
+ const userPath = m.windowsUserPath?.() ?? "";
99
+ const has = userPath.split(";").some((d) => d.replace(/[\\/]+$/, "").toLowerCase() === binDir.toLowerCase());
100
+ return {
101
+ ok: true,
102
+ how: "path",
103
+ target: binDir,
104
+ source: join(m, binDir, "codetrellis.cmd"),
105
+ admin: false,
106
+ state: has ? "installed" : "missing",
107
+ onPath: onPathOf(m, binDir),
108
+ says: `Adds ${binDir} to your PATH. Open a new terminal afterwards.`
109
+ };
110
+ }
111
+ const localBin = import_node_path.default.posix.join(m.home, ".local", "bin");
112
+ const appImage = m.platform === "linux" && m.env.APPIMAGE && m.env.APPDIR && import_node_path.default.isAbsolute(m.env.APPIMAGE) && m.resourcesPath.startsWith(m.env.APPDIR) ? m.env.APPIMAGE : null;
113
+ const source = appImage ?? import_node_path.default.posix.join(binDir, "codetrellis");
114
+ const how = appImage ? "launcher" : "link";
115
+ const target = m.platform === "darwin" ? import_node_path.default.posix.join(m.macBin ?? "/usr/local/bin", "codetrellis") : import_node_path.default.posix.join(localBin, "codetrellis");
116
+ const dir = import_node_path.default.posix.dirname(target);
117
+ const admin = m.platform === "darwin" && !writable(dir);
118
+ let state = "missing";
119
+ let existing;
120
+ if (present(target)) {
121
+ const link = readLink(target);
122
+ if (how === "link" && link === source) state = "installed";
123
+ else if (how === "launcher" && readText(target) === appImageLauncher(appImage)) state = "installed";
124
+ else if (oursAt(target)) state = "stale";
125
+ else {
126
+ state = "other";
127
+ existing = link ? `a link to ${link}` : "a file CodeTrellis did not write";
128
+ }
129
+ }
130
+ const says = how === "launcher" ? `Writes ${target}, which runs this AppImage as the codetrellis command.` : `Links ${target} to the codetrellis command inside the app${admin ? ". macOS asks for your password to write there" : ""}.`;
131
+ return { ok: true, how, target, source, admin, state, existing, onPath: onPathOf(m, dir), says };
132
+ }
133
+ function writable(dir) {
134
+ try {
135
+ import_node_fs.default.accessSync(dir, import_node_fs.default.constants.W_OK);
136
+ return true;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+ const appleString = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
142
+ const runProgram = (command, args, env) => new Promise((resolve, reject) => {
143
+ (0, import_node_child_process.execFile)(command, args, { env: env ? { ...process.env, ...env } : process.env }, (err) => err ? reject(err) : resolve());
144
+ });
145
+ async function applyCliInstall(m, opts = {}, run = runProgram) {
146
+ const plan = planCliInstall(m);
147
+ if (!plan.ok) return plan;
148
+ if (plan.state === "installed") return { ok: true, target: plan.target, onPath: plan.onPath };
149
+ if (plan.state === "other" && !opts.replace) {
150
+ return { ok: false, reason: `${plan.target} is already ${plan.existing}. Choose Replace to put the app's command there instead.` };
151
+ }
152
+ try {
153
+ if (plan.how === "path") {
154
+ await run(
155
+ "powershell.exe",
156
+ [
157
+ "-NoProfile",
158
+ "-NonInteractive",
159
+ "-Command",
160
+ "$p=[Environment]::GetEnvironmentVariable('Path','User'); if (-not $p) { $p='' }; $parts=@($p -split ';' | Where-Object { $_ -and ($_.TrimEnd('\\') -ne $env:CT_DIR.TrimEnd('\\')) }); [Environment]::SetEnvironmentVariable('Path', (($parts + $env:CT_DIR) -join ';'), 'User')"
161
+ ],
162
+ { CT_DIR: plan.target }
163
+ );
164
+ } else if (plan.how === "launcher") {
165
+ import_node_fs.default.mkdirSync(import_node_path.default.posix.dirname(plan.target), { recursive: true });
166
+ if (present(plan.target)) import_node_fs.default.rmSync(plan.target, { force: true });
167
+ import_node_fs.default.writeFileSync(plan.target, appImageLauncher(plan.source), { mode: 493 });
168
+ } else if (plan.admin) {
169
+ const dir = import_node_path.default.posix.dirname(plan.target);
170
+ const script = `do shell script "mkdir -p " & quoted form of ${appleString(dir)} & " && ln -sf " & quoted form of ${appleString(plan.source)} & " " & quoted form of ${appleString(plan.target)} with administrator privileges`;
171
+ await run("osascript", ["-e", script]);
172
+ } else {
173
+ import_node_fs.default.mkdirSync(import_node_path.default.posix.dirname(plan.target), { recursive: true });
174
+ if (present(plan.target)) import_node_fs.default.rmSync(plan.target, { force: true });
175
+ import_node_fs.default.symlinkSync(plan.source, plan.target);
176
+ }
177
+ } catch (err) {
178
+ const msg = err instanceof Error ? err.message : String(err);
179
+ return { ok: false, reason: /User canceled|-128/.test(msg) ? "Cancelled: macOS did not get the password." : `Could not add the command: ${msg}` };
180
+ }
181
+ const after = planCliInstall(m);
182
+ if (!after.ok || after.state !== "installed") return { ok: false, reason: `The command was not added at ${plan.target}.` };
183
+ return { ok: true, target: after.target, onPath: after.onPath };
184
+ }
185
+ async function refreshCliInstall(m) {
186
+ const plan = planCliInstall(m);
187
+ if (!plan.ok || plan.state !== "stale" || plan.admin || plan.how === "path") return false;
188
+ return (await applyCliInstall(m)).ok;
189
+ }
190
+ async function removeCliInstall(m, run = runProgram) {
191
+ const plan = planCliInstall(m);
192
+ if (!plan.ok) return plan;
193
+ if (plan.state === "missing") return { ok: true, target: plan.target, onPath: plan.onPath };
194
+ if (plan.state === "other") return { ok: false, reason: `${plan.target} is not CodeTrellis's; it was left as it is.` };
195
+ try {
196
+ if (plan.how === "path") {
197
+ await run(
198
+ "powershell.exe",
199
+ [
200
+ "-NoProfile",
201
+ "-NonInteractive",
202
+ "-Command",
203
+ "$p=[Environment]::GetEnvironmentVariable('Path','User'); if (-not $p) { $p='' }; [Environment]::SetEnvironmentVariable('Path', ((@($p -split ';' | Where-Object { $_ -and ($_.TrimEnd('\\') -ne $env:CT_DIR.TrimEnd('\\')) })) -join ';'), 'User')"
204
+ ],
205
+ { CT_DIR: plan.target }
206
+ );
207
+ } else if (plan.admin) {
208
+ await run("osascript", ["-e", `do shell script "rm -f " & quoted form of ${appleString(plan.target)} with administrator privileges`]);
209
+ } else {
210
+ import_node_fs.default.rmSync(plan.target, { force: true });
211
+ }
212
+ } catch (err) {
213
+ return { ok: false, reason: `Could not remove the command: ${err instanceof Error ? err.message : String(err)}` };
214
+ }
215
+ return { ok: true, target: plan.target, onPath: plan.onPath };
216
+ }
217
+ function thisCliMachine() {
218
+ return {
219
+ platform: process.platform,
220
+ home: process.env.HOME || process.env.USERPROFILE || "",
221
+ execPath: process.execPath,
222
+ resourcesPath: process.resourcesPath,
223
+ electronVersion: process.versions.electron,
224
+ env: {
225
+ APPIMAGE: process.env.APPIMAGE,
226
+ APPDIR: process.env.APPDIR,
227
+ PORTABLE_EXECUTABLE_FILE: process.env.PORTABLE_EXECUTABLE_FILE,
228
+ PATH: process.env.PATH
229
+ },
230
+ windowsUserPath: () => {
231
+ try {
232
+ const out = (0, import_node_child_process.execFileSync)("reg", ["query", "HKCU\\Environment", "/v", "Path"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
233
+ return /Path\s+REG_(?:EXPAND_)?SZ\s+(.*)/i.exec(out)?.[1]?.trim() ?? null;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+ };
239
+ }
240
+ // Annotate the CommonJS export names for ESM import in node:
241
+ 0 && (module.exports = {
242
+ LAUNCHER_MARK,
243
+ appImageLauncher,
244
+ applyCliInstall,
245
+ planCliInstall,
246
+ refreshCliInstall,
247
+ removeCliInstall,
248
+ thisCliMachine
249
+ });
@@ -156,6 +156,13 @@ function signRuleChange(projectRoot, change, device, now = Date.now()) {
156
156
  (0, import_confined_fs.writeFileWithin)(projectRoot, rel, approvalYaml(statement, signature), "rule approval");
157
157
  return { file: rel, how: signature.how, as: statement.signer };
158
158
  }
159
+ function canonicalFolderOf(file) {
160
+ try {
161
+ return import_node_path.default.join(import_node_fs.default.realpathSync(import_node_path.default.dirname(file)), import_node_path.default.basename(file));
162
+ } catch {
163
+ return file;
164
+ }
165
+ }
159
166
  function keysAt(projectRoot, commit) {
160
167
  const devices = /* @__PURE__ */ new Map();
161
168
  const listing = git(projectRoot, ["ls-tree", "--name-only", commit, `./${import_trust.KEYS_DIR}/`]);
@@ -173,7 +180,7 @@ function keysAt(projectRoot, commit) {
173
180
  const configured = (0, import_signed_approvals.signingSetup)(projectRoot).allowedSigners;
174
181
  if (configured) {
175
182
  const top = git(projectRoot, ["rev-parse", "--show-toplevel"])?.trim();
176
- const inRepo = top ? import_node_path.default.relative(import_node_fs.default.realpathSync(top), import_node_path.default.resolve(configured)) : null;
183
+ const inRepo = top ? import_node_path.default.relative(import_node_fs.default.realpathSync(top), canonicalFolderOf(import_node_path.default.resolve(configured))) : null;
177
184
  if (inRepo !== null && !inRepo.startsWith("..") && !import_node_path.default.isAbsolute(inRepo)) {
178
185
  const text = git(top, ["show", `${commit}:${inRepo.split(import_node_path.default.sep).join("/")}`]);
179
186
  if (text !== null) {
package/src/cli/args.js CHANGED
@@ -32,7 +32,8 @@ __export(args_exports, {
32
32
  flag: () => flag,
33
33
  headlessCacheRoot: () => headlessCacheRoot,
34
34
  headlessDataDir: () => headlessDataDir,
35
- parseArgs: () => parseArgs
35
+ parseArgs: () => parseArgs,
36
+ selfCommand: () => selfCommand
36
37
  });
37
38
  module.exports = __toCommonJS(args_exports);
38
39
  var import_node_path = __toESM(require("node:path"));
@@ -86,13 +87,19 @@ function headlessCacheRoot(env, home, platform = process.platform) {
86
87
  return import_node_path.default.join(cache, "codetrellis");
87
88
  }
88
89
  const quote = (s) => /^[A-Za-z0-9_./:=@-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
89
- function connectorLine(nodePath, binPath, dataDir) {
90
- const args = [binPath, "mcp", "--data-dir", dataDir];
90
+ function selfCommand(binDir, proc) {
91
+ if (proc.electron) return { command: proc.execPath, args: [import_node_path.default.join(binDir, "app.cjs")], env: { ELECTRON_RUN_AS_NODE: "1" } };
92
+ return { command: proc.execPath, args: [import_node_path.default.join(binDir, "codetrellis.mjs")], env: {} };
93
+ }
94
+ function connectorLine(self, dataDir) {
95
+ const args = [...self.args, "mcp", "--data-dir", dataDir];
96
+ const env = Object.keys(self.env).length ? self.env : void 0;
97
+ const envFlags = Object.entries(self.env).map(([k, v]) => `-e ${quote(`${k}=${v}`)} `).join("");
91
98
  return {
92
- command: nodePath,
99
+ command: self.command,
93
100
  args,
94
- claude: `claude mcp add codetrellis -- ${[nodePath, ...args].map(quote).join(" ")}`,
95
- json: JSON.stringify({ mcpServers: { codetrellis: { command: nodePath, args } } })
101
+ claude: `claude mcp add codetrellis ${envFlags}-- ${[self.command, ...args].map(quote).join(" ")}`,
102
+ json: JSON.stringify({ mcpServers: { codetrellis: { command: self.command, args, ...env ? { env } : {} } } })
96
103
  };
97
104
  }
98
105
  const USAGE = `codetrellis \u2014 CodeTrellis for agents, without the desktop app
@@ -197,5 +204,6 @@ Changing and committing the plan:
197
204
  flag,
198
205
  headlessCacheRoot,
199
206
  headlessDataDir,
200
- parseArgs
207
+ parseArgs,
208
+ selfCommand
201
209
  });
package/src/cli/main.js CHANGED
@@ -111,7 +111,7 @@ async function serve(p) {
111
111
  const project = projectOf(p);
112
112
  const dataDir = import_node_path.default.resolve((0, import_args.flag)(p, "data-dir") ?? (0, import_args.headlessDataDir)(project, process.env, import_node_os.default.homedir()));
113
113
  const { base, scan, mcp: mcp2 } = await boot(p, project, dataDir);
114
- const line = (0, import_args.connectorLine)(process.execPath, binPath(), dataDir);
114
+ const line = (0, import_args.connectorLine)(self(), dataDir);
115
115
  const c = counts(scan);
116
116
  markReady(dataDir, project);
117
117
  if (p.flags.json) {
@@ -179,7 +179,7 @@ async function runningIn(dataDir) {
179
179
  async function start(p) {
180
180
  const project = projectOf(p);
181
181
  const dataDir = import_node_path.default.resolve((0, import_args.flag)(p, "data-dir") ?? (0, import_args.headlessDataDir)(project, process.env, import_node_os.default.homedir()));
182
- const line = (0, import_args.connectorLine)(process.execPath, binPath(), dataDir);
182
+ const line = (0, import_args.connectorLine)(self(), dataDir);
183
183
  const say = (state, pid) => {
184
184
  if (p.flags.quiet) return;
185
185
  if (p.flags.json) out(JSON.stringify({ state, pid, project, dataDir, connector: { command: line.command, args: line.args } }));
@@ -191,13 +191,14 @@ Connect an agent with:
191
191
  if (up) return say("running", up.pid);
192
192
  import_node_fs.default.mkdirSync(dataDir, { recursive: true });
193
193
  const log = import_node_fs.default.openSync(import_node_path.default.join(dataDir, "serve.log"), "a");
194
- const args = [binPath(), "serve", "--project", project, "--data-dir", dataDir];
194
+ const me = self();
195
+ const args = [...me.args, "serve", "--project", project, "--data-dir", dataDir];
195
196
  for (const k of ["port", "mcp-port"]) {
196
197
  const v = portOf(p, k);
197
198
  if (v !== void 0) args.push(`--${k}`, v);
198
199
  }
199
200
  if (p.flags["share-task-state"]) args.push("--share-task-state");
200
- const child = (0, import_node_child_process.spawn)(process.execPath, args, { detached: true, stdio: ["ignore", log, log], cwd: project, env: process.env });
201
+ const child = (0, import_node_child_process.spawn)(me.command, args, { detached: true, stdio: ["ignore", log, log], cwd: project, env: { ...process.env, ...me.env } });
201
202
  child.unref();
202
203
  import_node_fs.default.closeSync(log);
203
204
  let exited = null;
@@ -292,7 +293,8 @@ async function reviewCmd(p) {
292
293
  throw err;
293
294
  }
294
295
  try {
295
- const sinkFor = (dir) => ({ command: process.execPath, args: [binPath(), "review-sink", "--pass", dir] });
296
+ const me = self();
297
+ const sinkFor = (dir) => ({ command: me.command, args: [...me.args, "review-sink", "--pass", dir], env: me.env });
296
298
  const post = opts.post ? await reviewPoster(opts.post, cwd) : void 0;
297
299
  const { out: text, code, note } = await review(agent, opts, cwd, process.env, sinkFor, { post, version: import_package.version });
298
300
  out(text);
@@ -371,8 +373,8 @@ async function desktopCmd(p) {
371
373
  if (error instanceof DesktopUsageError) fail(error.message);
372
374
  if (error) fail(error instanceof Error ? error.message : String(error), 1);
373
375
  }
374
- function binPath() {
375
- return import_node_path.default.resolve(__dirname, "..", "..", "bin", "codetrellis.mjs");
376
+ function self() {
377
+ return (0, import_args.selfCommand)(import_node_path.default.resolve(__dirname, "..", "..", "bin"), { execPath: process.execPath, electron: process.versions.electron });
376
378
  }
377
379
  async function main() {
378
380
  const p = (0, import_args.parseArgs)(process.argv.slice(2));
@@ -63,7 +63,8 @@ const claudeCode = {
63
63
  command({ instructions, message, sink, model, maxTurns, dir, auth, tools }) {
64
64
  const withKey = auth.kind === "oidc" || auth.kind === "key" && !oauthToken(auth);
65
65
  const mcp = import_node_path.default.join(dir, "mcp.json");
66
- import_node_fs.default.writeFileSync(mcp, JSON.stringify({ mcpServers: { [SINK]: { type: "stdio", command: sink.command, args: sink.args } } }));
66
+ const env = sink.env && Object.keys(sink.env).length ? { env: sink.env } : {};
67
+ import_node_fs.default.writeFileSync(mcp, JSON.stringify({ mcpServers: { [SINK]: { type: "stdio", command: sink.command, args: sink.args, ...env } } }));
67
68
  const system = import_node_path.default.join(dir, "system.md");
68
69
  import_node_fs.default.writeFileSync(system, instructions);
69
70
  return {
@@ -159,6 +160,7 @@ const codex = {
159
160
  ...set("project_doc_max_bytes", "0"),
160
161
  ...set(`${server}.command`, toml(sink.command)),
161
162
  ...set(`${server}.args`, `[${sink.args.map(toml).join(", ")}]`),
163
+ ...sink.env && Object.keys(sink.env).length ? set(`${server}.env`, `{ ${Object.entries(sink.env).map(([k, v]) => `${k} = ${toml(v)}`).join(", ")} }`) : [],
162
164
  ...set(`${server}.enabled_tools`, `[${tools.map(toml).join(", ")}]`),
163
165
  ...set(`${server}.default_tools_approval_mode`, toml("approve")),
164
166
  ...set("model_providers.codetrellis_review.name", toml("codetrellis review")),
package/src/cli/verbs.js CHANGED
@@ -292,7 +292,8 @@ async function pipelined(ctx, root, changed) {
292
292
  if (err instanceof ReviewUsageError) throw new UsageError(err.message);
293
293
  throw err;
294
294
  }
295
- const sinkFor = (dir) => ({ command: process.execPath, args: [import_node_path.default.resolve(__dirname, "..", "..", "bin", "codetrellis.mjs"), "review-sink", "--pass", dir] });
295
+ const me = (0, import_args.selfCommand)(import_node_path.default.resolve(__dirname, "..", "..", "bin"), { execPath: process.execPath, electron: process.versions.electron });
296
+ const sinkFor = (dir) => ({ command: me.command, args: [...me.args, "review-sink", "--pass", dir], env: me.env });
296
297
  review = async (stage, scope, grounding) => {
297
298
  const r = await runReview(ctx.agent, { ...o, scope: { suite: scope.suite, rule: scope.rule, path: scope.path, engine: scope.engine, strength: scope.strength, tag: scope.tag }, stage: stage.id, grounding, format: "json", failOn: /* @__PURE__ */ new Set(["block"]), post: null }, ctx.cwd, process.env, sinkFor, { version: import_package.version });
298
299
  let j;
@@ -1,4 +0,0 @@
1
- var import_read = require("./read");
2
- process.once("message", (req) => {
3
- (0, import_read.readMaterialBytes)(req).catch((err) => ({ ok: false, reason: `The file could not be read (${err.message})` })).then((reply) => process.send?.(reply, () => process.disconnect()));
4
- });
@@ -1,430 +0,0 @@
1
- var __create = Object.create;
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
- // If the importer is in node compatibility mode or this is not an ESM
21
- // file that has been converted to a CommonJS file using a Babel-
22
- // compatible transform (i.e. "__esModule" has not been set), then set
23
- // "default" to the CommonJS "module.exports" for node compatibility.
24
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
- mod
26
- ));
27
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
- var read_exports = {};
29
- __export(read_exports, {
30
- MAX_OUTPUT_CHARS: () => MAX_OUTPUT_CHARS,
31
- readMaterialBytes: () => readMaterialBytes
32
- });
33
- module.exports = __toCommonJS(read_exports);
34
- var import_mammoth = __toESM(require("mammoth"));
35
- var import_zip_reader = require("../../../shared/lib/zip-reader");
36
- var import_xlsx_xml = require("../../../shared/lib/xlsx-xml");
37
- var import_pptx_xml = require("../../../shared/lib/pptx-xml");
38
- var import_locator = require("../../../shared/lib/locator");
39
- var import_docx_markdown = require("./docx-markdown");
40
- var import_csv = require("../../../shared/lib/csv");
41
- const MAX_OUTPUT_CHARS = 1e5;
42
- const MAX_PART_BYTES = 30 * 1024 * 1024;
43
- const MAX_TOTAL_BYTES = 100 * 1024 * 1024;
44
- const SHEET_CAPS = { maxRows: 5e3, maxCols: 200, maxCells: 25e4 };
45
- const MAX_PDF_PAGES = 500;
46
- const MAX_SLIDES = 500;
47
- const TEXT_CONTEXT_LINES = 15;
48
- class ReadError extends Error {
49
- }
50
- const squash = (s) => s.replace(/\s+/g, " ").trim().toLowerCase();
51
- const dash = (a, b) => a === b ? String(a) : `${a}\u2013${b}`;
52
- function fit(sections, notes, rest) {
53
- const out = [];
54
- let left = MAX_OUTPUT_CHARS;
55
- for (const [i, s] of sections.entries()) {
56
- const cost = s.heading.length + s.body.length + 4;
57
- if (cost <= left) {
58
- out.push(s);
59
- left -= cost;
60
- continue;
61
- }
62
- let partly = false;
63
- if (left > 2e3) {
64
- const heading = `${s.heading} (cut short)`;
65
- const cut = s.body.slice(0, left - heading.length - 4);
66
- out.push({ heading, body: cut.slice(0, Math.max(0, cut.lastIndexOf("\n"))) });
67
- partly = true;
68
- }
69
- const skipped = sections.slice(partly ? i + 1 : i).map((x) => x.heading);
70
- notes.push(`Stopped at about ${MAX_OUTPUT_CHARS.toLocaleString("en-GB")} characters. ${rest(s.heading)}${skipped.length ? ` Not shown: ${skipped.slice(0, 20).join(", ")}${skipped.length > 20 ? ", \u2026" : ""}.` : ""}`);
71
- break;
72
- }
73
- return out;
74
- }
75
- function numbered(lines, start, end) {
76
- const width = String(end).length;
77
- const out = [];
78
- for (let n = start; n <= end; n++) out.push(`${String(n).padStart(width)} ${lines[n - 1]}`);
79
- return out.join("\n");
80
- }
81
- function readLines(lines, locator, opts) {
82
- const total = lines.length;
83
- const body = (a, b) => opts.number ? numbered(lines, a, b) : lines.slice(a - 1, b).join("\n");
84
- if (locator.lines !== void 0) {
85
- const span = (0, import_locator.toSpan)(locator.lines);
86
- if (!span || span.start < 1 || span.end < span.start) {
87
- throw new ReadError(`lines must be a line number or a span such as "10-20" \u2014 got ${JSON.stringify(locator.lines)}`);
88
- }
89
- if (span.start > total) throw new ReadError(`${opts.name} has ${total} lines; ${span.start} is past the end`);
90
- const end = Math.min(span.end, total);
91
- return { where: `lines ${dash(span.start, end)}`, section: { heading: `Lines ${dash(span.start, end)} of ${total}`, body: body(span.start, end) } };
92
- }
93
- if (locator.text !== void 0) {
94
- const quote = squash(locator.text);
95
- if (!quote) throw new ReadError("text must be the words to find");
96
- const at = lines.findIndex((_, i) => squash(lines.slice(i, i + 8).join(" ")).includes(quote));
97
- if (at < 0) throw new ReadError(`${opts.name} does not contain "${locator.text.slice(0, 80)}"`);
98
- let first = at;
99
- while (first + 1 < total && squash(lines.slice(first + 1, first + 9).join(" ")).includes(quote)) first++;
100
- const a = Math.max(1, first + 1 - TEXT_CONTEXT_LINES);
101
- const b = Math.min(total, first + 1 + TEXT_CONTEXT_LINES);
102
- return { where: `the passage at line ${first + 1}`, section: { heading: `Lines ${dash(a, b)} of ${total}`, body: body(a, b) } };
103
- }
104
- return { where: null, section: { heading: `Lines 1\u2013${total}`, body: body(1, total) } };
105
- }
106
- function readTextFile(req, locator) {
107
- const text = new TextDecoder("utf-8", { fatal: false }).decode(req.bytes);
108
- const lines = text.split(/\r?\n/);
109
- if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
110
- const notes = [];
111
- refuseKeys(locator, req.ext === "csv" ? ["lines", "text", "range"] : ["lines", "text"], req);
112
- const { where, section } = readLines(lines, locator, { number: true, name: req.name });
113
- const sections = fit([section], notes, () => 'Ask for {"lines": "N-M"} to read further.');
114
- return { ok: true, format: "text", where, outline: `${lines.length} lines`, sections, notes };
115
- }
116
- function readCsvRange(req, locator) {
117
- refuseKeys(locator, ["range"], req);
118
- const text = new TextDecoder("utf-8", { fatal: false }).decode(req.bytes);
119
- const rows = (0, import_csv.parseCsv)(text);
120
- const shape = (0, import_csv.csvShape)(rows);
121
- const range = (0, import_locator.parseRange)(String(locator.range).trim());
122
- if (!range) throw new ReadError(`"${locator.range}" is not a cell or range \u2014 write it as C14 or A1:F20`);
123
- if (range.to.row > shape.rows || range.to.col > shape.cols) {
124
- throw new ReadError(`${locator.range} is outside ${req.name}, which has ${shape.rows} rows and ${shape.cols} columns`);
125
- }
126
- const at = `${(0, import_xlsx_xml.colLetters)(range.from.col)}${range.from.row}:${(0, import_xlsx_xml.colLetters)(range.to.col)}${range.to.row}`;
127
- const body = toCsv(rows.slice(range.from.row - 1, range.to.row).map((r) => r.slice(range.from.col - 1, range.to.col)));
128
- const notes = [];
129
- return {
130
- ok: true,
131
- format: "csv",
132
- where: `cells ${at}`,
133
- outline: `${shape.rows} rows, ${shape.cols} columns`,
134
- sections: fit([{ heading: `Cells ${at}`, body }], notes, () => 'Ask for a smaller {"range"} to read the rest.'),
135
- notes
136
- };
137
- }
138
- function csvField(v) {
139
- return /[",\r\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
140
- }
141
- function toCsv(rows) {
142
- let lastRow = rows.length;
143
- while (lastRow > 0 && rows[lastRow - 1].every((c) => c === "")) lastRow--;
144
- const width = Math.max(0, ...rows.slice(0, lastRow).map((r) => {
145
- let w = r.length;
146
- while (w > 0 && r[w - 1] === "") w--;
147
- return w;
148
- }));
149
- return rows.slice(0, lastRow).map((r) => Array.from({ length: width }, (_, i) => csvField(r[i] ?? "")).join(",")).join("\n");
150
- }
151
- async function readWorkbook(req, locator) {
152
- refuseKeys(locator, ["sheet", "range", "text"], req);
153
- const view = new DataView(req.bytes.buffer, req.bytes.byteOffset, req.bytes.byteLength);
154
- const entries = (0, import_zip_reader.listEntries)(view);
155
- const budget = { maxPart: MAX_PART_BYTES, maxTotal: MAX_TOTAL_BYTES, used: 0 };
156
- const part = (name) => (0, import_zip_reader.readText)(view, entries, name, budget);
157
- const workbook = await part("xl/workbook.xml");
158
- if (!workbook) throw new ReadError(`${req.name} is not an Excel workbook \u2014 it has no workbook part`);
159
- const refs = (0, import_xlsx_xml.parseWorkbookSheets)(workbook, await part("xl/_rels/workbook.xml.rels") ?? "");
160
- const shared = (0, import_xlsx_xml.parseSharedStrings)(await part("xl/sharedStrings.xml") ?? "");
161
- const dates = (0, import_xlsx_xml.parseDateStyles)(await part("xl/styles.xml") ?? "");
162
- const notes = ["Each cell is the value the spreadsheet app last saved; formulas are not re-calculated."];
163
- if (entries.some((e) => /vbaProject\.bin$/i.test(e.name))) notes.push("This workbook carries macros. They were not run.");
164
- let range = null;
165
- if (locator.range !== void 0) {
166
- range = (0, import_locator.parseRange)(String(locator.range).trim());
167
- if (!range) throw new ReadError(`"${locator.range}" is not a cell or range \u2014 write it as C14 or A1:F20`);
168
- }
169
- let chosen = refs;
170
- if (locator.sheet !== void 0) {
171
- const want = String(locator.sheet);
172
- const hit = refs.find((r) => r.name === want) ?? refs.find((r) => r.name.toLowerCase() === want.trim().toLowerCase());
173
- if (!hit) throw new ReadError(`${req.name} has no sheet "${want}" \u2014 it has ${refs.map((r) => `"${r.name}"`).join(", ") || "none"}`);
174
- chosen = [hit];
175
- } else if (range) {
176
- chosen = refs.slice(0, 1);
177
- }
178
- const dims = [];
179
- const sections = [];
180
- let cellsLeft = SHEET_CAPS.maxCells;
181
- for (const ref of refs) {
182
- const xml = ref.part ? await part(ref.part) : null;
183
- const dimension = xml ? (0, import_xlsx_xml.sheetDimension)(xml) : null;
184
- dims.push(`${ref.name}${dimension ? ` (${dimension})` : ""}`);
185
- if (!chosen.includes(ref) || !xml) continue;
186
- const caps = range ? { maxRows: range.to.row, maxCols: range.to.col, maxCells: cellsLeft } : { ...SHEET_CAPS, maxCells: cellsLeft };
187
- const grid = (0, import_xlsx_xml.parseSheetGrid)(xml, shared, dates, caps);
188
- cellsLeft = Math.max(0, cellsLeft - grid.cells);
189
- let rows = grid.rows;
190
- let heading = `Sheet "${ref.name}"`;
191
- if (range) {
192
- rows = rows.slice(range.from.row - 1, range.to.row).map((r) => r.slice(range.from.col - 1, range.to.col));
193
- heading += `, ${(0, import_xlsx_xml.colLetters)(range.from.col)}${range.from.row}:${(0, import_xlsx_xml.colLetters)(range.to.col)}${range.to.row}`;
194
- } else if (grid.truncated) {
195
- notes.push(`Sheet "${ref.name}" goes past ${SHEET_CAPS.maxRows.toLocaleString("en-GB")} rows or ${SHEET_CAPS.maxCols} columns; ask for {"sheet": "${ref.name}", "range": "A${SHEET_CAPS.maxRows + 1}:\u2026"} to read further.`);
196
- }
197
- if (!range) heading += " from A1";
198
- sections.push({ heading, body: toCsv(rows) });
199
- }
200
- let shown = sections;
201
- let where = locator.sheet !== void 0 || range ? sections.map((s) => s.heading.replace(/^Sheet /, "sheet ").replace(/ from A1$/, "")).join("; ") : null;
202
- if (locator.text !== void 0) {
203
- const quote = squash(locator.text);
204
- if (!quote) throw new ReadError("text must be the words to find");
205
- shown = sections.filter((s) => squash(s.body.replace(/"/g, "")).includes(quote) || squash(s.body).includes(quote));
206
- if (shown.length === 0) throw new ReadError(`No sheet in ${req.name} contains "${locator.text.slice(0, 80)}"`);
207
- where = shown.map((s) => s.heading.replace(/^Sheet /, "sheet ").replace(/ from A1$/, "")).join("; ");
208
- }
209
- return {
210
- ok: true,
211
- format: "csv",
212
- where,
213
- outline: `${refs.length} sheet${refs.length === 1 ? "" : "s"}: ${dims.join(", ")}`,
214
- sections: fit(shown, notes, () => 'Ask for one {"sheet"} and a {"range"} to read the rest.'),
215
- notes
216
- };
217
- }
218
- async function readWord(req, locator) {
219
- if (req.rendition && locator.lines === void 0) {
220
- refuseKeys(locator, ["page", "text", "lines"], req);
221
- const read = await readPdfPages(req.rendition, req.name, locator, "page");
222
- return pagesReply(read, locator, "page", [
223
- "Read from the pages CodeTrellis shows: its rendition of this document, laid out as the person sees it.",
224
- `To cite this document, quote it: {"text": "\u2026"}. A {"page"} says where the words are on these pages, but is not checked \u2014 a Word document's pages depend on layout.`,
225
- 'Ask for {"lines": "1-200"} to read it as markdown instead, with its headings and tables.'
226
- ]);
227
- }
228
- if (locator.page !== void 0) {
229
- throw new ReadError(`${req.name} is a Word document; its pages depend on layout. Ask for {"lines"} of what this returns, or {"text"}`);
230
- }
231
- refuseKeys(locator, ["lines", "text"], req);
232
- const view = new DataView(req.bytes.buffer, req.bytes.byteOffset, req.bytes.byteLength);
233
- const entries = (0, import_zip_reader.listEntries)(view);
234
- if (!entries.some((e) => e.name === "word/document.xml")) {
235
- throw new ReadError(`${req.name} is not a Word document \u2014 it has no document part`);
236
- }
237
- const budget = { maxPart: MAX_PART_BYTES, maxTotal: MAX_TOTAL_BYTES, used: 0 };
238
- for (const e of entries) await (0, import_zip_reader.readEntry)(view, e, budget);
239
- let images = 0;
240
- const result = await import_mammoth.default.convertToHtml({ buffer: Buffer.from(req.bytes.buffer, req.bytes.byteOffset, req.bytes.byteLength) }, {
241
- // A document can link an image by path instead of embedding it. Never follow one.
242
- externalFileAccess: false,
243
- convertImage: import_mammoth.default.images.imgElement(async () => {
244
- images++;
245
- return { src: "" };
246
- })
247
- });
248
- const markdown = (0, import_docx_markdown.docxHtmlToMarkdown)(result.value);
249
- const lines = markdown.split("\n");
250
- while (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
251
- const notes = [
252
- "Converted to markdown: headings, lists, tables and emphasis survive; page layout does not.",
253
- `To cite this document, quote it: {"text": "\u2026"}. Line numbers here are this markdown's, not the document's.`
254
- ];
255
- if (images) notes.push(`${images} image${images === 1 ? "" : "s"} shown as [image]; open the document in CodeTrellis to see them.`);
256
- const { where, section } = readLines(lines, locator, { number: false, name: req.name });
257
- return {
258
- ok: true,
259
- format: "markdown",
260
- where,
261
- outline: `${lines.length} lines of markdown`,
262
- sections: fit([section], notes, () => 'Ask for {"lines": "N-M"} of this markdown to read further.'),
263
- notes
264
- };
265
- }
266
- function pageSpan(locator, count, noun, name) {
267
- if (locator.page === void 0) return null;
268
- const span = (0, import_locator.toSpan)(locator.page);
269
- if (!span || span.start < 1 || span.end < span.start) {
270
- throw new ReadError(`page must be a ${noun} number from 1, or a span such as "2-4" \u2014 got ${JSON.stringify(locator.page)}`);
271
- }
272
- if (span.start > count) throw new ReadError(`${name} has ${count} ${noun}s; ${span.start} is not one of them`);
273
- return { start: span.start, end: Math.min(span.end, count) };
274
- }
275
- function narrowByText(sections, locator, name) {
276
- if (locator.text === void 0) return sections;
277
- const quote = squash(locator.text);
278
- if (!quote) throw new ReadError("text must be the words to find");
279
- const hits = sections.filter((s) => squash(`${s.heading} ${s.body}`).includes(quote));
280
- if (hits.length === 0) throw new ReadError(`${name} does not contain "${locator.text.slice(0, 80)}"`);
281
- return hits;
282
- }
283
- async function readDeck(req, locator) {
284
- refuseKeys(locator, ["page", "text"], req);
285
- const view = new DataView(req.bytes.buffer, req.bytes.byteOffset, req.bytes.byteLength);
286
- const entries = (0, import_zip_reader.listEntries)(view);
287
- const names = entries.map((e) => e.name);
288
- if (!names.includes("ppt/presentation.xml")) throw new ReadError(`${req.name} is not a PowerPoint deck \u2014 it has no presentation part`);
289
- const budget = { maxPart: MAX_PART_BYTES, maxTotal: MAX_TOTAL_BYTES, used: 0 };
290
- const order = (0, import_pptx_xml.slideOrder)(
291
- await (0, import_zip_reader.readText)(view, entries, "ppt/presentation.xml", budget),
292
- await (0, import_zip_reader.readText)(view, entries, "ppt/_rels/presentation.xml.rels", budget),
293
- names
294
- );
295
- const notes = ["Each slide's title and words in presentation order; pictures and charts are not described."];
296
- if (req.rendition) {
297
- const read = await readPdfPages(req.rendition, req.name, locator, "slide", order.length);
298
- if (read) {
299
- return pagesReply(read, locator, "slide", [
300
- "Read from the slides as CodeTrellis shows them: the words on each, laid out as the person sees it. Pictures are not described."
301
- ]);
302
- }
303
- notes.push("Read from the slides themselves: the view CodeTrellis shows leaves some out (hidden slides), so its pages are not numbered as the slides are.");
304
- }
305
- const span = pageSpan(locator, order.length, "slide", req.name);
306
- const first = span?.start ?? 1;
307
- const last = Math.min(span?.end ?? order.length, MAX_SLIDES);
308
- if (order.length > MAX_SLIDES && !span) notes.push(`Only the first ${MAX_SLIDES} slides are read.`);
309
- const sections = [];
310
- for (let n = first; n <= last; n++) {
311
- const xml = await (0, import_zip_reader.readText)(view, entries, order[n - 1], budget);
312
- const s = xml ? (0, import_pptx_xml.slideText)(n, xml) : { n, title: null, paragraphs: [] };
313
- sections.push({ heading: `Slide ${n}${s.title ? ` \u2014 ${s.title}` : ""}`, body: s.paragraphs.join("\n") || "(no words)" });
314
- }
315
- const shown = narrowByText(sections, locator, req.name);
316
- const where = locator.text !== void 0 ? shown.map((s) => s.heading.split(" \u2014 ")[0].toLowerCase()).join(", ") : span ? `slide${span.start === span.end ? "" : "s"} ${dash(span.start, span.end)}` : null;
317
- return {
318
- ok: true,
319
- format: "markdown",
320
- where,
321
- outline: `${order.length} slide${order.length === 1 ? "" : "s"}`,
322
- sections: fit(shown, notes, () => 'Ask for {"page": "N-M"} to read further slides.'),
323
- notes
324
- };
325
- }
326
- let pdfjsLoaded = null;
327
- function loadPdfJs() {
328
- pdfjsLoaded ??= (async () => {
329
- globalThis.pdfjsWorker = await Promise.resolve().then(() => __toESM(require("pdfjs-dist/legacy/build/pdf.worker.mjs")));
330
- return await Promise.resolve().then(() => __toESM(require("pdfjs-dist/legacy/build/pdf.mjs")));
331
- })();
332
- return pdfjsLoaded;
333
- }
334
- async function readPdfPages(bytes, name, locator, noun, expectPages) {
335
- const Noun = noun === "page" ? "Page" : "Slide";
336
- const pdfjs = await loadPdfJs();
337
- const task = pdfjs.getDocument({
338
- data: bytes,
339
- isEvalSupported: false,
340
- enableXfa: false,
341
- disableFontFace: true,
342
- useSystemFonts: false,
343
- stopAtErrors: false,
344
- verbosity: 0
345
- });
346
- try {
347
- let doc;
348
- try {
349
- doc = await task.promise;
350
- } catch (err) {
351
- const errName = err.name;
352
- throw new ReadError(errName === "PasswordException" ? `${name} is password-protected, so its text cannot be read` : `${name} could not be read as a PDF (${err.message})`);
353
- }
354
- if (expectPages !== void 0 && doc.numPages !== expectPages) return null;
355
- const span = pageSpan(locator, doc.numPages, noun, name);
356
- const notes = [];
357
- const first = span?.start ?? 1;
358
- const last = Math.min(span?.end ?? doc.numPages, first + MAX_PDF_PAGES - 1);
359
- if (!span && doc.numPages > MAX_PDF_PAGES) notes.push(`Only the first ${MAX_PDF_PAGES} ${noun}s are read.`);
360
- const sections = [];
361
- let chars = 0;
362
- for (let n = first; n <= last; n++) {
363
- const page = await doc.getPage(n);
364
- const content = await page.getTextContent();
365
- const body = content.items.map((it) => (it.str ?? "") + (it.hasEOL ? "\n" : "")).join("").replace(/[ \t]+\n/g, "\n").trim();
366
- page.cleanup();
367
- sections.push({ heading: `${Noun} ${n}`, body: body || (noun === "page" ? "(no text on this page \u2014 it may be a scanned image)" : "(no text on this slide \u2014 it may be a picture)") });
368
- chars += body.length;
369
- if (chars > MAX_OUTPUT_CHARS * 2 && locator.text === void 0) break;
370
- }
371
- return { count: doc.numPages, span, sections: narrowByText(sections, locator, name), notes };
372
- } finally {
373
- await task.destroy().catch(() => {
374
- });
375
- }
376
- }
377
- function pagesReply(read, locator, noun, notes) {
378
- const { count, span, sections } = read;
379
- const all = [...notes, ...read.notes];
380
- const where = locator.text !== void 0 ? sections.map((s) => s.heading.toLowerCase()).join(", ") : span ? `${noun}${span.start === span.end ? "" : "s"} ${dash(span.start, span.end)}` : null;
381
- return {
382
- ok: true,
383
- format: "text",
384
- where,
385
- outline: `${count} ${noun}${count === 1 ? "" : "s"}`,
386
- sections: fit(sections, all, () => `Ask for {"page": "N-M"} to read further ${noun}s.`),
387
- notes: all
388
- };
389
- }
390
- async function readPdf(req, locator) {
391
- refuseKeys(locator, ["page", "text"], req);
392
- const read = await readPdfPages(req.bytes, req.name, locator, "page");
393
- return pagesReply(read, locator, "page", []);
394
- }
395
- function refuseKeys(locator, allowed, req) {
396
- const extra = Object.keys(locator).filter((k) => locator[k] !== void 0 && !allowed.includes(k));
397
- if (extra.length) {
398
- throw new ReadError(`A .${req.ext} file is read by ${allowed.map((k) => `{${k}}`).join(", ")} \u2014 not by ${extra.map((k) => `{${k}}`).join(", ")}`);
399
- }
400
- }
401
- async function readMaterialBytes(input) {
402
- const view = (b) => new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
403
- const req = { ...input, bytes: view(input.bytes), rendition: input.rendition ? view(input.rendition) : null };
404
- const locator = req.locator ?? {};
405
- try {
406
- switch (req.ext) {
407
- case "xlsx":
408
- case "xlsm":
409
- return await readWorkbook(req, locator);
410
- case "docx":
411
- return await readWord(req, locator);
412
- case "pptx":
413
- return await readDeck(req, locator);
414
- case "pdf":
415
- return await readPdf(req, locator);
416
- default:
417
- if (req.ext === "csv" && locator.range !== void 0) return readCsvRange(req, locator);
418
- if (import_locator.TEXT_EXTS.has(req.ext)) return readTextFile(req, locator);
419
- return { ok: false, reason: `.${req.ext} files are not read as text` };
420
- }
421
- } catch (err) {
422
- if (err instanceof ReadError || err instanceof import_zip_reader.CapError) return { ok: false, reason: err.message };
423
- return { ok: false, reason: `${req.name} could not be read as a .${req.ext} file (${err.message})` };
424
- }
425
- }
426
- // Annotate the CommonJS export names for ESM import in node:
427
- 0 && (module.exports = {
428
- MAX_OUTPUT_CHARS,
429
- readMaterialBytes
430
- });