@lucasschirm/devinp 0.1.13 → 0.1.14
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 +14 -6
- package/bin/command.js +52 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,24 +31,32 @@ when a script calls it: the model list comes back paged ("click to show more")
|
|
|
31
31
|
and the JSON never reaches stdout, so `devinp` used to die with *"returned
|
|
32
32
|
invalid JSON (… 0 bytes)"*. It now works harder to get the whole document:
|
|
33
33
|
|
|
34
|
-
-
|
|
35
|
-
`devin`
|
|
36
|
-
real
|
|
34
|
+
- **A pipe first, so `TMPDIR` cannot break anything.** The happy path captures
|
|
35
|
+
`devin`'s stdout over a pipe — no scratch file, no permissions, no sandbox.
|
|
36
|
+
Measured against the real CLI, the piped document is byte-identical to the
|
|
37
|
+
file capture (471 855 bytes, 46 families), so the old "pipes truncate" worry
|
|
38
|
+
that put a temp file first was misplaced; an unwritable or missing `TMPDIR`
|
|
39
|
+
used to cost two extra `devin` runs, and a missing one even reported *"`devin`
|
|
40
|
+
CLI not found in PATH"*.
|
|
37
41
|
- **Retries that take the terminal away from `devin`.** If an attempt yields no
|
|
38
42
|
usable JSON, `devinp` runs `devin` again, each time stripping more of the
|
|
39
43
|
interactive terminal off it:
|
|
40
44
|
|
|
41
45
|
| # | stdout goes to | environment | controlling terminal |
|
|
42
46
|
| - | ------------ | -------------------------------------------------------- | --------------------- |
|
|
43
|
-
| 1 |
|
|
44
|
-
| 2 |
|
|
45
|
-
| 3 |
|
|
47
|
+
| 1 | pipe | yours, untouched | yes — the healthy path, one `devin` call |
|
|
48
|
+
| 2 | pipe | `PAGER=cat`, `GIT_PAGER=cat`, `TERM=dumb`, `NO_COLOR=1`, `CI=1` | **none** |
|
|
49
|
+
| 3 | scratch file | same | **none** |
|
|
46
50
|
|
|
47
51
|
Spawned in its own session, `devin` cannot open `/dev/tty`: a pager or a TUI
|
|
48
52
|
that paints a "click to show more" view onto the terminal has nowhere to
|
|
49
53
|
draw, so the document has to come out of stdout. Nothing lands on the screen
|
|
50
54
|
you are typing in, and Ctrl-C there no longer kills the lookup. The first
|
|
51
55
|
attempt that parses wins, so the healthy path is unchanged.
|
|
56
|
+
- **The scratch file (attempt 3) walks candidate directories** — `DEVINP_TMP`,
|
|
57
|
+
`os.tmpdir()`, `~/.devinp`, then the current directory — and uses the first
|
|
58
|
+
one it can actually write, removing the file afterwards. Set `DEVINP_TMP` to
|
|
59
|
+
pin it somewhere you control.
|
|
52
60
|
- Noise around the document is tolerated: ANSI colours, an update banner in
|
|
53
61
|
front, or the `pass a slug to --model` hint behind are stripped before the
|
|
54
62
|
JSON is parsed.
|
package/bin/command.js
CHANGED
|
@@ -26885,7 +26885,7 @@ var require_sonic_boom = __commonJS({
|
|
|
26885
26885
|
if (!(this instanceof SonicBoom)) {
|
|
26886
26886
|
return new SonicBoom(opts);
|
|
26887
26887
|
}
|
|
26888
|
-
let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir, retryEAGAIN, fsync, contentMode, mode } = opts || {};
|
|
26888
|
+
let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir: mkdir2, retryEAGAIN, fsync, contentMode, mode } = opts || {};
|
|
26889
26889
|
fd = fd || dest;
|
|
26890
26890
|
this._len = 0;
|
|
26891
26891
|
this.fd = -1;
|
|
@@ -26910,7 +26910,7 @@ var require_sonic_boom = __commonJS({
|
|
|
26910
26910
|
this.append = append || false;
|
|
26911
26911
|
this.mode = mode;
|
|
26912
26912
|
this.retryEAGAIN = retryEAGAIN || (() => true);
|
|
26913
|
-
this.mkdir =
|
|
26913
|
+
this.mkdir = mkdir2 || false;
|
|
26914
26914
|
let fsWriteSync;
|
|
26915
26915
|
let fsWrite;
|
|
26916
26916
|
if (contentMode === kContentModeBuffer) {
|
|
@@ -55024,8 +55024,8 @@ async function startServer({ rows, benchMeta, port, log }) {
|
|
|
55024
55024
|
|
|
55025
55025
|
// src/lib/source.js
|
|
55026
55026
|
import { spawn } from "node:child_process";
|
|
55027
|
-
import { open, readFile as readFile2, unlink } from "node:fs/promises";
|
|
55028
|
-
import { tmpdir } from "node:os";
|
|
55027
|
+
import { mkdir, open, readFile as readFile2, unlink } from "node:fs/promises";
|
|
55028
|
+
import { homedir, tmpdir } from "node:os";
|
|
55029
55029
|
import { join as join2 } from "node:path";
|
|
55030
55030
|
|
|
55031
55031
|
// src/lib/repair-json.js
|
|
@@ -55097,9 +55097,9 @@ var NON_INTERACTIVE_ENV = {
|
|
|
55097
55097
|
};
|
|
55098
55098
|
var DETACHED_SPAWN = { detached: true, windowsHide: true };
|
|
55099
55099
|
var STRATEGIES = [
|
|
55100
|
-
{ label: "
|
|
55101
|
-
{ label: "
|
|
55102
|
-
{ label: "
|
|
55100
|
+
{ label: "pipe", file: false, env: null },
|
|
55101
|
+
{ label: "pipe, paging disabled, no controlling terminal", file: false, env: NON_INTERACTIVE_ENV, detached: true },
|
|
55102
|
+
{ label: "temp file, paging disabled, no controlling terminal", file: true, env: NON_INTERACTIVE_ENV, detached: true }
|
|
55103
55103
|
];
|
|
55104
55104
|
var ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][\s\S]*?(?:\u0007|\u001b\\)|\u001b[@-Z\\-_]/g;
|
|
55105
55105
|
function stripAnsi(text) {
|
|
@@ -55142,6 +55142,7 @@ function spawnDevin({ command, env, stdout, timeoutMs, detached = false }) {
|
|
|
55142
55142
|
stderr += chunk;
|
|
55143
55143
|
});
|
|
55144
55144
|
child.on("error", (error) => {
|
|
55145
|
+
error.devinSpawn = true;
|
|
55145
55146
|
clearTimeout(timer);
|
|
55146
55147
|
reject(error);
|
|
55147
55148
|
});
|
|
@@ -55151,9 +55152,39 @@ function spawnDevin({ command, env, stdout, timeoutMs, detached = false }) {
|
|
|
55151
55152
|
});
|
|
55152
55153
|
});
|
|
55153
55154
|
}
|
|
55155
|
+
async function writableScratchDir() {
|
|
55156
|
+
const candidates = [];
|
|
55157
|
+
if (process.env.DEVINP_TMP) candidates.push(process.env.DEVINP_TMP);
|
|
55158
|
+
candidates.push(tmpdir(), join2(homedir(), ".devinp"), process.cwd());
|
|
55159
|
+
for (const dir of candidates) {
|
|
55160
|
+
try {
|
|
55161
|
+
await mkdir(dir, { recursive: true });
|
|
55162
|
+
const probe = join2(dir, `.devinp-probe-${process.pid}`);
|
|
55163
|
+
const handle = await open(probe, "w");
|
|
55164
|
+
await handle.close();
|
|
55165
|
+
await unlink(probe);
|
|
55166
|
+
return dir;
|
|
55167
|
+
} catch {
|
|
55168
|
+
}
|
|
55169
|
+
}
|
|
55170
|
+
return null;
|
|
55171
|
+
}
|
|
55154
55172
|
async function captureToFile({ command, env, timeoutMs, detached }) {
|
|
55155
|
-
const
|
|
55156
|
-
|
|
55173
|
+
const dir = await writableScratchDir();
|
|
55174
|
+
if (!dir) {
|
|
55175
|
+
const error = new Error(`cannot write a scratch file \u2014 DEVINP_TMP, os.tmpdir() ("${tmpdir()}"), ~/.devinp and "${process.cwd()}" all refused`);
|
|
55176
|
+
error.scratchPath = dir;
|
|
55177
|
+
throw error;
|
|
55178
|
+
}
|
|
55179
|
+
const file = join2(dir, `devinp-models-${process.pid}-${Date.now()}.json`);
|
|
55180
|
+
let handle;
|
|
55181
|
+
try {
|
|
55182
|
+
handle = await open(file, "w");
|
|
55183
|
+
} catch (error) {
|
|
55184
|
+
error.message = `cannot write scratch file "${file}" (${error.code ?? error.message})`;
|
|
55185
|
+
error.scratchPath = file;
|
|
55186
|
+
throw error;
|
|
55187
|
+
}
|
|
55157
55188
|
try {
|
|
55158
55189
|
const result = await spawnDevin({ command, env, stdout: handle.fd, timeoutMs, detached });
|
|
55159
55190
|
result.stdout = await readFile2(file, "utf8");
|
|
@@ -55171,7 +55202,7 @@ async function runDevinJson({ command = "devin", timeoutMs = DEVIN_TIMEOUT_MS }
|
|
|
55171
55202
|
try {
|
|
55172
55203
|
result = strategy.file ? await captureToFile({ command, env: strategy.env, timeoutMs, detached: strategy.detached }) : await spawnDevin({ command, env: strategy.env, stdout: "pipe", timeoutMs, detached: strategy.detached });
|
|
55173
55204
|
} catch (error) {
|
|
55174
|
-
if (error.code === "ENOENT") throw error;
|
|
55205
|
+
if (error.code === "ENOENT" && error.devinSpawn) throw error;
|
|
55175
55206
|
attempts.push({ label: strategy.label, bytes: 0, code: null, stderr: "", timedOut: false, note: error.message });
|
|
55176
55207
|
continue;
|
|
55177
55208
|
}
|
|
@@ -55185,7 +55216,7 @@ async function runDevinJson({ command = "devin", timeoutMs = DEVIN_TIMEOUT_MS }
|
|
|
55185
55216
|
timedOut: result.timedOut,
|
|
55186
55217
|
raw
|
|
55187
55218
|
});
|
|
55188
|
-
if (parsed != null) return { raw, parsed, attempts, timeoutMs };
|
|
55219
|
+
if (parsed != null) return { raw, parsed, attempts, timeoutMs, via: strategy.label };
|
|
55189
55220
|
if (index === 0 && result.code !== 0 && !result.timedOut && result.stderr.trim()) {
|
|
55190
55221
|
throw new Error(`\`devin models list\` failed \u2014 ${result.stderr.trim()}`);
|
|
55191
55222
|
}
|
|
@@ -55194,8 +55225,8 @@ async function runDevinJson({ command = "devin", timeoutMs = DEVIN_TIMEOUT_MS }
|
|
|
55194
55225
|
if (explained && attempts.every((a2) => a2.timedOut || a2.code !== 0)) {
|
|
55195
55226
|
throw new Error(`\`devin models list\` failed \u2014 ${explained.stderr}`);
|
|
55196
55227
|
}
|
|
55197
|
-
const longest = attempts.reduce((acc, a2) => (a2.raw?.length ?? 0)
|
|
55198
|
-
return { raw: longest || "", parsed: null, attempts, timeoutMs };
|
|
55228
|
+
const longest = attempts.reduce((acc, a2) => (a2.raw?.length ?? 0) >= acc.length ? a2.raw : acc, "");
|
|
55229
|
+
return { raw: longest || "", parsed: null, attempts, timeoutMs, via: null };
|
|
55199
55230
|
}
|
|
55200
55231
|
function assertFamilies(data, sourceName) {
|
|
55201
55232
|
if (data == null || typeof data !== "object" || !Array.isArray(data.families)) {
|
|
@@ -55238,6 +55269,9 @@ function describeFailure(attempts, timeoutMs) {
|
|
|
55238
55269
|
);
|
|
55239
55270
|
}
|
|
55240
55271
|
lines.push(`Tried: ${attempts.map((a2) => describeAttempt(a2, timeoutMs)).join("; ")}.`);
|
|
55272
|
+
if (attempts.some((a2) => a2.note?.includes("scratch"))) {
|
|
55273
|
+
lines.push("devinp could not write its scratch file \u2014 point it somewhere writable with `DEVINP_TMP=/tmp devinp`.");
|
|
55274
|
+
}
|
|
55241
55275
|
lines.push("Capture the JSON yourself and hand it to devinp \u2014 piping or redirecting makes `devin` write the whole document in one go:");
|
|
55242
55276
|
lines.push(" devin models list --format json > models.json && devinp --source models.json");
|
|
55243
55277
|
lines.push(" devin models list --format json | devinp --source=-");
|
|
@@ -55264,7 +55298,7 @@ async function loadModelData({
|
|
|
55264
55298
|
throw new Error(`Unable to get model prices: cannot read ${source === "-" ? "stdin" : `source file "${source}"`} (${error.code ?? error.message})`);
|
|
55265
55299
|
}
|
|
55266
55300
|
try {
|
|
55267
|
-
return { data: assertFamilies(JSON.parse(raw), label), retries: 0 };
|
|
55301
|
+
return { data: assertFamilies(JSON.parse(raw), label), retries: 0, via: source === "-" ? "stdin" : "file" };
|
|
55268
55302
|
} catch (error) {
|
|
55269
55303
|
if (error.message.startsWith("Unable to get model prices")) throw error;
|
|
55270
55304
|
throw new Error(`Unable to get model prices: ${label} is not valid JSON (expected \`devin models list --format json\` output)`);
|
|
@@ -55274,17 +55308,17 @@ async function loadModelData({
|
|
|
55274
55308
|
try {
|
|
55275
55309
|
outcome = await runDevinJson({ command, timeoutMs });
|
|
55276
55310
|
} catch (error) {
|
|
55277
|
-
if (error.code === "ENOENT") {
|
|
55311
|
+
if (error.code === "ENOENT" && error.devinSpawn) {
|
|
55278
55312
|
throw new Error("Unable to get model prices: `devin` CLI not found in PATH. Install it, or pass --source <file> / --source=- to supply the JSON yourself.");
|
|
55279
55313
|
}
|
|
55280
55314
|
throw new Error(`Unable to get model prices: ${error.message}`);
|
|
55281
55315
|
}
|
|
55282
55316
|
if (outcome.parsed != null) {
|
|
55283
|
-
return { data: assertFamilies(outcome.parsed, "devin models list"), retries: outcome.attempts.length - 1 };
|
|
55317
|
+
return { data: assertFamilies(outcome.parsed, "devin models list"), retries: outcome.attempts.length - 1, via: outcome.via };
|
|
55284
55318
|
}
|
|
55285
55319
|
const recovered = recoverTruncatedJson(outcome.raw);
|
|
55286
55320
|
if (recovered != null) {
|
|
55287
|
-
return { data: assertFamilies(recovered, "devin models list (recovered)"), truncated: true, retries: outcome.attempts.length - 1 };
|
|
55321
|
+
return { data: assertFamilies(recovered, "devin models list (recovered)"), truncated: true, retries: outcome.attempts.length - 1, via: outcome.via ?? "repaired" };
|
|
55288
55322
|
}
|
|
55289
55323
|
throw new Error(describeFailure(outcome.attempts, outcome.timeoutMs));
|
|
55290
55324
|
}
|
|
@@ -55345,7 +55379,7 @@ var Devinp = class _Devinp extends import_core.Command {
|
|
|
55345
55379
|
this.warn("devin output was truncated mid-stream \u2014 showing partial data, re-run for the full list");
|
|
55346
55380
|
}
|
|
55347
55381
|
if (loaded.retries > 0) {
|
|
55348
|
-
this.warn(`devin needed ${loaded.retries + 1} attempts to hand over the model list \u2014
|
|
55382
|
+
this.warn(`devin needed ${loaded.retries + 1} attempts to hand over the model list \u2014 the data arrived via ${loaded.via ?? "a fallback"} (something on this machine interferes with \`devin models list\`; DEVINP_TMP sets the scratch directory)`);
|
|
55349
55383
|
}
|
|
55350
55384
|
data = loaded.data;
|
|
55351
55385
|
} catch (error) {
|