@lucasschirm/devinp 0.1.10 → 0.1.13

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 (3) hide show
  1. package/README.md +57 -12
  2. package/bin/command.js +186 -68
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -24,18 +24,48 @@ The package ships as a self-contained bundle — no dependencies are installed.
24
24
  not authenticated, `devinp` exits with an error (use `--source` for a saved
25
25
  snapshot).
26
26
 
27
- ### When `devin` fails or truncates
28
-
29
- - Prices are captured through a temp file in `os.tmpdir()` — `devin`'s
30
- interactive CLI can truncate a pipe stream mid-write, so `devinp` points
31
- `devin`'s stdout at a real file instead. The temp file is always removed
32
- after the run.
27
+ ### When `devin` fails, pages, or truncates
28
+
29
+ `devin` is an interactive CLI, and on some machines it stays interactive even
30
+ when a script calls it: the model list comes back paged ("click to show more")
31
+ and the JSON never reaches stdout, so `devinp` used to die with *"returned
32
+ invalid JSON (… 0 bytes)"*. It now works harder to get the whole document:
33
+
34
+ - Prices are captured through a temp file in `os.tmpdir()` — a pipe stream from
35
+ `devin` can be truncated mid-write, so `devinp` points `devin`'s stdout at a
36
+ real file first. The temp file is always removed after the run.
37
+ - **Retries that take the terminal away from `devin`.** If an attempt yields no
38
+ usable JSON, `devinp` runs `devin` again, each time stripping more of the
39
+ interactive terminal off it:
40
+
41
+ | # | stdout goes to | environment | controlling terminal |
42
+ | - | ------------ | -------------------------------------------------------- | --------------------- |
43
+ | 1 | temp file | yours, untouched | yes — the healthy path, one `devin` call |
44
+ | 2 | temp file | `PAGER=cat`, `GIT_PAGER=cat`, `TERM=dumb`, `NO_COLOR=1`, `CI=1` | **none** |
45
+ | 3 | pipe | same | **none** |
46
+
47
+ Spawned in its own session, `devin` cannot open `/dev/tty`: a pager or a TUI
48
+ that paints a "click to show more" view onto the terminal has nowhere to
49
+ draw, so the document has to come out of stdout. Nothing lands on the screen
50
+ you are typing in, and Ctrl-C there no longer kills the lookup. The first
51
+ attempt that parses wins, so the healthy path is unchanged.
52
+ - Noise around the document is tolerated: ANSI colours, an update banner in
53
+ front, or the `pass a slug to --model` hint behind are stripped before the
54
+ JSON is parsed.
33
55
  - If `devin`'s output is cut off mid-stream, `devinp` repairs the document,
34
56
  prints a warning, and renders the partial table.
35
- - If the output cannot be recovered, `devinp` reports a diagnostic with the
36
- size and tail of what `devin` wrote, and suggests re-running or saving the
37
- output manually: `devin models list --format json > models.json`, then
38
- `devinp --source models.json`.
57
+ - A real CLI failure (not signed in, network down) is reported as-is instead of
58
+ being retried.
59
+ - If nothing worked, the error lists all three attempts — bytes, exit code,
60
+ stderr, timeout — and how to feed the data in yourself. That always works,
61
+ because a redirect or a pipe makes `devin` write the whole document in one go:
62
+ (and if it does not, `command -v devin` tells you whether devinp is even
63
+ talking to the CLI and not to a shell function or agent wrapper around it).
64
+
65
+ ```sh
66
+ devin models list --format json > models.json && devinp --source models.json
67
+ devin models list --format json | devinp --source=-
68
+ ```
39
69
 
40
70
  ## Usage
41
71
 
@@ -55,6 +85,7 @@ devinp --web # serve an interactive page on http://127.0.0.1:432
55
85
  devinp --web --port 8080 # pick a port
56
86
 
57
87
  devinp --source models.json # read a saved `devin models list --format json`
