@proagentstore/cli 0.4.74 → 0.4.75

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.
@@ -29,17 +29,39 @@ export const MAX_TURN_DETAIL = 240;
29
29
  export const MAX_TURN_TAIL_LINES = 60;
30
30
  export const MAX_TURN_TAIL_LINE = 400;
31
31
  export const MAX_TURN_TAIL_CHARS = 8000;
32
- /** Append one line of the turn's own output to its tail, keeping the newest within the bounds above. */
32
+ /** Leads a tail that has lost its oldest lines, with how many (#898). */
33
+ const DROPPED_LINE = /^\[(\d+) earlier lines? of this turn not kept\]$/;
34
+ /**
35
+ * Append one line of the turn's own output to its tail, keeping the newest within the bounds above.
36
+ *
37
+ * What falls off the front is COUNTED, in a first line that says so (#898): the cloud reports "the
38
+ * last N lines", and a tail that silently lost its head made N read as all the turn printed.
39
+ */
33
40
  export function appendTurnTail(tail, line) {
34
41
  const text = line.trim();
35
42
  if (!text)
36
43
  return;
44
+ const marked = tail.length ? DROPPED_LINE.exec(tail[0]) : null;
45
+ let dropped = marked ? Number(marked[1]) : 0;
46
+ if (marked)
47
+ tail.shift();
37
48
  tail.push(text.length > MAX_TURN_TAIL_LINE ? `${text.slice(0, MAX_TURN_TAIL_LINE)}…` : text);
38
- while (tail.length > MAX_TURN_TAIL_LINES)
49
+ while (tail.length > MAX_TURN_TAIL_LINES - 1) {
39
50
  tail.shift();
51
+ dropped++;
52
+ }
40
53
  let total = tail.reduce((n, l) => n + l.length + 1, 0);
41
- while (total > MAX_TURN_TAIL_CHARS && tail.length > 1)
54
+ // Room is kept for the count line itself, so the tail stays inside its total bound.
55
+ while (total > MAX_TURN_TAIL_CHARS - 48 && tail.length > 1) {
42
56
  total -= tail.shift().length + 1;
57
+ dropped++;
58
+ }
59
+ if (dropped)
60
+ tail.unshift(`[${dropped} earlier line${dropped === 1 ? "" : "s"} of this turn not kept]`);
61
+ }
62
+ /** The engine's last line within {@link MAX_TURN_DETAIL}, with an ellipsis when it was cut (#898). */
63
+ function detailOf(text) {
64
+ return text.length > MAX_TURN_DETAIL ? `${text.slice(0, MAX_TURN_DETAIL)}…` : text;
43
65
  }
44
66
  /**
45
67
  * A one-shot turn's process has exited — say what that means, and nothing more.
@@ -49,7 +71,7 @@ export function appendTurnTail(tail, line) {
49
71
  * failure would let three slow builds read as a broken CLI.
50
72
  */
