agent-sh 0.12.25 → 0.12.27

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.
@@ -1105,7 +1105,7 @@ export class AgentLoop {
1105
1105
  // Compact deeply — shallow targets buy only 1–2 turns of runway on
1106
1106
  // tool-heavy workloads.
1107
1107
  const target = Math.floor(threshold * 0.25);
1108
- const result = this.compactWithHooks(target, 6);
1108
+ const result = this.compactWithHooks(target, 1);
1109
1109
  if (!result) {
1110
1110
  // Auto-compact fired but nothing was evictable. This can happen
1111
1111
  // in short conversations with heavy tool output where the pin
@@ -1488,7 +1488,7 @@ export class AgentLoop {
1488
1488
  if (this.isContextOverflow(e)) {
1489
1489
  const contextWindow = this.currentMode.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
1490
1490
  const target = Math.floor((contextWindow - RESPONSE_RESERVE) * 0.6);
1491
- const stats = this.compactWithHooks(target, 6);
1491
+ const stats = this.compactWithHooks(target, 1);
1492
1492
  // If compaction freed nothing, retrying will hit the same error.
1493
1493
  // Surface the real failure instead of looping until exhaustion.
1494
1494
  if (!stats || stats.after >= stats.before) {
@@ -29,6 +29,19 @@ function firstMatchExcerpt(text, regex) {
29
29
  function recencyWeight(idx, total) {
30
30
  return Math.max(0.1, 1 - idx / total);
31
31
  }
32
+ // Head+tail because the start (command, opening lines) and end (final
33
+ // result, exit code) are the informative parts of shell/file output.
34
+ function slimToolContent(content, maxLen) {
35
+ const exitMatch = content.match(/exit code:?\s*(\d+)/i);
36
+ const exitSuffix = exitMatch ? ` (exit ${exitMatch[1]})` : "";
37
+ const lines = content.split("\n");
38
+ if (lines.length > 6) {
39
+ const head = lines.slice(0, 3).join("\n");
40
+ const tail = lines.slice(-2).join("\n");
41
+ return `${head}\n... [${lines.length - 5} lines trimmed by compact]\n${tail}${exitSuffix}`;
42
+ }
43
+ return `${content.slice(0, maxLen)}\n... [${content.length - maxLen} chars trimmed by compact]${exitSuffix}`;
44
+ }
32
45
  /**
33
46
  * Conversation state with eager nucleation — shell-history shaped.
34
47
  *
@@ -611,6 +624,7 @@ export class ConversationState {
611
624
  // ── Internal: Two-tier pin for recent turns ────────────────────
612
625
  slimTurn(messages) {
613
626
  const MAX_RESULT_LEN = 1500;
627
+ const MAX_ASSISTANT_LEN = 1500;
614
628
  const result = [];
615
629
  const droppedToolIds = new Set();
616
630
  for (const msg of messages) {
@@ -642,13 +656,20 @@ export class ConversationState {
642
656
  continue;
643
657
  const content = typeof msg.content === "string" ? msg.content : "";
644
658
  if (content.length > MAX_RESULT_LEN) {
645
- result.push({ ...msg, content: content.slice(0, MAX_RESULT_LEN) + "\n... [truncated by compact]" });
659
+ result.push({ ...msg, content: slimToolContent(content, MAX_RESULT_LEN) });
646
660
  }
647
661
  else {
648
662
  result.push(msg);
649
663
  }
650
664
  continue;
651
665
  }
666
+ if (msg.role === "assistant" && typeof msg.content === "string" && msg.content.length > MAX_ASSISTANT_LEN) {
667
+ const head = msg.content.slice(0, Math.floor(MAX_ASSISTANT_LEN * 0.6));
668
+ const tail = msg.content.slice(-Math.floor(MAX_ASSISTANT_LEN * 0.2));
669
+ const trimmed = msg.content.length - head.length - tail.length;
670
+ result.push({ ...msg, content: `${head}\n... [${trimmed} chars trimmed by compact]\n${tail}` });
671
+ continue;
672
+ }
652
673
  result.push(msg);
653
674
  }
654
675
  return result;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
3
  import * as path from "node:path";
4
- import { activateShell } from "./shell/index.js";
4
+ import { activateShell, registerShellHandlers } from "./shell/index.js";
5
5
  import { createCore } from "./core.js";
6
6
  import { palette as p } from "./utils/palette.js";
7
7
  import { loadBuiltinExtensions } from "./extensions/index.js";
@@ -237,6 +237,8 @@ async function main() {
237
237
  process.exit(0);
238
238
  };
239
239
  const extCtx = core.extensionContext({ quit: cleanup });
240
+ // Before loadExtensions: extensions look up shell handlers at activation.
241
+ registerShellHandlers(extCtx);
240
242
  // Load before spawning the shell so PS1 lands below the banner.
241
243
  await loadBuiltinExtensions(extCtx, getSettings().disabledBuiltins);
242
244
  const loadExtensionsTimeoutMs = 10000;
package/dist/install.js CHANGED
@@ -69,11 +69,13 @@ function pickResolver(spec) {
69
69
  return r;
70
70
  return bundledResolver;
71
71
  }
72
- function maybeNpmInstall(target) {
72
+ function readPackageJson(target) {
73
73
  const pkgJson = path.join(target, "package.json");
74
74
  if (!fs.existsSync(pkgJson))
75
- return;
76
- const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
75
+ return null;
76
+ return JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
77
+ }
78
+ function maybeNpmInstall(target, pkg) {
77
79
  const deps = { ...(pkg.dependencies ?? {}), ...(pkg.peerDependencies ?? {}) };
78
80
  if (Object.keys(deps).length === 0)
79
81
  return;
@@ -88,6 +90,56 @@ function maybeNpmInstall(target) {
88
90
  throw new Error(`npm install failed in ${target}; run it manually.`);
89
91
  }
90
92
  }
93
+ function normalizeBin(pkg) {
94
+ if (!pkg.bin)
95
+ return {};
96
+ if (typeof pkg.bin === "string") {
97
+ const name = pkg.name?.startsWith("@") ? pkg.name.split("/")[1] : pkg.name;
98
+ return name ? { [name]: pkg.bin } : {};
99
+ }
100
+ return pkg.bin;
101
+ }
102
+ function maybeNpmBuild(target, pkg) {
103
+ if (!pkg.scripts?.build)
104
+ return;
105
+ const binPaths = Object.values(normalizeBin(pkg)).map((p) => path.join(target, p));
106
+ if (binPaths.length === 0)
107
+ return;
108
+ if (binPaths.every((p) => fs.existsSync(p)))
109
+ return;
110
+ console.log(`Running npm run build in ${target}...`);
111
+ const result = spawnSync("npm", ["run", "build"], { cwd: target, stdio: "inherit" });
112
+ if (result.status !== 0) {
113
+ throw new Error(`npm run build failed in ${target}; run it manually.`);
114
+ }
115
+ }
116
+ function linkBins(target, pkg) {
117
+ const bins = normalizeBin(pkg);
118
+ if (Object.keys(bins).length === 0)
119
+ return [];
120
+ const binDir = path.join(CONFIG_DIR, "bin");
121
+ fs.mkdirSync(binDir, { recursive: true });
122
+ const linked = [];
123
+ for (const [name, relPath] of Object.entries(bins)) {
124
+ const src = path.resolve(target, relPath);
125
+ if (!fs.existsSync(src)) {
126
+ console.error(`agent-sh: skipping bin "${name}" — ${src} not found`);
127
+ continue;
128
+ }
129
+ try {
130
+ fs.chmodSync(src, 0o755);
131
+ }
132
+ catch { /* ignore */ }
133
+ const linkPath = path.join(binDir, name);
134
+ try {
135
+ fs.unlinkSync(linkPath);
136
+ }
137
+ catch { /* ignore */ }
138
+ fs.symlinkSync(src, linkPath);
139
+ linked.push(name);
140
+ }
141
+ return linked;
142
+ }
91
143
  export async function runInstall(spec, opts = {}) {
92
144
  if (!spec) {
93
145
  console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force]\n\n" +
@@ -114,10 +166,16 @@ export async function runInstall(spec, opts = {}) {
114
166
  }
115
167
  fs.rmSync(target, { recursive: true, force: true });
116
168
  }
169
+ let linkedBins = [];
117
170
  if (resolved.isDirectory) {
118
171
  fs.cpSync(resolved.sourcePath, target, { recursive: true });
119
172
  try {
120
- maybeNpmInstall(target);
173
+ const pkg = readPackageJson(target);
174
+ if (pkg) {
175
+ maybeNpmInstall(target, pkg);
176
+ maybeNpmBuild(target, pkg);
177
+ linkedBins = linkBins(target, pkg);
178
+ }
121
179
  }
122
180
  catch (err) {
123
181
  console.error(`agent-sh: ${err instanceof Error ? err.message : String(err)}`);
@@ -128,6 +186,11 @@ export async function runInstall(spec, opts = {}) {
128
186
  fs.copyFileSync(resolved.sourcePath, target);
129
187
  }
130
188
  console.log(`Installed: ${resolved.name} -> ${target}`);
189
+ if (linkedBins.length > 0) {
190
+ const binDir = path.join(CONFIG_DIR, "bin");
191
+ console.log(`Linked bins: ${linkedBins.join(", ")} -> ${binDir}`);
192
+ console.log(`Add to PATH: export PATH="${binDir}:$PATH"`);
193
+ }
131
194
  }
132
195
  export async function runUninstall(name) {
133
196
  if (!name) {
@@ -146,6 +209,23 @@ export async function runUninstall(name) {
146
209
  console.error(`agent-sh: not installed: ${name}`);
147
210
  process.exit(1);
148
211
  }
212
+ const pkg = readPackageJson(target);
213
+ if (pkg) {
214
+ const binDir = path.join(CONFIG_DIR, "bin");
215
+ const targetPrefix = path.resolve(target) + path.sep;
216
+ for (const binName of Object.keys(normalizeBin(pkg))) {
217
+ const linkPath = path.join(binDir, binName);
218
+ try {
219
+ const stat = fs.lstatSync(linkPath, { throwIfNoEntry: false });
220
+ if (!stat?.isSymbolicLink())
221
+ continue;
222
+ const dest = path.resolve(binDir, fs.readlinkSync(linkPath));
223
+ if (dest.startsWith(targetPrefix))
224
+ fs.unlinkSync(linkPath);
225
+ }
226
+ catch { /* ignore */ }
227
+ }
228
+ }
149
229
  fs.rmSync(target, { recursive: true, force: true });
150
230
  console.log(`Uninstalled: ${name}`);
151
231
  }
@@ -27,6 +27,11 @@ export interface ShellHandle {
27
27
  /** Forward terminal size changes to the PTY. */
28
28
  resize(cols: number, rows: number): void;
29
29
  }
30
+ /**
31
+ * Register shell-owned handlers extensions can `ctx.call`. Must run before
32
+ * `loadExtensions`; the handlers only need the bus, not the PTY.
33
+ */
34
+ export declare function registerShellHandlers(ctx: ExtensionContext): void;
30
35
  /**
31
36
  * Construct the Shell, wire resize forwarding, and register cleanup with the
32
37
  * provided ExtensionContext. Returns a handle the caller (typically
@@ -1,6 +1,19 @@
1
1
  import { Shell } from "./shell.js";
2
2
  import { StdoutSurface } from "../utils/compositor.js";
3
3
  import { TerminalBuffer } from "../utils/terminal-buffer.js";
4
+ /**
5
+ * Register shell-owned handlers extensions can `ctx.call`. Must run before
6
+ * `loadExtensions`; the handlers only need the bus, not the PTY.
7
+ */
8
+ export function registerShellHandlers(ctx) {
9
+ let terminalBufferSingleton;
10
+ ctx.define("terminal-buffer", () => {
11
+ if (terminalBufferSingleton !== undefined)
12
+ return terminalBufferSingleton;
13
+ terminalBufferSingleton = TerminalBuffer.createWired(ctx.bus);
14
+ return terminalBufferSingleton;
15
+ });
16
+ }
4
17
  /**
5
18
  * Construct the Shell, wire resize forwarding, and register cleanup with the
6
19
  * provided ExtensionContext. Returns a handle the caller (typically
@@ -13,14 +26,6 @@ export function activateShell(ctx, opts) {
13
26
  ctx.compositor.setDefault("agent", stdoutSurface);
14
27
  ctx.compositor.setDefault("query", stdoutSurface);
15
28
  ctx.compositor.setDefault("status", stdoutSurface);
16
- // Lazy because @xterm/headless is optional; null when not installed.
17
- let terminalBufferSingleton;
18
- ctx.define("terminal-buffer", () => {
19
- if (terminalBufferSingleton !== undefined)
20
- return terminalBufferSingleton;
21
- terminalBufferSingleton = TerminalBuffer.createWired(ctx.bus);
22
- return terminalBufferSingleton;
23
- });
24
29
  const shell = new Shell({
25
30
  bus: ctx.bus,
26
31
  handlers: { define: ctx.define, call: ctx.call },
@@ -229,9 +229,15 @@ export class InputHandler {
229
229
  return false;
230
230
  }
231
231
  renderModeInput() {
232
- this.view.clearAutocomplete();
233
- this.drawPrompt();
234
- this.updateAutocomplete();
232
+ this.view.beginFrame();
233
+ try {
234
+ this.view.clearAutocomplete();
235
+ this.drawPrompt();
236
+ this.updateAutocomplete();
237
+ }
238
+ finally {
239
+ this.view.endFrame();
240
+ }
235
241
  }
236
242
  updateAutocomplete() {
237
243
  const buf = this.editor.text;
@@ -278,13 +284,19 @@ export class InputHandler {
278
284
  else {
279
285
  this.editor.setText(selected.name);
280
286
  }
281
- this.view.clearAutocomplete();
282
- this.autocompleteActive = false;
283
- this.autocompleteItems = [];
284
- this.autocompleteIndex = 0;
285
- this.drawPrompt();
286
- if (isFileAc)
287
- this.updateAutocomplete();
287
+ this.view.beginFrame();
288
+ try {
289
+ this.view.clearAutocomplete();
290
+ this.autocompleteActive = false;
291
+ this.autocompleteItems = [];
292
+ this.autocompleteIndex = 0;
293
+ this.drawPrompt();
294
+ if (isFileAc)
295
+ this.updateAutocomplete();
296
+ }
297
+ finally {
298
+ this.view.endFrame();
299
+ }
288
300
  }
289
301
  dismissAutocomplete() {
290
302
  this.view.clearAutocomplete();
@@ -314,11 +326,17 @@ export class InputHandler {
314
326
  case "changed": {
315
327
  const switchMode = this.modes.get(this.editor.text);
316
328
  if (this.editor.text.length === 1 && switchMode && switchMode !== this.activeMode) {
317
- this.dismissAutocomplete();
318
- this.view.clearPromptArea();
319
- this.activeMode = switchMode;
320
- this.editor.clear();
321
- this.drawPrompt(false);
329
+ this.view.beginFrame();
330
+ try {
331
+ this.dismissAutocomplete();
332
+ this.view.clearPromptArea();
333
+ this.activeMode = switchMode;
334
+ this.editor.clear();
335
+ this.drawPrompt(false);
336
+ }
337
+ finally {
338
+ this.view.endFrame();
339
+ }
322
340
  break;
323
341
  }
324
342
  this.historyIndex = -1;
@@ -371,8 +389,14 @@ export class InputHandler {
371
389
  }
372
390
  case "cancel":
373
391
  if (this.autocompleteActive) {
374
- this.dismissAutocomplete();
375
- this.drawPrompt();
392
+ this.view.beginFrame();
393
+ try {
394
+ this.dismissAutocomplete();
395
+ this.drawPrompt();
396
+ }
397
+ finally {
398
+ this.view.endFrame();
399
+ }
376
400
  }
377
401
  else {
378
402
  this.exitMode();
@@ -393,9 +417,15 @@ export class InputHandler {
393
417
  this.autocompleteIndex === 0
394
418
  ? this.autocompleteItems.length - 1
395
419
  : this.autocompleteIndex - 1;
396
- this.view.clearAutocomplete();
397
- this.drawPrompt();
398
- this.view.drawAutocomplete({ items: this.autocompleteItems, selected: this.autocompleteIndex });
420
+ this.view.beginFrame();
421
+ try {
422
+ this.view.clearAutocomplete();
423
+ this.drawPrompt();
424
+ this.view.drawAutocomplete({ items: this.autocompleteItems, selected: this.autocompleteIndex });
425
+ }
426
+ finally {
427
+ this.view.endFrame();
428
+ }
399
429
  }
400
430
  else if (this.history.length > 0) {
401
431
  if (this.historyIndex === -1) {
@@ -406,8 +436,14 @@ export class InputHandler {
406
436
  this.historyIndex--;
407
437
  }
408
438
  this.editor.setText(this.history[this.historyIndex]);
409
- this.view.clearAutocomplete();
410
- this.drawPrompt();
439
+ this.view.beginFrame();
440
+ try {
441
+ this.view.clearAutocomplete();
442
+ this.drawPrompt();
443
+ }
444
+ finally {
445
+ this.view.endFrame();
446
+ }
411
447
  }
412
448
  break;
413
449
  case "arrow-down":
@@ -416,9 +452,15 @@ export class InputHandler {
416
452
  this.autocompleteIndex === this.autocompleteItems.length - 1
417
453
  ? 0
418
454
  : this.autocompleteIndex + 1;
419
- this.view.clearAutocomplete();
420
- this.drawPrompt();
421
- this.view.drawAutocomplete({ items: this.autocompleteItems, selected: this.autocompleteIndex });
455
+ this.view.beginFrame();
456
+ try {
457
+ this.view.clearAutocomplete();
458
+ this.drawPrompt();
459
+ this.view.drawAutocomplete({ items: this.autocompleteItems, selected: this.autocompleteIndex });
460
+ }
461
+ finally {
462
+ this.view.endFrame();
463
+ }
422
464
  }
423
465
  else if (this.historyIndex !== -1) {
424
466
  if (this.historyIndex < this.history.length - 1) {
@@ -429,8 +471,14 @@ export class InputHandler {
429
471
  this.historyIndex = -1;
430
472
  this.editor.setText(this.savedBuffer);
431
473
  }
432
- this.view.clearAutocomplete();
433
- this.drawPrompt();
474
+ this.view.beginFrame();
475
+ try {
476
+ this.view.clearAutocomplete();
477
+ this.drawPrompt();
478
+ }
479
+ finally {
480
+ this.view.endFrame();
481
+ }
434
482
  }
435
483
  break;
436
484
  }
@@ -26,7 +26,12 @@ export declare class TuiInputView {
26
26
  private cursorTermCol;
27
27
  private autocompleteLines;
28
28
  private readonly surface;
29
+ private frameBuf;
29
30
  constructor(surface?: RenderSurface);
31
+ beginFrame(): void;
32
+ endFrame(): void;
33
+ private emit;
34
+ private autoFrame;
30
35
  resetCursor(): void;
31
36
  enableModeKeys(): void;
32
37
  disableModeKeys(): void;