88
+ devin models list --format json | devinp --source=- # feed it straight from stdin
58
89
  devinp --json # emit normalized rows as JSON
59
90
  ```
60
91
 
@@ -106,11 +137,25 @@ so `npm publish`, git-URL installs, and `npm link` always ship a fresh bundle.
106
137
 
107
138
  ## Publishing
108
139
 
109
- `.github/workflows/publish.yml` bumps the patch version and publishes with
110
- [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC —
140
+ `.github/workflows/publish.yml` publishes a patch release on every push to main
141
+ with [npm trusted publishing](https://docs.npmjs.org/trusted-publishers) (OIDC —
111
142
  no `NPM_TOKEN`). Requires the trusted publisher to be configured on npmjs.com:
112
143
  repository `lucasschirm/devinp`, workflow `publish.yml`.
113
144
 
145
+ It **never commits to main** — the branch ruleset requires every change to
146
+ arrive through a pull request, and a rejected release commit used to strand the
147
+ publish step that ran after it (GH013, then `npm publish` skipped). The released
148
+ version therefore lives in the npm registry and in the git tag, not in main's
149
+ `package.json`:
150
+
151
+ ```sh
152
+ git ls-remote --tags origin # released versions
153
+ npm view @lucasschirm/devinp version # latest published
154
+ ```
155
+
156
+ The workflow takes the first patch number that is free in both places, so a tag
157
+ left behind by an older run cannot wedge it.
158
+
114
159
  ## License
115
160
 
116
161
  MIT
package/bin/command.js CHANGED
@@ -4193,14 +4193,14 @@ var require_emoji_regex = __commonJS({
4193
4193
  var require_string_width = __commonJS({
4194
4194
  "node_modules/string-width/index.js"(exports, module) {
4195
4195
  "use strict";
4196
- var stripAnsi = require_strip_ansi();
4196
+ var stripAnsi2 = require_strip_ansi();
4197
4197
  var isFullwidthCodePoint = require_is_fullwidth_code_point();
4198
4198
  var emojiRegex = require_emoji_regex();
4199
4199
  var stringWidth = (string) => {
4200
4200
  if (typeof string !== "string" || string.length === 0) {
4201
4201
  return 0;
4202
4202
  }
4203
- string = stripAnsi(string);
4203
+ string = stripAnsi2(string);
4204
4204
  if (string.length === 0) {
4205
4205
  return 0;
4206
4206
  }
@@ -5332,7 +5332,7 @@ var require_wrap_ansi = __commonJS({
5332
5332
  "node_modules/wrap-ansi/index.js"(exports, module) {
5333
5333
  "use strict";
5334
5334
  var stringWidth = require_string_width();
5335
- var stripAnsi = require_strip_ansi();
5335
+ var stripAnsi2 = require_strip_ansi();
5336
5336
  var ansiStyles = require_ansi_styles();
5337
5337
  var ESCAPES = /* @__PURE__ */ new Set([
5338
5338
  "\x1B",
@@ -5351,7 +5351,7 @@ var require_wrap_ansi = __commonJS({
5351
5351
  const characters = [...word];
5352
5352
  let isInsideEscape = false;
5353
5353
  let isInsideLinkEscape = false;
5354
- let visible2 = stringWidth(stripAnsi(rows[rows.length - 1]));
5354
+ let visible2 = stringWidth(stripAnsi2(rows[rows.length - 1]));
5355
5355
  for (const [index, character] of characters.entries()) {
5356
5356
  const characterLength = stringWidth(character);
5357
5357
  if (visible2 + characterLength <= columns) {
@@ -13355,7 +13355,7 @@ var require_parse2 = __commonJS({
13355
13355
  const parts = [];
13356
13356
  let bracket = 0;
13357
13357
  let paren = 0;
13358
- let quote = 0;
13358
+ let quote2 = 0;
13359
13359
  let value = "";
13360
13360
  let escaped = false;
13361
13361
  for (const ch of input) {
@@ -13370,11 +13370,11 @@ var require_parse2 = __commonJS({
13370
13370
  continue;
13371
13371
  }
13372
13372
  if (ch === '"') {
13373
- quote = quote === 1 ? 0 : 1;
13373
+ quote2 = quote2 === 1 ? 0 : 1;
13374
13374
  value += ch;
13375
13375
  continue;
13376
13376
  }
13377
- if (quote === 0) {
13377
+ if (quote2 === 0) {
13378
13378
  if (ch === "[") {
13379
13379
  bracket++;
13380
13380
  } else if (ch === "]" && bracket > 0) {
@@ -13451,7 +13451,7 @@ var require_parse2 = __commonJS({
13451
13451
  }
13452
13452
  let bracket = 0;
13453
13453
  let paren = 0;
13454
- let quote = 0;
13454
+ let quote2 = 0;
13455
13455
  let escaped = false;
13456
13456
  for (let i = 1; i < pattern.length; i++) {
13457
13457
  const ch = pattern[i];
@@ -13464,10 +13464,10 @@ var require_parse2 = __commonJS({
13464
13464
  continue;
13465
13465
  }
13466
13466
  if (ch === '"') {
13467
- quote = quote === 1 ? 0 : 1;
13467
+ quote2 = quote2 === 1 ? 0 : 1;
13468
13468
  continue;
13469
13469
  }
13470
- if (quote === 1) {
13470
+ if (quote2 === 1) {
13471
13471
  continue;
13472
13472
  }
13473
13473
  if (ch === "[") {
@@ -17633,7 +17633,7 @@ var require_parse3 = __commonJS({
17633
17633
  debug = () => {
17634
17634
  };
17635
17635
  }
17636
- var readStdin = async () => {
17636
+ var readStdin2 = async () => {
17637
17637
  const { stdin, stdout } = process;
17638
17638
  if (stdin.isTTY)
17639
17639
  return null;
@@ -17669,7 +17669,7 @@ var require_parse3 = __commonJS({
17669
17669
  }, { once: true });
17670
17670
  });
17671
17671
  };
17672
- exports.readStdin = readStdin;
17672
+ exports.readStdin = readStdin2;
17673
17673
  function isNegativeNumber(input) {
17674
17674
  return /^-\d/g.test(input);
17675
17675
  }
@@ -41394,15 +41394,15 @@ var require_quote = __commonJS({
41394
41394
  '"': '\\"',
41395
41395
  "\\": "\\\\"
41396
41396
  };
41397
- function quote(s) {
41397
+ function quote2(s) {
41398
41398
  rxEscapable.lastIndex = 0;
41399
41399
  return '"' + (rxEscapable.test(s) ? s.replace(rxEscapable, (a2) => {
41400
41400
  const c = escaped[a2];
41401
41401
  return typeof c === "string" ? c : "\\u" + ("0000" + a2.charCodeAt(0).toString(16)).slice(-4);
41402
41402
  }) : s) + '"';
41403
41403
  }
41404
- exports.default = quote;
41405
- quote.code = 'require("ajv/dist/runtime/quote").default';
41404
+ exports.default = quote2;
41405
+ quote2.code = 'require("ajv/dist/runtime/quote").default';
41406
41406
  }
41407
41407
  });
41408
41408
 
@@ -55088,45 +55088,115 @@ function scanJson(s, end) {
55088
55088
 
55089
55089
  // src/lib/source.js
55090
55090
  var DEVIN_TIMEOUT_MS = 3e4;
55091
- async function runDevinJson({ command = "devin", timeoutMs = DEVIN_TIMEOUT_MS } = {}) {
55091
+ var NON_INTERACTIVE_ENV = {
55092
+ PAGER: "cat",
55093
+ GIT_PAGER: "cat",
55094
+ TERM: "dumb",
55095
+ NO_COLOR: "1",
55096
+ CI: "1"
55097
+ };
55098
+ var DETACHED_SPAWN = { detached: true, windowsHide: true };
55099
+ var STRATEGIES = [
55100
+ { label: "temp file", file: true, env: null },
55101
+ { label: "temp file, paging disabled, no controlling terminal", file: true, env: NON_INTERACTIVE_ENV, detached: true },
55102
+ { label: "pipe, paging disabled, no controlling terminal", file: false, env: NON_INTERACTIVE_ENV, detached: true }
55103
+ ];
55104
+ var ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][\s\S]*?(?:\u0007|\u001b\\)|\u001b[@-Z\\-_]/g;
55105
+ function stripAnsi(text) {
55106
+ return text.replace(ANSI, "");
55107
+ }
55108
+ function parseJsonLoosely(text) {
55109
+ const clean = stripAnsi(text);
55110
+ try {
55111
+ return JSON.parse(clean);
55112
+ } catch {
55113
+ }
55114
+ const start = clean.indexOf("{");
55115
+ const end = clean.lastIndexOf("}");
55116
+ if (start >= 0 && end > start) {
55117
+ try {
55118
+ return JSON.parse(clean.slice(start, end + 1));
55119
+ } catch {
55120
+ }
55121
+ }
55122
+ return null;
55123
+ }
55124
+ function spawnDevin({ command, env, stdout, timeoutMs, detached = false }) {
55125
+ return new Promise((resolve, reject) => {
55126
+ const child = spawn(command, ["models", "list", "--format", "json"], {
55127
+ stdio: ["ignore", stdout, "pipe"],
55128
+ ...env ? { env: { ...process.env, ...env } } : {},
55129
+ ...detached ? DETACHED_SPAWN : {}
55130
+ });
55131
+ let stdoutText = "";
55132
+ let stderr = "";
55133
+ let timedOut = false;
55134
+ const timer = setTimeout(() => {
55135
+ timedOut = true;
55136
+ child.kill("SIGKILL");
55137
+ }, timeoutMs);
55138
+ child.stdout?.on("data", (chunk) => {
55139
+ stdoutText += chunk;
55140
+ });
55141
+ child.stderr.on("data", (chunk) => {
55142
+ stderr += chunk;
55143
+ });
55144
+ child.on("error", (error) => {
55145
+ clearTimeout(timer);
55146
+ reject(error);
55147
+ });
55148
+ child.on("close", (code) => {
55149
+ clearTimeout(timer);
55150
+ resolve({ stdout: stdoutText, stderr, code, timedOut });
55151
+ });
55152
+ });
55153
+ }
55154
+ async function captureToFile({ command, env, timeoutMs, detached }) {
55092
55155
  const file = join2(tmpdir(), `devinp-models-${process.pid}-${Date.now()}.json`);
55093
55156
  const handle = await open(file, "w");
55094
55157
  try {
55095
- const { code, stderr, timedOut } = await new Promise((resolve, reject) => {
55096
- const child = spawn(command, ["models", "list", "--format", "json"], {
55097
- stdio: ["ignore", handle.fd, "pipe"]
55098
- });
55099
- let stderr2 = "";
55100
- let timedOut2 = false;
55101
- const timer = setTimeout(() => {
55102
- timedOut2 = true;
55103
- child.kill("SIGKILL");
55104
- }, timeoutMs);
55105
- child.stderr.on("data", (chunk) => {
55106
- stderr2 += chunk;
55107
- });
55108
- child.on("error", (error) => {
55109
- clearTimeout(timer);
55110
- reject(error);
55111
- });
55112
- child.on("close", (childCode) => {
55113
- clearTimeout(timer);
55114
- resolve({ code: childCode, stderr: stderr2, timedOut: timedOut2 });
55115
- });
55116
- });
55117
- if (timedOut) {
55118
- throw new Error(`\`devin models list\` timed out after ${timeoutMs / 1e3}s`);
55119
- }
55120
- if (code !== 0) {
55121
- throw new Error(`\`devin models list\` failed \u2014 ${stderr.trim() || `exited with code ${code}`}`);
55122
- }
55123
- return await readFile2(file, "utf8");
55158
+ const result = await spawnDevin({ command, env, stdout: handle.fd, timeoutMs, detached });
55159
+ result.stdout = await readFile2(file, "utf8");
55160
+ return result;
55124
55161
  } finally {
55125
55162
  await handle.close();
55126
55163
  await unlink(file).catch(() => {
55127
55164
  });
55128
55165
  }