51
73
  export function turnReportFromExit(code, signal, lastLine = "", now = Date.now(), tail = []) {
52
- const detail = (lastLine.trim() || tail.at(-1) || "").slice(0, MAX_TURN_DETAIL);
74
+ const detail = detailOf(lastLine.trim() || tail.at(-1) || "");
53
75
  const verdict = signal !== null ? "killed" : code === 0 ? "ok" : "failed";
54
76
  return {
55
77
  verdict,
@@ -68,7 +90,7 @@ export function turnReportFromExit(code, signal, lastLine = "", now = Date.now()
68
90
  * case that runs most.
69
91
  */
70
92
  export function turnReportFromResult(isError, detail = "", now = Date.now(), tail = []) {
71
- const text = detail.trim().slice(0, MAX_TURN_DETAIL);
93
+ const text = detailOf(detail.trim());
72
94
  return {
73
95
  verdict: isError ? "failed" : "ok",
74
96
  exitCode: null,
@@ -59,6 +59,8 @@ export class HeadlessSession {
59
59
  proc = null;
60
60
  buf = "";
61
61
  transcript = [];
62
+ /** Records trimmed off the front of {@link transcript} — said in the pane, never silent (#898). */
63
+ droppedRecords = 0;
62
64
  run = "idle";
63
65
  /** Claude Code's own session id (from the init event) — used to --resume. */
64
66
  claudeSessionId = null;
@@ -351,7 +353,8 @@ export class HeadlessSession {
351
353
  }
352
354
  /** The rendered conversation the brain/console reads (Claude's real output). */
353
355
  snapshot() {
354
- return this.transcript.join("\n");
356
+ const text = this.transcript.join("\n");
357
+ return this.droppedRecords > 0 ? `[${this.droppedRecords} earlier lines of this session's output are no longer kept]\n${text}` : text;
355
358
  }
356
359
  /** Launch (or resume) the agent process. Idempotent if already alive. */
357
360
  /**
@@ -691,6 +694,10 @@ export class HeadlessSession {
691
694
  if (this.buf.length > cap) {
692
695
  if (this.mode === "raw")
693
696
  this.pushRaw(this.buf);
697
+ // Said in the pane (#898): an event this size was discarded whole, and a reader could not
698
+ // tell a dropped tool result from one that never came.
699
+ else
700
+ this.push(`[a ${this.buf.length.toLocaleString("en-US")}-character line of engine output was too large to keep and was dropped]`);
694
701
  this.buf = "";
695
702
  }
696
703
  }
@@ -730,8 +737,14 @@ export class HeadlessSession {
730
737
  // never scraped back out of the rendered pane.
731
738
  this.turnLastLine = clean.trim();
732
739
  }
733
- if (this.transcript.length > 4000)
734
- this.transcript = this.transcript.slice(-3000);
740
+ this.trimTranscript();
741
+ }
742
+ /** Keep the newest 3,000 records once past 4,000, and COUNT what went (#898). */
743
+ trimTranscript() {
744
+ if (this.transcript.length <= 4000)
745
+ return;
746
+ this.droppedRecords += this.transcript.length - 3000;
747
+ this.transcript = this.transcript.slice(-3000);
735
748
  }
736
749
  handle(line) {
737
750
  const events = this.adapter.parseLine(line);
@@ -808,8 +821,7 @@ export class HeadlessSession {
808
821
  // Keep the in-memory transcript bounded. Counts ENTRIES, and an entry may now be a
809
822
  // multi-line block (a result, or a long assistant reply) rather than one line — the
810
823
  // character bound that matters is `MAX_PANE` in runtime.ts, applied on the way out.
811
- if (this.transcript.length > 4000)
812
- this.transcript = this.transcript.slice(-3000);
824
+ this.trimTranscript();
813
825
  return events.length > 0;
814
826
  }
815
827
  push(line) {
@@ -272,7 +272,9 @@ export function repoSearch(workDir, opts) {
272
272
  const limit = Math.max(1, Math.min(SEARCH_MAX_RESULTS, Math.floor(opts.maxResults ?? SEARCH_MAX_RESULTS)));
273
273
  const relPath = opts.path ? relative(workDir, resolveInside(workDir, opts.path)) : undefined;
274
274
  const argv = mode === "content"
275
- ? ["grep", "-n", "-I", "-i", "-F", "--untracked", "--max-count", String(SEARCH_PER_FILE), "-e", pattern]
275
+ ? // No `--max-count` (#898): git stopped at 5 per file and `total` counted only those, so a
276
+ // file with 200 matches read as one with 5. The per-file cap is applied below, COUNTED.
277
+ ["grep", "-n", "-I", "-i", "-F", "--untracked", "-e", pattern]
276
278
  : // Tracked PLUS untracked-not-ignored: a file created ten minutes ago is exactly the one
277
279
  // somebody is trying to find, and `ls-files` alone would deny it exists.
278
280
  ["ls-files", "--cached", "--others", "--exclude-standard"];
@@ -297,12 +299,33 @@ export function repoSearch(workDir, opts) {
297
299
  throw new InspectError(err.message || `git ${mode} search failed`);
298
300
  }
299
301
  const lines = out.split("\n").filter((l) => l !== "");
300
- const all = mode === "content"
301
- ? lines.map(parseGrepLine).filter((m) => m !== null)
302
- : // The pattern never reaches git here — the filter is a plain case-insensitive substring
303
- // over the path, which is what "find the file called X" actually means.
304
- lines.filter((p) => p.toLowerCase().includes(pattern.toLowerCase())).map((p) => ({ path: p }));
305
- return { mode, pattern, matches: all.slice(0, limit), shown: Math.min(all.length, limit), total: all.length, truncated: all.length > limit };
302
+ if (mode === "path") {
303
+ // The pattern never reaches git here — the filter is a plain case-insensitive substring
304
+ // over the path, which is what "find the file called X" actually means.
305
+ const all = lines.filter((p) => p.toLowerCase().includes(pattern.toLowerCase())).map((p) => ({ path: p }));
306
+ return { mode, pattern, matches: all.slice(0, limit), shown: Math.min(all.length, limit), total: all.length, truncated: all.length > limit };
307
+ }
308
+ const all = lines.map(parseGrepLine).filter((m) => m !== null);
309
+ const perFile = new Map();
310
+ const omittedPerFile = {};
311
+ const capped = all.filter((m) => {
312
+ const n = (perFile.get(m.path) ?? 0) + 1;
313
+ perFile.set(m.path, n);
314
+ if (n <= SEARCH_PER_FILE)
315
+ return true;
316
+ omittedPerFile[m.path] = (omittedPerFile[m.path] ?? 0) + 1;
317
+ return false;
318
+ });
319
+ const matches = capped.slice(0, limit);
320
+ return {
321
+ mode,
322
+ pattern,
323
+ matches,
324
+ shown: matches.length,
325
+ total: all.length,
326
+ truncated: matches.length < all.length,
327
+ ...(Object.keys(omittedPerFile).length ? { omittedPerFile } : {}),
328
+ };
306
329
  }
307
330
  /** `path:line:text` — split on the FIRST two colons only, since code is full of them. */
308
331
  function parseGrepLine(line) {
@@ -315,7 +338,9 @@ function parseGrepLine(line) {
315
338
  const n = Number.parseInt(line.slice(first + 1, second), 10);
316
339
  if (!Number.isFinite(n))
317
340
  return null;
318
- return { path: line.slice(0, first), line: n, text: line.slice(second + 1).trim().slice(0, SEARCH_MAX_LINE) };
341
+ const text = line.slice(second + 1).trim();
342
+ // Marked (#898): a cut line read as the whole line. `repo_read_file` with `startLine` has the rest.
343
+ return { path: line.slice(0, first), line: n, text: text.length > SEARCH_MAX_LINE ? `${text.slice(0, SEARCH_MAX_LINE)}…` : text };
319
344
  }
320
345
  /** The deepest `maxDepth` this will honour. Named because the TOOL has to say it (#508). */
321
346
  export const TREE_MAX_DEPTH = 4;
@@ -342,6 +367,10 @@ export function repoTree(workDir, relPath = ".", maxDepth = 3, maxEntries = 500)
342
367
  const queue = [{ dir: start, depth: 0 }];
343
368
  let truncated = false;
344
369
  let truncatedByDepth = false;
370
+ // Dot-entries ARE listed now (#898): `.github/`, `.env.example`, `.claude/` were hidden with
371
+ // nothing saying so. What is still skipped is named in `skipped`, so absence is never a claim.
372
+ const skipped = new Set();
373
+ const result = () => ({ root: relPath, entries, truncated, truncatedByDepth, depthCap, ...(skipped.size ? { skipped: [...skipped].sort() } : {}) });
345
374
  while (queue.length) {
346
375
  const { dir, depth } = queue.shift();
347
376
  let items;
@@ -352,11 +381,13 @@ export function repoTree(workDir, relPath = ".", maxDepth = 3, maxEntries = 500)
352
381
  continue;
353
382
  }
354
383
  for (const it of items) {
355
- if (it.name.startsWith(".") || IGNORE_DIRS.has(it.name))
384
+ if (IGNORE_DIRS.has(it.name)) {
385
+ skipped.add(it.name);
356
386
  continue;
387
+ }
357
388
  if (entries.length >= entryCap) {
358
389
  truncated = true;
359
- return { root: relPath, entries, truncated, truncatedByDepth, depthCap };
390
+ return result();
360
391
  }
361
392
  const abs = resolve(dir, it.name);
362
393
  const rel = relative(workDir, abs);
@@ -387,7 +418,7 @@ export function repoTree(workDir, relPath = ".", maxDepth = 3, maxEntries = 500)
387
418
  }
388
419
  }
389
420
  }
390
- return { root: relPath, entries, truncated, truncatedByDepth, depthCap };
421
+ return result();
391
422
  }
392
423
  export const SYNC_FETCH_TTL_MS = 60_000;
393
424
  /** Per work-tree root: when the last fetch attempt was made and how it went. */
@@ -189,10 +189,12 @@ export class CodingRuntime {
189
189
  // `[exited with code N]` / `[error]` lines recorded on exit. Blanking it when the
190
190
  // process is dead lost exactly the output + failure reason the brain/console needs
191
191
  // to diagnose a crash or read a one-shot CLI's final result.
192
- const pane = clip(session.snapshot());
192
+ const full = session.snapshot();
193
+ const pane = clip(full);
193
194
  return {
194
195
  sessionId,
195
196
  pane,
197
+ paneChars: full.length,
196
198
  alive,
197
199
  ready: alive ? session.ready : false,
198
200
  runState: alive ? session.runState() : "idle",
@@ -391,6 +393,7 @@ export class CodingRuntime {
391
393
  return session;
392
394
  }
393
395
  }
396
+ /** The tail, marked (#898): the head it drops is said, with both numbers, at the top. */
394
397
  function clip(pane) {
395
- return pane.length > MAX_PANE ? pane.slice(pane.length - MAX_PANE) : pane;
398
+ return pane.length > MAX_PANE ? `[cut: showing the last ${MAX_PANE} of ${pane.length} characters]\n${pane.slice(pane.length - MAX_PANE)}` : pane;
396
399
  }
@@ -304,15 +304,16 @@ async function route(runner, req, res) {
304
304
  const raw = (b.dir || "~").replace(/^~(?=$|\/)/, homedir());
305
305
  const dir = resolve(raw);
306
306
  try {
307
- const entries = readdirSync(dir, { withFileTypes: true })
308
- .filter((e) => !e.name.startsWith("."))
307
+ const visible = readdirSync(dir, { withFileTypes: true }).filter((e) => !e.name.startsWith("."));
308
+ const entries = visible
309
309
  .slice(0, 200)
310
310
  .map((e) => ({
311
311
  name: e.name,
312
312
  type: e.isDirectory() ? "dir" : "file",
313
313
  size: e.isFile() ? statSync(resolve(dir, e.name)).size : undefined,
314
314
  }));
315
- return json(res, 200, { dir, entries });
315
+ // Said, not silent (#898): a folder of 500 entries read as one of 200.
316
+ return json(res, 200, { dir, entries, ...(visible.length > entries.length ? { total: visible.length, truncated: true } : {}) });
316
317
  }
317
318
  catch (e) {
318
319
  return json(res, 400, { error: e instanceof Error ? e.message : String(e), dir });
package/dist/index.js CHANGED
@@ -1718,7 +1718,7 @@ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, f
1718
1718
  try {
1719
1719
  result = text2 ? JSON.parse(text2) : {};
1720
1720
  } catch {
1721
- result = { raw: text2.slice(0, 500) };
1721
+ result = text2.length > 500 ? { raw: text2.slice(0, 500), rawChars: text2.length, truncated: true } : { raw: text2 };
1722
1722
  }
1723
1723
  try {
1724
1724
  ws.send(JSON.stringify({ id: cmd.id, status: res.status, result }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.74",
3
+ "version": "0.4.75",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",