agent-sh 0.10.0 → 0.10.2
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 +12 -9
- package/dist/agent/agent-loop.d.ts +0 -3
- package/dist/agent/agent-loop.js +18 -35
- package/dist/agent/conversation-state.js +8 -2
- package/dist/agent/nuclear-form.d.ts +2 -0
- package/dist/agent/nuclear-form.js +11 -1
- package/dist/agent/system-prompt.js +1 -1
- package/dist/agent/token-budget.d.ts +8 -12
- package/dist/agent/token-budget.js +5 -40
- package/dist/agent/tool-registry.js +6 -0
- package/dist/agent/types.d.ts +3 -1
- package/dist/context-manager.d.ts +1 -21
- package/dist/context-manager.js +26 -163
- package/dist/event-bus.d.ts +0 -1
- package/dist/extension-loader.js +25 -4
- package/dist/extensions/agent-backend.js +3 -2
- package/dist/extensions/index.js +0 -1
- package/dist/extensions/tui-renderer.js +47 -29
- package/dist/settings.d.ts +3 -11
- package/dist/settings.js +0 -4
- package/dist/shell/input-handler.js +14 -9
- package/dist/types.d.ts +3 -0
- package/dist/utils/ansi.d.ts +6 -1
- package/dist/utils/ansi.js +114 -7
- package/dist/utils/box-frame.js +8 -2
- package/dist/utils/llm-client.d.ts +4 -0
- package/dist/utils/llm-client.js +8 -0
- package/dist/utils/markdown.d.ts +4 -0
- package/dist/utils/markdown.js +136 -48
- package/dist/utils/package-version.d.ts +1 -0
- package/dist/utils/package-version.js +10 -0
- package/dist/utils/shell-output-spill.d.ts +2 -0
- package/dist/utils/shell-output-spill.js +81 -0
- package/examples/extensions/claude-code-bridge/README.md +14 -0
- package/examples/extensions/claude-code-bridge/index.ts +13 -101
- package/examples/extensions/pi-bridge/README.md +16 -0
- package/examples/extensions/pi-bridge/index.ts +8 -154
- package/package.json +9 -1
- package/dist/extensions/shell-recall.d.ts +0 -9
- package/dist/extensions/shell-recall.js +0 -8
package/dist/utils/ansi.js
CHANGED
|
@@ -10,9 +10,67 @@ export const RESET = "\x1b[0m";
|
|
|
10
10
|
// ── ANSI utility functions ───────────────────────────────────
|
|
11
11
|
/**
|
|
12
12
|
* Check if a Unicode code point is a wide character (CJK, fullwidth, emoji, etc.)
|
|
13
|
-
* Returns 2 for wide chars, 1 for normal chars.
|
|
13
|
+
* Returns 2 for wide chars, 1 for normal chars, 0 for combining chars.
|
|
14
|
+
*
|
|
15
|
+
* Based on East Asian Width and Unicode categories.
|
|
14
16
|
*/
|
|
15
17
|
export function charWidth(codePoint) {
|
|
18
|
+
// Combining characters (zero width)
|
|
19
|
+
if (codePoint >= 0x0300 && codePoint <= 0x036f)
|
|
20
|
+
return 0; // Combining Diacritical Marks
|
|
21
|
+
if (codePoint >= 0x1ab0 && codePoint <= 0x1aff)
|
|
22
|
+
return 0; // Combining Diacritical Marks Extended
|
|
23
|
+
if (codePoint >= 0x1dc0 && codePoint <= 0x1dff)
|
|
24
|
+
return 0; // Combining Diacritical Marks Supplement
|
|
25
|
+
if (codePoint >= 0x20d0 && codePoint <= 0x20ff)
|
|
26
|
+
return 0; // Combining Diacritical Marks for Symbols
|
|
27
|
+
if (codePoint >= 0xfe20 && codePoint <= 0xfe2f)
|
|
28
|
+
return 0; // Combining Half Marks
|
|
29
|
+
if (codePoint >= 0xfe00 && codePoint <= 0xfe0f)
|
|
30
|
+
return 0; // Variation Selectors
|
|
31
|
+
if (codePoint >= 0xe0100 && codePoint <= 0xe01ef)
|
|
32
|
+
return 0; // Variation Selectors Supplement
|
|
33
|
+
// Emoji and symbols that render as wide (2 columns)
|
|
34
|
+
// Emoji presentation sequences and keycap
|
|
35
|
+
if (codePoint === 0x20e3)
|
|
36
|
+
return 2; // Combining Enclosing Keycap
|
|
37
|
+
// Emoji blocks
|
|
38
|
+
if (codePoint >= 0x1f600 && codePoint <= 0x1f64f)
|
|
39
|
+
return 2; // Emoticons
|
|
40
|
+
if (codePoint >= 0x1f300 && codePoint <= 0x1f5ff)
|
|
41
|
+
return 2; // Misc Symbols and Pictographs
|
|
42
|
+
if (codePoint >= 0x1f680 && codePoint <= 0x1f6ff)
|
|
43
|
+
return 2; // Transport and Map
|
|
44
|
+
if (codePoint >= 0x1f700 && codePoint <= 0x1f77f)
|
|
45
|
+
return 2; // Alchemical Symbols
|
|
46
|
+
if (codePoint >= 0x1f780 && codePoint <= 0x1f7ff)
|
|
47
|
+
return 2; // Geometric Shapes Extended
|
|
48
|
+
if (codePoint >= 0x1f800 && codePoint <= 0x1f8ff)
|
|
49
|
+
return 2; // Supplemental Arrows-C
|
|
50
|
+
if (codePoint >= 0x1f900 && codePoint <= 0x1f9ff)
|
|
51
|
+
return 2; // Supplemental Symbols and Pictographs
|
|
52
|
+
if (codePoint >= 0x1fa00 && codePoint <= 0x1faff)
|
|
53
|
+
return 2; // Chess Symbols, Symbols and Pictographs Extended-A
|
|
54
|
+
// NOTE: 0x2300-0x23ff (Misc Technical), 0x2600-0x26ff (Misc Symbols),
|
|
55
|
+
// and 0x2700-0x27bf (Dingbats) are mostly "Ambiguous" width — render as
|
|
56
|
+
// 1 column in non-CJK terminal locales (e.g. ❯, ⌘, ★, ♦). But a handful
|
|
57
|
+
// of dingbats have Emoji_Presentation=Yes and render as 2 cols everywhere.
|
|
58
|
+
if (codePoint === 0x2705 || // ✅ white heavy check mark
|
|
59
|
+
codePoint === 0x270a || // ✊ raised fist
|
|
60
|
+
codePoint === 0x270b || // ✋ raised hand
|
|
61
|
+
codePoint === 0x2728 || // ✨ sparkles
|
|
62
|
+
codePoint === 0x274c || // ❌ cross mark
|
|
63
|
+
codePoint === 0x274e || // ❎ negative squared cross mark
|
|
64
|
+
(codePoint >= 0x2753 && codePoint <= 0x2755) || // ❓❔❕
|
|
65
|
+
codePoint === 0x2757 || // ❗ heavy exclamation mark
|
|
66
|
+
(codePoint >= 0x2795 && codePoint <= 0x2797) || // ➕➖➗
|
|
67
|
+
codePoint === 0x27b0 || // ➰ curly loop
|
|
68
|
+
codePoint === 0x27bf // ➿ double curly loop
|
|
69
|
+
)
|
|
70
|
+
return 2;
|
|
71
|
+
// Regional indicator symbols (flag emoji components)
|
|
72
|
+
if (codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff)
|
|
73
|
+
return 2;
|
|
16
74
|
// CJK Unified Ideographs
|
|
17
75
|
if (codePoint >= 0x4e00 && codePoint <= 0x9fff)
|
|
18
76
|
return 2;
|
|
@@ -28,7 +86,6 @@ export function charWidth(codePoint) {
|
|
|
28
86
|
// Fullwidth ASCII variants
|
|
29
87
|
if (codePoint >= 0xff01 && codePoint <= 0xff5e)
|
|
30
88
|
return 2;
|
|
31
|
-
// Halfwidth Katakana (actually narrow, skip)
|
|
32
89
|
// Fullwidth bracket forms
|
|
33
90
|
if (codePoint >= 0xff5f && codePoint <= 0xff60)
|
|
34
91
|
return 2;
|
|
@@ -76,18 +133,68 @@ export function visibleLen(str) {
|
|
|
76
133
|
*/
|
|
77
134
|
export function truncateToWidth(str, maxWidth) {
|
|
78
135
|
const clean = str.replace(/\x1b\[[^m]*m/g, "");
|
|
136
|
+
if (maxWidth <= 0)
|
|
137
|
+
return "";
|
|
138
|
+
// First check if the entire string fits
|
|
139
|
+
let fullWidth = 0;
|
|
140
|
+
for (const char of clean) {
|
|
141
|
+
fullWidth += charWidth(char.codePointAt(0) ?? 0);
|
|
142
|
+
}
|
|
143
|
+
if (fullWidth <= maxWidth)
|
|
144
|
+
return clean;
|
|
145
|
+
// String doesn't fit — truncate with "…"
|
|
146
|
+
// At maxWidth=1 the ellipsis alone fills the budget.
|
|
147
|
+
if (maxWidth === 1)
|
|
148
|
+
return "…";
|
|
149
|
+
// Reserve 1 column for "…", so target content width is maxWidth - 1
|
|
150
|
+
const target = maxWidth - 1;
|
|
79
151
|
let width = 0;
|
|
80
152
|
let i = 0;
|
|
81
153
|
for (const char of clean) {
|
|
82
154
|
const cw = charWidth(char.codePointAt(0) ?? 0);
|
|
83
|
-
if (width + cw >
|
|
84
|
-
|
|
85
|
-
return clean.slice(0, i) + "…";
|
|
86
|
-
}
|
|
155
|
+
if (width + cw > target)
|
|
156
|
+
break;
|
|
87
157
|
width += cw;
|
|
88
158
|
i += char.length;
|
|
89
159
|
}
|
|
90
|
-
|
|
160
|
+
// If nothing fit (first char is wider than target), just show the ellipsis
|
|
161
|
+
// rather than emit a character that would overflow the budget.
|
|
162
|
+
if (i === 0)
|
|
163
|
+
return "…";
|
|
164
|
+
return clean.slice(0, i) + "…";
|
|
165
|
+
}
|
|
166
|
+
/** Truncate to visible width while preserving SGR sequences — use when
|
|
167
|
+
* input carries color/bold codes. `truncateToWidth` strips them. */
|
|
168
|
+
export function truncateAnsiToWidth(str, maxWidth) {
|
|
169
|
+
if (maxWidth <= 0)
|
|
170
|
+
return "";
|
|
171
|
+
if (visibleLen(str) <= maxWidth)
|
|
172
|
+
return str;
|
|
173
|
+
if (maxWidth === 1)
|
|
174
|
+
return "…";
|
|
175
|
+
const target = maxWidth - 1;
|
|
176
|
+
let width = 0;
|
|
177
|
+
let out = "";
|
|
178
|
+
let i = 0;
|
|
179
|
+
while (i < str.length) {
|
|
180
|
+
if (str[i] === "\x1b" && str[i + 1] === "[") {
|
|
181
|
+
const end = str.indexOf("m", i);
|
|
182
|
+
if (end !== -1) {
|
|
183
|
+
out += str.slice(i, end + 1);
|
|
184
|
+
i = end + 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const cp = str.codePointAt(i) ?? 0;
|
|
189
|
+
const cw = charWidth(cp);
|
|
190
|
+
if (width + cw > target)
|
|
191
|
+
break;
|
|
192
|
+
const chLen = cp > 0xffff ? 2 : 1;
|
|
193
|
+
out += str.slice(i, i + chLen);
|
|
194
|
+
width += cw;
|
|
195
|
+
i += chLen;
|
|
196
|
+
}
|
|
197
|
+
return out + "\x1b[0m…";
|
|
91
198
|
}
|
|
92
199
|
/**
|
|
93
200
|
* Pad a string with spaces to fill `targetWidth` visible columns.
|
package/dist/utils/box-frame.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* never writes to stdout. Supports multiple border styles and
|
|
6
6
|
* optional title/footer sections with dividers.
|
|
7
7
|
*/
|
|
8
|
-
import { visibleLen } from "./ansi.js";
|
|
8
|
+
import { visibleLen, truncateToWidth } from "./ansi.js";
|
|
9
9
|
import { palette as p } from "./palette.js";
|
|
10
10
|
const BORDERS = {
|
|
11
11
|
rounded: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", ml: "├", mr: "┤" },
|
|
@@ -63,6 +63,12 @@ export function renderBoxFrame(content, opts) {
|
|
|
63
63
|
}
|
|
64
64
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
65
65
|
function boxLine(text, innerW, v, bc) {
|
|
66
|
-
const
|
|
66
|
+
const textWidth = visibleLen(text);
|
|
67
|
+
if (textWidth > innerW) {
|
|
68
|
+
// Content is too wide — truncate to fit exactly
|
|
69
|
+
const truncated = truncateToWidth(text, innerW);
|
|
70
|
+
return `${bc}${v}${p.reset} ${truncated} ${bc}${v}${p.reset}`;
|
|
71
|
+
}
|
|
72
|
+
const pad = innerW - textWidth;
|
|
67
73
|
return `${bc}${v}${p.reset} ${text}${" ".repeat(pad)} ${bc}${v}${p.reset}`;
|
|
68
74
|
}
|
|
@@ -12,6 +12,10 @@ export interface LlmClientConfig {
|
|
|
12
12
|
apiKey: string;
|
|
13
13
|
baseURL?: string;
|
|
14
14
|
model: string;
|
|
15
|
+
/** Sent as OpenRouter X-Title; ignored by other providers. */
|
|
16
|
+
appName?: string;
|
|
17
|
+
/** Sent as OpenRouter HTTP-Referer; ignored by other providers. */
|
|
18
|
+
appUrl?: string;
|
|
15
19
|
}
|
|
16
20
|
export declare class LlmClient {
|
|
17
21
|
private config;
|
package/dist/utils/llm-client.js
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
* (command suggestions, completions).
|
|
7
7
|
*/
|
|
8
8
|
import OpenAI from "openai";
|
|
9
|
+
function attributionHeaders(config) {
|
|
10
|
+
return {
|
|
11
|
+
"HTTP-Referer": config.appUrl ?? "https://agent-sh.dev",
|
|
12
|
+
"X-Title": config.appName ?? "agent-sh",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
9
15
|
export class LlmClient {
|
|
10
16
|
config;
|
|
11
17
|
client;
|
|
@@ -15,6 +21,7 @@ export class LlmClient {
|
|
|
15
21
|
this.client = new OpenAI({
|
|
16
22
|
apiKey: config.apiKey,
|
|
17
23
|
baseURL: config.baseURL,
|
|
24
|
+
defaultHeaders: attributionHeaders(config),
|
|
18
25
|
});
|
|
19
26
|
this.model = config.model;
|
|
20
27
|
}
|
|
@@ -24,6 +31,7 @@ export class LlmClient {
|
|
|
24
31
|
this.client = new OpenAI({
|
|
25
32
|
apiKey: newConfig.apiKey,
|
|
26
33
|
baseURL: newConfig.baseURL,
|
|
34
|
+
defaultHeaders: attributionHeaders(newConfig),
|
|
27
35
|
});
|
|
28
36
|
this.model = newConfig.model;
|
|
29
37
|
}
|
package/dist/utils/markdown.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ export declare const MAX_CONTENT_WIDTH = 90;
|
|
|
2
2
|
/**
|
|
3
3
|
* Word-wrap a string (which may contain ANSI codes) to a maximum visible width.
|
|
4
4
|
* Returns an array of lines, each fitting within `maxWidth` visible characters.
|
|
5
|
+
*
|
|
6
|
+
* Handles CJK text by breaking between wide characters and applying basic
|
|
7
|
+
* CJK rules (closing punctuation sticks to the previous line; opening
|
|
8
|
+
* punctuation sticks to the next).
|
|
5
9
|
*/
|
|
6
10
|
export declare function wrapLine(text: string, maxWidth: number): string[];
|
|
7
11
|
/**
|
package/dist/utils/markdown.js
CHANGED
|
@@ -1,9 +1,65 @@
|
|
|
1
|
-
import { visibleLen,
|
|
1
|
+
import { visibleLen, truncateAnsiToWidth, padEndToWidth, charWidth } from "./ansi.js";
|
|
2
2
|
import { palette as p } from "./palette.js";
|
|
3
3
|
export const MAX_CONTENT_WIDTH = 90;
|
|
4
|
+
// CJK line-breaking rules: closing punctuation must not start a line,
|
|
5
|
+
// opening punctuation must not end a line. Both CJK fullwidth and ASCII
|
|
6
|
+
// equivalents are included so mixed text wraps correctly.
|
|
7
|
+
const CJK_NO_LINE_START = new Set([
|
|
8
|
+
"。", ",", "、", ".", ";", ":", "!", "?",
|
|
9
|
+
")", "」", "』", "】", "》", "〉", "〕", "]", "}",
|
|
10
|
+
"・", "々", "〜", "~", "ー",
|
|
11
|
+
".", ",", ";", ":", "!", "?", ")", "]", "}",
|
|
12
|
+
]);
|
|
13
|
+
const CJK_NO_LINE_END = new Set([
|
|
14
|
+
"(", "「", "『", "【", "《", "〈", "〔", "[", "{",
|
|
15
|
+
"(", "[", "{",
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Tokenize a visible-text run into units suitable for wrapping.
|
|
19
|
+
* Each width-2 character (CJK, fullwidth, emoji) becomes its own token so the
|
|
20
|
+
* wrapper can break between them; ASCII runs stay together as word tokens.
|
|
21
|
+
*/
|
|
22
|
+
function tokenizeVisible(text) {
|
|
23
|
+
const tokens = [];
|
|
24
|
+
let ascii = "";
|
|
25
|
+
const flush = () => { if (ascii) {
|
|
26
|
+
tokens.push(ascii);
|
|
27
|
+
ascii = "";
|
|
28
|
+
} };
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < text.length) {
|
|
31
|
+
const cp = text.codePointAt(i) ?? 0;
|
|
32
|
+
const chLen = cp > 0xffff ? 2 : 1;
|
|
33
|
+
const ch = text.slice(i, i + chLen);
|
|
34
|
+
if (ch === " ") {
|
|
35
|
+
flush();
|
|
36
|
+
let spaces = "";
|
|
37
|
+
while (i < text.length && text[i] === " ") {
|
|
38
|
+
spaces += " ";
|
|
39
|
+
i += 1;
|
|
40
|
+
}
|
|
41
|
+
tokens.push(spaces);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (charWidth(cp) === 2) {
|
|
45
|
+
flush();
|
|
46
|
+
tokens.push(ch);
|
|
47
|
+
i += chLen;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
ascii += ch;
|
|
51
|
+
i += chLen;
|
|
52
|
+
}
|
|
53
|
+
flush();
|
|
54
|
+
return tokens;
|
|
55
|
+
}
|
|
4
56
|
/**
|
|
5
57
|
* Word-wrap a string (which may contain ANSI codes) to a maximum visible width.
|
|
6
58
|
* Returns an array of lines, each fitting within `maxWidth` visible characters.
|
|
59
|
+
*
|
|
60
|
+
* Handles CJK text by breaking between wide characters and applying basic
|
|
61
|
+
* CJK rules (closing punctuation sticks to the previous line; opening
|
|
62
|
+
* punctuation sticks to the next).
|
|
7
63
|
*/
|
|
8
64
|
export function wrapLine(text, maxWidth) {
|
|
9
65
|
if (!(maxWidth > 0))
|
|
@@ -11,63 +67,92 @@ export function wrapLine(text, maxWidth) {
|
|
|
11
67
|
if (visibleLen(text) <= maxWidth)
|
|
12
68
|
return [text];
|
|
13
69
|
const result = [];
|
|
14
|
-
// Split into segments: ANSI codes and visible text
|
|
15
70
|
const segments = text.match(/(\x1b\[[^m]*m|[^\x1b]+)/g) || [text];
|
|
16
|
-
let
|
|
17
|
-
let
|
|
18
|
-
let activeStyles = "";
|
|
71
|
+
let lineTokens = [];
|
|
72
|
+
let lineWidth = 0;
|
|
73
|
+
let activeStyles = "";
|
|
74
|
+
let lastVisibleIdx = -1;
|
|
75
|
+
const commit = () => {
|
|
76
|
+
result.push(lineTokens.join("") + p.reset);
|
|
77
|
+
lineTokens = activeStyles ? [activeStyles] : [];
|
|
78
|
+
lineWidth = 0;
|
|
79
|
+
lastVisibleIdx = -1;
|
|
80
|
+
};
|
|
19
81
|
for (const seg of segments) {
|
|
20
82
|
if (seg.startsWith("\x1b[")) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (seg === p.reset) {
|
|
83
|
+
lineTokens.push(seg);
|
|
84
|
+
if (seg === p.reset)
|
|
24
85
|
activeStyles = "";
|
|
25
|
-
|
|
26
|
-
else {
|
|
86
|
+
else
|
|
27
87
|
activeStyles += seg;
|
|
28
|
-
}
|
|
29
88
|
continue;
|
|
30
89
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (
|
|
90
|
+
for (const token of tokenizeVisible(seg)) {
|
|
91
|
+
const tokenWidth = visibleLen(token);
|
|
92
|
+
const isSpace = token[0] === " ";
|
|
93
|
+
if (lineWidth + tokenWidth <= maxWidth) {
|
|
94
|
+
lineTokens.push(token);
|
|
95
|
+
lineWidth += tokenWidth;
|
|
96
|
+
if (!isSpace)
|
|
97
|
+
lastVisibleIdx = lineTokens.length - 1;
|
|
35
98
|
continue;
|
|
36
|
-
if (currentWidth + word.length <= maxWidth) {
|
|
37
|
-
currentLine += word;
|
|
38
|
-
currentWidth += word.length;
|
|
39
99
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
100
|
+
// Token doesn't fit on the current line.
|
|
101
|
+
if (isSpace)
|
|
102
|
+
continue; // spaces at wrap points are dropped
|
|
103
|
+
if (lineWidth === 0) {
|
|
104
|
+
// Token longer than the entire line — hard-break by char width.
|
|
105
|
+
let remaining = token;
|
|
43
106
|
while (remaining.length > 0) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
currentWidth += chunk.length;
|
|
107
|
+
let fitLen = 0, fitWidth = 0;
|
|
108
|
+
for (const ch of remaining) {
|
|
109
|
+
const cw = charWidth(ch.codePointAt(0) ?? 0);
|
|
110
|
+
if (fitWidth + cw > maxWidth)
|
|
111
|
+
break;
|
|
112
|
+
fitWidth += cw;
|
|
113
|
+
fitLen += ch.length;
|
|
54
114
|
}
|
|
115
|
+
if (fitLen === 0)
|
|
116
|
+
fitLen = remaining[0]?.length ?? 1;
|
|
117
|
+
const chunk = remaining.slice(0, fitLen);
|
|
118
|
+
remaining = remaining.slice(fitLen);
|
|
119
|
+
lineTokens.push(chunk);
|
|
120
|
+
lineWidth += visibleLen(chunk);
|
|
121
|
+
lastVisibleIdx = lineTokens.length - 1;
|
|
122
|
+
if (remaining.length > 0)
|
|
123
|
+
commit();
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Rule (a): closing punctuation must not start a line. Allow up to 2
|
|
128
|
+
// columns of overflow so the punctuation stays with its phrase.
|
|
129
|
+
if (CJK_NO_LINE_START.has(token)) {
|
|
130
|
+
lineTokens.push(token);
|
|
131
|
+
lineWidth += tokenWidth;
|
|
132
|
+
commit();
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// Rule (b): opening punctuation must not end a line. Pull the trailing
|
|
136
|
+
// opener down to the next line with us.
|
|
137
|
+
let carried = [];
|
|
138
|
+
if (lastVisibleIdx >= 0 && CJK_NO_LINE_END.has(lineTokens[lastVisibleIdx])) {
|
|
139
|
+
carried = lineTokens.splice(lastVisibleIdx);
|
|
140
|
+
while (lineTokens.length > 0 && /^ +$/.test(lineTokens[lineTokens.length - 1])) {
|
|
141
|
+
lineTokens.pop();
|
|
55
142
|
}
|
|
56
143
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
currentWidth = 0;
|
|
62
|
-
// Skip leading spaces on new line
|
|
63
|
-
const trimmed = word.replace(/^ +/, "");
|
|
64
|
-
currentLine += trimmed;
|
|
65
|
-
currentWidth = trimmed.length;
|
|
144
|
+
commit();
|
|
145
|
+
for (const t of carried) {
|
|
146
|
+
lineTokens.push(t);
|
|
147
|
+
lineWidth += visibleLen(t);
|
|
66
148
|
}
|
|
149
|
+
lineTokens.push(token);
|
|
150
|
+
lineWidth += tokenWidth;
|
|
151
|
+
lastVisibleIdx = lineTokens.length - 1;
|
|
67
152
|
}
|
|
68
153
|
}
|
|
69
|
-
if (
|
|
70
|
-
result.push(
|
|
154
|
+
if (lineWidth > 0) {
|
|
155
|
+
result.push(lineTokens.join(""));
|
|
71
156
|
}
|
|
72
157
|
return result;
|
|
73
158
|
}
|
|
@@ -173,17 +258,17 @@ export class MarkdownRenderer {
|
|
|
173
258
|
while (row.length < numCols)
|
|
174
259
|
row.push("");
|
|
175
260
|
}
|
|
176
|
-
//
|
|
261
|
+
// Width from rendered cell — raw `**bold**` over-counts by 4 per pair.
|
|
177
262
|
const colWidths = new Array(numCols).fill(0);
|
|
178
263
|
for (const row of dataRows) {
|
|
179
264
|
for (let c = 0; c < numCols; c++) {
|
|
180
|
-
colWidths[c] = Math.max(colWidths[c], visibleLen(row[c]));
|
|
265
|
+
colWidths[c] = Math.max(colWidths[c], visibleLen(this.renderInline(row[c])));
|
|
181
266
|
}
|
|
182
267
|
}
|
|
183
|
-
//
|
|
184
|
-
// Account for separators: " │ " between cols (3 chars each) + 2 outer padding
|
|
268
|
+
// Tables bypass the prose width cap — borders guide the eye, so wider is fine.
|
|
185
269
|
const separatorWidth = (numCols - 1) * 3;
|
|
186
|
-
const
|
|
270
|
+
const tableWidth = Math.max(10, this.width - 2);
|
|
271
|
+
const availableWidth = tableWidth - separatorWidth;
|
|
187
272
|
const totalWidth = colWidths.reduce((a, b) => a + b, 0);
|
|
188
273
|
if (totalWidth > availableWidth && availableWidth > numCols) {
|
|
189
274
|
const scale = availableWidth / totalWidth;
|
|
@@ -201,7 +286,10 @@ export class MarkdownRenderer {
|
|
|
201
286
|
const isHeader = hasHeader && i === 0;
|
|
202
287
|
const cells = row.map((cell, c) => {
|
|
203
288
|
const w = colWidths[c];
|
|
204
|
-
const
|
|
289
|
+
const rendered = this.renderInline(cell);
|
|
290
|
+
const text = visibleLen(rendered) > w
|
|
291
|
+
? truncateAnsiToWidth(rendered, w)
|
|
292
|
+
: padEndToWidth(rendered, w);
|
|
205
293
|
return isHeader ? `${p.bold}${text}${p.reset}` : text;
|
|
206
294
|
});
|
|
207
295
|
this.writeLine(`${p.dim}│${p.reset} ${cells.join(` ${p.dim}│${p.reset} `)} ${p.dim}│${p.reset}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PACKAGE_VERSION: string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent-sh package version, read from package.json at load time.
|
|
3
|
+
* Emitted on `agent:info` so consumers (TUI, remote peers, logs) see a
|
|
4
|
+
* version that tracks releases instead of a hand-edited constant.
|
|
5
|
+
*/
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
// dist/utils/package-version.js → ../../package.json (project root)
|
|
9
|
+
const pkg = require("../../package.json");
|
|
10
|
+
export const PACKAGE_VERSION = pkg.version ?? "0.0.0";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spill long shell outputs to per-session tempfiles.
|
|
3
|
+
*
|
|
4
|
+
* Captured PTY output that exceeds the truncation threshold is written to
|
|
5
|
+
* `<tmpdir>/agent-sh-<pid>/<id>.out`. The in-memory exchange keeps only a
|
|
6
|
+
* head+tail stub pointing at that path, so the agent can fetch the full
|
|
7
|
+
* text via `read_file` on demand. The session dir is removed on process
|
|
8
|
+
* exit; stale dirs from dead processes are swept lazily on first use.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
const DIR_PREFIX = "agent-sh-";
|
|
14
|
+
let sessionDir = null;
|
|
15
|
+
let cleanupRegistered = false;
|
|
16
|
+
export function getSessionDir() {
|
|
17
|
+
if (sessionDir)
|
|
18
|
+
return sessionDir;
|
|
19
|
+
sessionDir = join(tmpdir(), `${DIR_PREFIX}${process.pid}`);
|
|
20
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
21
|
+
sweepStaleDirs();
|
|
22
|
+
if (!cleanupRegistered) {
|
|
23
|
+
cleanupRegistered = true;
|
|
24
|
+
const cleanup = () => {
|
|
25
|
+
if (!sessionDir)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
sessionDir = null;
|
|
32
|
+
};
|
|
33
|
+
process.on("exit", cleanup);
|
|
34
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
35
|
+
process.on(sig, () => { cleanup(); process.exit(128); });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return sessionDir;
|
|
39
|
+
}
|
|
40
|
+
export function spillOutput(id, text) {
|
|
41
|
+
const path = join(getSessionDir(), `${id}.out`);
|
|
42
|
+
writeFileSync(path, text);
|
|
43
|
+
return path;
|
|
44
|
+
}
|
|
45
|
+
function sweepStaleDirs() {
|
|
46
|
+
const base = tmpdir();
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = readdirSync(base);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const name of entries) {
|
|
55
|
+
if (!name.startsWith(DIR_PREFIX))
|
|
56
|
+
continue;
|
|
57
|
+
const pid = Number(name.slice(DIR_PREFIX.length));
|
|
58
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid)
|
|
59
|
+
continue;
|
|
60
|
+
if (isProcessAlive(pid))
|
|
61
|
+
continue;
|
|
62
|
+
const full = join(base, name);
|
|
63
|
+
try {
|
|
64
|
+
// Small safety check: only remove directories.
|
|
65
|
+
if (statSync(full).isDirectory()) {
|
|
66
|
+
rmSync(full, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function isProcessAlive(pid) {
|
|
73
|
+
try {
|
|
74
|
+
process.kill(pid, 0);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
// ESRCH = no such process; EPERM = exists but we can't signal it
|
|
79
|
+
return e.code === "EPERM";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -33,3 +33,17 @@ Or switch at runtime:
|
|
|
33
33
|
|
|
34
34
|
- `ANTHROPIC_API_KEY` must be set in your environment
|
|
35
35
|
- Claude Code manages its own model selection — no model configuration needed in agent-sh
|
|
36
|
+
|
|
37
|
+
## What this bridge is
|
|
38
|
+
|
|
39
|
+
A pure protocol translator between the Claude Agent SDK's event stream and agent-sh's bus events. Claude Code uses its own built-in tools exactly as the SDK ships them (`Read`, `Edit`, `Write`, `Bash`, `Glob`, `Grep`). The bridge adds no tools of its own.
|
|
40
|
+
|
|
41
|
+
## What this bridge intentionally does NOT bundle
|
|
42
|
+
|
|
43
|
+
Three PTY-access tools are left out on purpose:
|
|
44
|
+
|
|
45
|
+
- `terminal_read` — observe the user's live terminal screen
|
|
46
|
+
- `terminal_keys` — send keystrokes to the user's PTY
|
|
47
|
+
- `user_shell` — run commands in the user's live shell with lasting `cd`/`export`/`source` effects
|
|
48
|
+
|
|
49
|
+
These are opt-in capabilities that belong in their own extensions. If you want any of them with Claude Code, write a companion extension that uses the SDK's `tool()` + `createSdkMcpServer()` to expose them as MCP tools, and extend the bridge (or fork it) to attach that MCP server to the SDK's `query()` options.
|