55129
55166
  }
55167
+ async function runDevinJson({ command = "devin", timeoutMs = DEVIN_TIMEOUT_MS } = {}) {
55168
+ const attempts = [];
55169
+ for (const [index, strategy] of STRATEGIES.entries()) {
55170
+ let result;
55171
+ try {
55172
+ 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
+ } catch (error) {
55174
+ if (error.code === "ENOENT") throw error;
55175
+ attempts.push({ label: strategy.label, bytes: 0, code: null, stderr: "", timedOut: false, note: error.message });
55176
+ continue;
55177
+ }
55178
+ const raw = result.stdout ?? "";
55179
+ const parsed = raw.trim() ? parseJsonLoosely(raw) : null;
55180
+ attempts.push({
55181
+ label: strategy.label,
55182
+ bytes: raw.length,
55183
+ code: result.code,
55184
+ stderr: result.stderr.trim(),
55185
+ timedOut: result.timedOut,
55186
+ raw
55187
+ });
55188
+ if (parsed != null) return { raw, parsed, attempts, timeoutMs };
55189
+ if (index === 0 && result.code !== 0 && !result.timedOut && result.stderr.trim()) {
55190
+ throw new Error(`\`devin models list\` failed \u2014 ${result.stderr.trim()}`);
55191
+ }
55192
+ }
55193
+ const explained = [...attempts].reverse().find((a2) => a2.stderr);
55194
+ if (explained && attempts.every((a2) => a2.timedOut || a2.code !== 0)) {
55195
+ throw new Error(`\`devin models list\` failed \u2014 ${explained.stderr}`);
55196
+ }
55197
+ const longest = attempts.reduce((acc, a2) => (a2.raw?.length ?? 0) > acc.length ? a2.raw : acc, "");
55198
+ return { raw: longest || "", parsed: null, attempts, timeoutMs };
55199
+ }
55130
55200
  function assertFamilies(data, sourceName) {
55131
55201
  if (data == null || typeof data !== "object" || !Array.isArray(data.families)) {
55132
55202
  throw new Error(`Unable to get model prices: ${sourceName} has no "families" array (expected \`devin models list --format json\` output)`);
@@ -55136,43 +55206,87 @@ function assertFamilies(data, sourceName) {
55136
55206
  }
55137
55207
  return data;
55138
55208
  }
55139
- async function loadModelData({ source, command = "devin", timeoutMs = DEVIN_TIMEOUT_MS } = {}) {
55209
+ function quote(text) {
55210
+ return JSON.stringify(text.length > 200 ? `\u2026${text.slice(-199)}` : text);
55211
+ }
55212
+ function describeAttempt(attempt, timeoutMs) {
55213
+ const parts = [`${attempt.label}: ${attempt.bytes} bytes`];
55214
+ if (attempt.timedOut) parts.push(`timed out after ${timeoutMs / 1e3}s`);
55215
+ else parts.push(`exit ${attempt.code}`);
55216
+ if (attempt.stderr) parts.push(`stderr ${quote(attempt.stderr)}`);
55217
+ if (attempt.note) parts.push(attempt.note);
55218
+ return parts.join(", ");
55219
+ }
55220
+ function describeFailure(attempts, timeoutMs) {
55221
+ const bytes = Math.max(...attempts.map((a2) => a2.bytes), 0);
55222
+ const longest = attempts.find((a2) => a2.bytes === bytes);
55223
+ const tail = longest?.raw?.trimEnd().slice(-200) ?? "";
55224
+ const lines = [];
55225
+ if (bytes === 0) {
55226
+ lines.push(
55227
+ 'Unable to get model prices: `devin models list --format json` exited without writing the JSON to stdout (0 bytes). devinp re-ran it with paging disabled and in a session with no controlling terminal, so the CLI had no terminal to paint a "click to show more" view on \u2014 whatever swallowed the output sits between devinp and `devin` (a shell function or agent wrapper around it, or a broken install).'
55228
+ );
55229
+ } else {
55230
+ let parseError = "";
55231
+ try {
55232
+ JSON.parse(stripAnsi(longest.raw));
55233
+ } catch (error) {
55234
+ parseError = `${error.message} \u2014 `;
55235
+ }
55236
+ lines.push(
55237
+ `Unable to get model prices: \`devin models list --format json\` returned invalid JSON (${parseError}${bytes} bytes, output ends with ${JSON.stringify(tail)}). The output may have been truncated mid-stream.`
55238
+ );
55239
+ }
55240
+ lines.push(`Tried: ${attempts.map((a2) => describeAttempt(a2, timeoutMs)).join("; ")}.`);
55241
+ 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
+ lines.push(" devin models list --format json > models.json && devinp --source models.json");
55243
+ lines.push(" devin models list --format json | devinp --source=-");
55244
+ lines.push("Check `command -v devin` \u2014 if that is not the real CLI, devinp is asking the wrong program. If a plain redirect still prints a paged view, update the CLI (`devin update`).");
55245
+ return lines.join("\n");
55246
+ }
55247
+ async function readStdin() {
55248
+ const chunks = [];
55249
+ for await (const chunk of process.stdin) chunks.push(chunk);
55250
+ return Buffer.concat(chunks.map((c) => typeof c === "string" ? Buffer.from(c) : c)).toString("utf8");
55251
+ }
55252
+ async function loadModelData({
55253
+ source,
55254
+ command = "devin",
55255
+ timeoutMs = DEVIN_TIMEOUT_MS,
55256
+ readStdinFrom = readStdin
55257
+ } = {}) {
55140
55258
  if (source) {
55141
- let raw2;
55259
+ const label = source === "-" ? "stdin" : `"${source}"`;
55260
+ let raw;
55142
55261
  try {
55143
- raw2 = await readFile2(source, "utf8");
55262
+ raw = source === "-" ? await readStdinFrom() : await readFile2(source, "utf8");
55144
55263
  } catch (error) {
55145
- throw new Error(`Unable to get model prices: cannot read source file "${source}" (${error.code ?? error.message})`);
55264
+ throw new Error(`Unable to get model prices: cannot read ${source === "-" ? "stdin" : `source file "${source}"`} (${error.code ?? error.message})`);
55146
55265
  }
55147
55266
  try {
55148
- return { data: assertFamilies(JSON.parse(raw2), source) };
55267
+ return { data: assertFamilies(JSON.parse(raw), label), retries: 0 };
55149
55268
  } catch (error) {
55150
55269
  if (error.message.startsWith("Unable to get model prices")) throw error;
55151
- throw new Error(`Unable to get model prices: "${source}" is not valid JSON (expected \`devin models list --format json\` output)`);
55270
+ throw new Error(`Unable to get model prices: ${label} is not valid JSON (expected \`devin models list --format json\` output)`);
55152
55271
  }
55153
55272
  }
55154
- let raw;
55273
+ let outcome;
55155
55274
  try {
55156
- raw = await runDevinJson({ command, timeoutMs });
55275
+ outcome = await runDevinJson({ command, timeoutMs });
55157
55276
  } catch (error) {
55158
55277
  if (error.code === "ENOENT") {
55159
- throw new Error("Unable to get model prices: `devin` CLI not found in PATH. Install it or pass --source <file>.");
55278
+ 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.");
55160
55279
  }
55161
55280
  throw new Error(`Unable to get model prices: ${error.message}`);
55162
55281
  }
55163
- try {
55164
- return { data: assertFamilies(JSON.parse(raw), "devin models list") };
55165
- } catch (error) {
55166
- if (error.message.startsWith("Unable to get model prices")) throw error;
55167
- const recovered = recoverTruncatedJson(raw);
55168
- if (recovered != null) {
55169
- return { data: assertFamilies(recovered, "devin models list (recovered)"), truncated: true };
55170
- }
55171
- const tail = raw.trimEnd().slice(-200);
55172
- throw new Error(
55173
- `Unable to get model prices: \`devin models list --format json\` returned invalid JSON (${error.message} \u2014 ${raw.length} bytes, output ends with ${JSON.stringify(tail)}). The output may have been truncated mid-stream: re-run, or save the output with \`devin models list --format json > models.json\` and pass \`--source models.json\`.`
55174
- );
55282
+ if (outcome.parsed != null) {
55283
+ return { data: assertFamilies(outcome.parsed, "devin models list"), retries: outcome.attempts.length - 1 };
55284
+ }
55285
+ const recovered = recoverTruncatedJson(outcome.raw);
55286
+ if (recovered != null) {
55287
+ return { data: assertFamilies(recovered, "devin models list (recovered)"), truncated: true, retries: outcome.attempts.length - 1 };
55175
55288
  }
55289
+ throw new Error(describeFailure(outcome.attempts, outcome.timeoutMs));
55176
55290
  }
55177
55291
 
55178
55292
  // src/command.js
@@ -55189,7 +55303,8 @@ var Devinp = class _Devinp extends import_core.Command {
55189
55303
  "<%= config.bin %> -o=input",
55190
55304
  "<%= config.bin %> -o=bench -g=family",
55191
55305
  "<%= config.bin %> --web --port 8080",
55192
- "<%= config.bin %> --source sample/models.json"
55306
+ "<%= config.bin %> --source sample/models.json",
55307
+ "devin models list --format json | <%= config.bin %> --source=-"
55193
55308
  ];
55194
55309
  static flags = {
55195
55310
  order: import_core.Flags.string({
@@ -55214,7 +55329,7 @@ var Devinp = class _Devinp extends import_core.Command {
55214
55329
  dependsOn: ["web"]
55215
55330
  }),
55216
55331
  source: import_core.Flags.string({
55217
- summary: "load prices from a saved `devin models list --format json` file"
55332
+ summary: "load prices from a saved `devin models list --format json` file, or from stdin with --source=- (use it when the CLI pages its output instead of writing it to the pipe)"
55218
55333
  }),
55219
55334
  json: import_core.Flags.boolean({
55220
55335
  summary: "print normalized rows as JSON instead of a table",
@@ -55229,6 +55344,9 @@ var Devinp = class _Devinp extends import_core.Command {
55229
55344
  if (loaded.truncated) {
55230
55345
  this.warn("devin output was truncated mid-stream \u2014 showing partial data, re-run for the full list");
55231
55346
  }
55347
+ if (loaded.retries > 0) {
55348
+ this.warn(`devin needed ${loaded.retries + 1} attempts to hand over the model list \u2014 something on this machine (pager, paged "show more" view, wrapper) interferes with \`devin models list\``);
55349
+ }
55232
55350
  data = loaded.data;
55233
55351
  } catch (error) {
55234
55352
  this.error(error instanceof Error ? error.message : String(error), { exit: 1 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucasschirm/devinp",
3
- "version": "0.1.10",
3
+ "version": "0.1.13",
4
4
  "description": "Pretty pricing table for Devin models, powered by `devin models list`",
5
5
  "type": "module",
6
6
  "bin": {