agent-sh 0.5.0 → 0.6.0
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 -43
- package/dist/agent/agent-loop.d.ts +1 -0
- package/dist/agent/agent-loop.js +119 -26
- package/dist/agent/subagent.js +3 -1
- package/dist/agent/system-prompt.d.ts +1 -1
- package/dist/agent/system-prompt.js +21 -16
- package/dist/agent/tools/bash.js +10 -1
- package/dist/agent/tools/display.d.ts +13 -0
- package/dist/agent/tools/display.js +70 -0
- package/dist/agent/tools/edit-file.js +60 -7
- package/dist/agent/tools/glob.js +39 -7
- package/dist/agent/tools/grep.js +111 -20
- package/dist/agent/tools/ls.js +31 -2
- package/dist/agent/tools/read-file.d.ts +9 -1
- package/dist/agent/tools/read-file.js +50 -4
- package/dist/agent/tools/user-shell.js +40 -13
- package/dist/agent/tools/write-file.js +9 -1
- package/dist/agent/types.d.ts +35 -1
- package/dist/core.d.ts +1 -3
- package/dist/core.js +7 -11
- package/dist/event-bus.d.ts +18 -3
- package/dist/extension-loader.d.ts +1 -1
- package/dist/extension-loader.js +1 -3
- package/dist/extensions/tui-renderer.js +341 -83
- package/dist/index.js +41 -36
- package/dist/input-handler.js +4 -2
- package/dist/settings.js +1 -1
- package/dist/shell.js +2 -2
- package/dist/utils/diff.js +10 -0
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +23 -1
- package/dist/utils/tool-display.d.ts +4 -0
- package/dist/utils/tool-display.js +22 -5
- package/examples/extensions/claude-code-bridge/index.ts +8 -12
- package/examples/extensions/pi-bridge/index.ts +10 -12
- package/examples/extensions/secret-guard.ts +100 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import shellRecall from "./extensions/shell-recall.js";
|
|
|
11
11
|
import commandSuggest from "./extensions/command-suggest.js";
|
|
12
12
|
import { loadExtensions } from "./extension-loader.js";
|
|
13
13
|
import { getSettings } from "./settings.js";
|
|
14
|
+
import { discoverSkills } from "./agent/skills.js";
|
|
14
15
|
/**
|
|
15
16
|
* Capture the user's full shell environment.
|
|
16
17
|
* This picks up env vars exported in .zshrc/.bashrc that the
|
|
@@ -129,8 +130,7 @@ Examples:
|
|
|
129
130
|
|
|
130
131
|
Inside the shell:
|
|
131
132
|
Type normally Commands run in your real shell
|
|
132
|
-
> <query> Ask the AI agent
|
|
133
|
-
? <command> Have the agent run a command in your shell (help mode)
|
|
133
|
+
> <query> Ask the AI agent (it decides how to help)
|
|
134
134
|
> /help Show available slash commands
|
|
135
135
|
Ctrl-C Cancel agent response (or signal shell as usual)
|
|
136
136
|
`);
|
|
@@ -210,40 +210,18 @@ async function main() {
|
|
|
210
210
|
if (process.env.DEBUG) {
|
|
211
211
|
console.error('[agent-sh] Shell created');
|
|
212
212
|
}
|
|
213
|
-
// ── Input
|
|
213
|
+
// ── Input mode ───────────────────────────────────────────────
|
|
214
214
|
bus.emit("input-mode:register", {
|
|
215
|
-
id: "
|
|
215
|
+
id: "agent",
|
|
216
216
|
trigger: ">",
|
|
217
|
-
label: "
|
|
217
|
+
label: "agent",
|
|
218
218
|
promptIcon: "❯",
|
|
219
219
|
indicator: "●",
|
|
220
220
|
onSubmit(query, b) {
|
|
221
|
-
b.emit("agent:submit", { query
|
|
221
|
+
b.emit("agent:submit", { query });
|
|
222
222
|
},
|
|
223
223
|
returnToSelf: true,
|
|
224
224
|
});
|
|
225
|
-
bus.emit("input-mode:register", {
|
|
226
|
-
id: "help",
|
|
227
|
-
trigger: "?",
|
|
228
|
-
label: "help",
|
|
229
|
-
promptIcon: "❯",
|
|
230
|
-
indicator: "❓",
|
|
231
|
-
onSubmit(query, b) {
|
|
232
|
-
const onToolDone = (e) => {
|
|
233
|
-
if (e.kind === "execute") {
|
|
234
|
-
b.emit("agent:cancel-request", { silent: true });
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
const cleanup = () => {
|
|
238
|
-
b.off("agent:tool-completed", onToolDone);
|
|
239
|
-
b.off("agent:processing-done", cleanup);
|
|
240
|
-
};
|
|
241
|
-
b.on("agent:tool-completed", onToolDone);
|
|
242
|
-
b.on("agent:processing-done", cleanup);
|
|
243
|
-
b.emit("agent:submit", { query, modeLabel: "Help", modeInstruction: "[mode: help]" });
|
|
244
|
-
},
|
|
245
|
-
returnToSelf: false,
|
|
246
|
-
});
|
|
247
225
|
// ── Extensions ────────────────────────────────────────────────
|
|
248
226
|
if (process.env.DEBUG) {
|
|
249
227
|
console.error('[agent-sh] Setting up extensions...');
|
|
@@ -259,8 +237,9 @@ async function main() {
|
|
|
259
237
|
console.error('[agent-sh] Loading extensions...');
|
|
260
238
|
}
|
|
261
239
|
const loadExtensionsTimeoutMs = 10000;
|
|
240
|
+
let loadedExtensions = [];
|
|
262
241
|
await Promise.race([
|
|
263
|
-
loadExtensions(extCtx, config.extensions),
|
|
242
|
+
loadExtensions(extCtx, config.extensions).then((names) => { loadedExtensions = names; }),
|
|
264
243
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`Extension loading timeout after ${loadExtensionsTimeoutMs}ms`)), loadExtensionsTimeoutMs)),
|
|
265
244
|
]).catch((err) => {
|
|
266
245
|
console.error(`Warning: ${err.message}`);
|
|
@@ -268,6 +247,8 @@ async function main() {
|
|
|
268
247
|
if (process.env.DEBUG) {
|
|
269
248
|
console.error('[agent-sh] Extensions loaded');
|
|
270
249
|
}
|
|
250
|
+
// ── Discover skills ───────────────────────────────────────────
|
|
251
|
+
const skills = discoverSkills(process.cwd());
|
|
271
252
|
// ── Activate agent backend ────────────────────────────────────
|
|
272
253
|
// Extensions had their chance to register via agent:register-backend.
|
|
273
254
|
// If none did, the built-in AgentLoop gets wired to bus events.
|
|
@@ -275,15 +256,39 @@ async function main() {
|
|
|
275
256
|
// ── Startup banner ───────────────────────────────────────────
|
|
276
257
|
const settings = getSettings();
|
|
277
258
|
if (settings.startupBanner !== false) {
|
|
278
|
-
const title = core.llmClient
|
|
279
|
-
? `${p.accent}${p.bold}agent-sh${p.reset}${p.dim} · ${core.llmClient.model}${p.reset}`
|
|
280
|
-
: `${p.accent}${p.bold}agent-sh${p.reset}`;
|
|
281
|
-
const hint = `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}?${p.muted} to run in shell · ${p.warning}/help${p.muted} for commands${p.reset}`;
|
|
282
259
|
const termW = process.stdout.columns || 80;
|
|
283
|
-
const
|
|
260
|
+
const bannerW = Math.min(termW, 60);
|
|
261
|
+
const productName = `${p.accent}${p.bold}agent-sh${p.reset}`;
|
|
262
|
+
const info = agentInfo;
|
|
263
|
+
const backendName = info?.name ?? "agent-sh";
|
|
264
|
+
const model = info?.model ?? core.llmClient?.model;
|
|
265
|
+
const provider = info?.provider;
|
|
266
|
+
const modelValue = model
|
|
267
|
+
? provider ? `${model} [${provider}]` : model
|
|
268
|
+
: null;
|
|
269
|
+
let sections = "";
|
|
270
|
+
sections += `\n\n ${p.muted}Backend:${p.reset} ${p.dim}${backendName}${p.reset}`;
|
|
271
|
+
if (modelValue) {
|
|
272
|
+
sections += `\n ${p.muted}Model:${p.reset} ${p.dim}${modelValue}${p.reset}`;
|
|
273
|
+
}
|
|
274
|
+
if (loadedExtensions.length > 0) {
|
|
275
|
+
sections += `\n\n ${p.muted}Extensions:${p.reset}`;
|
|
276
|
+
for (const name of loadedExtensions) {
|
|
277
|
+
sections += `\n ${p.dim}${name}${p.reset}`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (skills.length > 0) {
|
|
281
|
+
sections += `\n\n ${p.muted}Skills:${p.reset}`;
|
|
282
|
+
for (const s of skills) {
|
|
283
|
+
sections += `\n ${p.dim}${s.name}${p.reset}`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const hint = `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}>/help${p.muted} for commands${p.reset}`;
|
|
287
|
+
const borderLine = `${p.muted}${"─".repeat(bannerW)}${p.reset}`;
|
|
284
288
|
process.stdout.write("\n" + borderLine + "\n" +
|
|
285
|
-
" " +
|
|
286
|
-
|
|
289
|
+
" " + productName +
|
|
290
|
+
sections + "\n" +
|
|
291
|
+
"\n " + hint + "\n" +
|
|
287
292
|
borderLine + "\n\n");
|
|
288
293
|
}
|
|
289
294
|
// ── Terminal lifecycle ────────────────────────────────────────
|
package/dist/input-handler.js
CHANGED
|
@@ -509,7 +509,8 @@ export class InputHandler {
|
|
|
509
509
|
}
|
|
510
510
|
this.editor.buffer = this.history[this.historyIndex];
|
|
511
511
|
this.editor.cursor = this.editor.buffer.length;
|
|
512
|
-
this.
|
|
512
|
+
this.clearAutocompleteLines();
|
|
513
|
+
this.writeModePromptLine();
|
|
513
514
|
}
|
|
514
515
|
break;
|
|
515
516
|
case "arrow-down":
|
|
@@ -532,7 +533,8 @@ export class InputHandler {
|
|
|
532
533
|
this.editor.buffer = this.savedBuffer;
|
|
533
534
|
}
|
|
534
535
|
this.editor.cursor = this.editor.buffer.length;
|
|
535
|
-
this.
|
|
536
|
+
this.clearAutocompleteLines();
|
|
537
|
+
this.writeModePromptLine();
|
|
536
538
|
}
|
|
537
539
|
break;
|
|
538
540
|
}
|
package/dist/settings.js
CHANGED
package/dist/shell.js
CHANGED
|
@@ -274,7 +274,7 @@ export class Shell {
|
|
|
274
274
|
const handler = (e) => {
|
|
275
275
|
clearTimeout(timeout);
|
|
276
276
|
this.bus.off("shell:command-done", handler);
|
|
277
|
-
resolve({ output: e.output, cwd: e.cwd });
|
|
277
|
+
resolve({ output: e.output, cwd: e.cwd, exitCode: e.exitCode });
|
|
278
278
|
};
|
|
279
279
|
this.bus.on("shell:command-done", handler);
|
|
280
280
|
this.outputParser.onCommandEntered(payload.command, this.outputParser.getCwd());
|
|
@@ -283,7 +283,7 @@ export class Shell {
|
|
|
283
283
|
this.paused = true;
|
|
284
284
|
this.echoSkip = false;
|
|
285
285
|
this.bus.emit("shell:agent-exec-done", {});
|
|
286
|
-
return { ...payload, output: output.output, cwd: output.cwd, done: true };
|
|
286
|
+
return { ...payload, output: output.output, cwd: output.cwd, exitCode: output.exitCode, done: true };
|
|
287
287
|
});
|
|
288
288
|
}
|
|
289
289
|
// ── Public API (used by index.ts) ──
|
package/dist/utils/diff.js
CHANGED
|
@@ -39,6 +39,16 @@ export function computeDiff(oldText, newText) {
|
|
|
39
39
|
// Build LCS table and backtrack to produce diff lines
|
|
40
40
|
const a = oldText.split("\n");
|
|
41
41
|
const b = newText.split("\n");
|
|
42
|
+
// Bail out if LCS table would be too large (avoids OOM / hang)
|
|
43
|
+
if (a.length * b.length > 10_000_000) {
|
|
44
|
+
return {
|
|
45
|
+
hunks: [],
|
|
46
|
+
added: b.length,
|
|
47
|
+
removed: a.length,
|
|
48
|
+
isIdentical: false,
|
|
49
|
+
isNewFile: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
42
52
|
const dp = buildLcs(a, b);
|
|
43
53
|
const raw = backtrack(dp, a, b);
|
|
44
54
|
let added = 0;
|
package/dist/utils/markdown.d.ts
CHANGED
package/dist/utils/markdown.js
CHANGED
|
@@ -83,6 +83,7 @@ export class MarkdownRenderer {
|
|
|
83
83
|
buffer = "";
|
|
84
84
|
contentWidth;
|
|
85
85
|
firstLine = true;
|
|
86
|
+
lastLineBlank = false;
|
|
86
87
|
pendingLines = [];
|
|
87
88
|
width;
|
|
88
89
|
tableRows = [];
|
|
@@ -192,6 +193,9 @@ export class MarkdownRenderer {
|
|
|
192
193
|
}
|
|
193
194
|
// Render rows
|
|
194
195
|
const hasHeader = sepIdx.includes(1) && dataRows.length > 1;
|
|
196
|
+
// Top border
|
|
197
|
+
const topBorder = colWidths.map((w) => "─".repeat(w)).join(`─┬─`);
|
|
198
|
+
this.writeLine(`${p.dim}┌─${topBorder}─┐${p.reset}`);
|
|
195
199
|
for (let i = 0; i < dataRows.length; i++) {
|
|
196
200
|
const row = dataRows[i];
|
|
197
201
|
const isHeader = hasHeader && i === 0;
|
|
@@ -207,6 +211,9 @@ export class MarkdownRenderer {
|
|
|
207
211
|
this.writeLine(`${p.dim}├─${sep}─┤${p.reset}`);
|
|
208
212
|
}
|
|
209
213
|
}
|
|
214
|
+
// Bottom border
|
|
215
|
+
const bottomBorder = colWidths.map((w) => "─".repeat(w)).join(`─┴─`);
|
|
216
|
+
this.writeLine(`${p.dim}└─${bottomBorder}─┘${p.reset}`);
|
|
210
217
|
}
|
|
211
218
|
renderLine(line) {
|
|
212
219
|
if (line.trim() === "")
|
|
@@ -232,6 +239,16 @@ export class MarkdownRenderer {
|
|
|
232
239
|
const bq = line.match(/^>\s?(.*)/);
|
|
233
240
|
if (bq)
|
|
234
241
|
return `${p.muted}│${p.reset} ${p.dim}${p.italic}${this.renderInline(bq[1] || "")}${p.reset}`;
|
|
242
|
+
// Task list (checkbox items) — must come before generic unordered list
|
|
243
|
+
const task = line.match(/^(\s*)[*\-+]\s+\[([ xX])\]\s+(.*)/);
|
|
244
|
+
if (task) {
|
|
245
|
+
const indent = task[1] || "";
|
|
246
|
+
const checked = task[2] !== " ";
|
|
247
|
+
const box = checked
|
|
248
|
+
? `${p.success}☑${p.reset}`
|
|
249
|
+
: `${p.dim}☐${p.reset}`;
|
|
250
|
+
return `${indent} ${box} ${this.renderInline(task[3] || "")}`;
|
|
251
|
+
}
|
|
235
252
|
// Unordered list
|
|
236
253
|
const ul = line.match(/^(\s*)[*\-+]\s+(.*)/);
|
|
237
254
|
if (ul) {
|
|
@@ -268,9 +285,14 @@ export class MarkdownRenderer {
|
|
|
268
285
|
* The line is accumulated internally — call drainLines() to extract.
|
|
269
286
|
*/
|
|
270
287
|
writeLine(text) {
|
|
271
|
-
|
|
288
|
+
const isBlank = visibleLen(text) === 0;
|
|
289
|
+
if (this.firstLine && isBlank)
|
|
290
|
+
return;
|
|
291
|
+
// Collapse consecutive blank lines to a single one
|
|
292
|
+
if (isBlank && this.lastLineBlank)
|
|
272
293
|
return;
|
|
273
294
|
this.firstLine = false;
|
|
295
|
+
this.lastLineBlank = isBlank;
|
|
274
296
|
this.pendingLines.push(` ${text}`);
|
|
275
297
|
}
|
|
276
298
|
}
|
|
@@ -6,6 +6,8 @@ export interface ToolCallRender {
|
|
|
6
6
|
command?: string;
|
|
7
7
|
/** Tool kind from ACP (read, edit, execute, search, etc.). */
|
|
8
8
|
kind?: string;
|
|
9
|
+
/** Custom icon character — when set, tool name is omitted (icon implies tool). */
|
|
10
|
+
icon?: string;
|
|
9
11
|
/** File locations affected by the tool call. */
|
|
10
12
|
locations?: {
|
|
11
13
|
path: string;
|
|
@@ -13,6 +15,8 @@ export interface ToolCallRender {
|
|
|
13
15
|
}[];
|
|
14
16
|
/** Raw input parameters sent to the tool. */
|
|
15
17
|
rawInput?: unknown;
|
|
18
|
+
/** Pre-formatted display detail from tool's formatCall(). Takes precedence over rawInput extraction. */
|
|
19
|
+
displayDetail?: string;
|
|
16
20
|
}
|
|
17
21
|
export interface ToolResultRender {
|
|
18
22
|
exitCode: number | null;
|
|
@@ -39,6 +39,7 @@ const KIND_ICONS = {
|
|
|
39
39
|
move: "↗",
|
|
40
40
|
search: "⌕",
|
|
41
41
|
execute: "▶",
|
|
42
|
+
display: "◇",
|
|
42
43
|
think: "◇",
|
|
43
44
|
fetch: "↓",
|
|
44
45
|
switch_mode: "⇄",
|
|
@@ -49,7 +50,10 @@ function kindIcon(kind) {
|
|
|
49
50
|
// ── Tool call rendering ──────────────────────────────────────────
|
|
50
51
|
export function renderToolCall(tool, width) {
|
|
51
52
|
const mode = selectToolDisplayMode(width);
|
|
52
|
-
const icon = kindIcon(tool.kind);
|
|
53
|
+
const icon = tool.icon ?? kindIcon(tool.kind);
|
|
54
|
+
// If the tool registered a custom icon, it's self-describing — omit the name.
|
|
55
|
+
// Otherwise, include the tool name so the user knows what ran.
|
|
56
|
+
const hasCustomIcon = !!tool.icon;
|
|
53
57
|
if (mode === "summary") {
|
|
54
58
|
const text = truncateVisible(`${icon} ${tool.title}`, width);
|
|
55
59
|
return [`${p.warning}${text}${p.reset}`];
|
|
@@ -58,7 +62,10 @@ export function renderToolCall(tool, width) {
|
|
|
58
62
|
// Build a compact detail string to append after the title
|
|
59
63
|
let detail = "";
|
|
60
64
|
const cwd = process.cwd();
|
|
61
|
-
if (mode === "full") {
|
|
65
|
+
if (mode === "full" && tool.displayDetail) {
|
|
66
|
+
detail = tool.displayDetail;
|
|
67
|
+
}
|
|
68
|
+
else if (mode === "full") {
|
|
62
69
|
if (tool.command) {
|
|
63
70
|
detail = `$ ${tool.command}`;
|
|
64
71
|
}
|
|
@@ -97,14 +104,24 @@ export function renderToolCall(tool, width) {
|
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
106
|
}
|
|
100
|
-
// Render as single line: icon +
|
|
101
|
-
// Falls back to icon + title when no detail is available
|
|
107
|
+
// Render as single line: icon + kind + detail
|
|
102
108
|
const maxDetailW = Math.max(1, width - 4);
|
|
103
|
-
if (detail) {
|
|
109
|
+
if (detail && hasCustomIcon && tool.kind) {
|
|
110
|
+
const combined = `${tool.kind} ${detail}`;
|
|
111
|
+
const truncated = combined.length > maxDetailW ? combined.slice(0, maxDetailW - 1) + "…" : combined;
|
|
112
|
+
lines.push(`${p.warning}${icon}${p.reset} ${p.dim}${truncated}${p.reset}`);
|
|
113
|
+
}
|
|
114
|
+
else if (detail && hasCustomIcon) {
|
|
104
115
|
if (detail.length > maxDetailW)
|
|
105
116
|
detail = detail.slice(0, maxDetailW - 1) + "…";
|
|
106
117
|
lines.push(`${p.warning}${icon}${p.reset} ${p.dim}${detail}${p.reset}`);
|
|
107
118
|
}
|
|
119
|
+
else if (detail) {
|
|
120
|
+
const prefix = `${tool.title}: `;
|
|
121
|
+
const combined = prefix + detail;
|
|
122
|
+
const truncated = combined.length > maxDetailW ? combined.slice(0, maxDetailW - 1) + "…" : combined;
|
|
123
|
+
lines.push(`${p.warning}${icon}${p.reset} ${p.dim}${truncated}${p.reset}`);
|
|
124
|
+
}
|
|
108
125
|
else {
|
|
109
126
|
lines.push(`${p.warning}${icon} ${tool.title}${p.reset}`);
|
|
110
127
|
}
|
|
@@ -30,8 +30,8 @@ function createUserShellTool(bus: EventBus) {
|
|
|
30
30
|
|
|
31
31
|
return tool(
|
|
32
32
|
"user_shell",
|
|
33
|
-
"Run a command in the user's live shell (
|
|
34
|
-
"
|
|
33
|
+
"Run a command with lasting effects in the user's live shell (cd, export, " +
|
|
34
|
+
"install packages, start servers) or show output the user wants to see. " +
|
|
35
35
|
"Set return_output=true only if you need to inspect the result.",
|
|
36
36
|
{
|
|
37
37
|
command: z.string().describe("Command to execute in user's shell"),
|
|
@@ -71,12 +71,8 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
71
71
|
const listeners: Array<{ event: string; fn: Function }> = [];
|
|
72
72
|
|
|
73
73
|
const wireListeners = () => {
|
|
74
|
-
const onSubmit = async ({ query: userQuery
|
|
75
|
-
|
|
76
|
-
? `${modeInstruction}\n${userQuery}`
|
|
77
|
-
: userQuery;
|
|
78
|
-
|
|
79
|
-
bus.emit("agent:query", { query: userQuery, modeLabel });
|
|
74
|
+
const onSubmit = async ({ query: userQuery }: any) => {
|
|
75
|
+
bus.emit("agent:query", { query: userQuery });
|
|
80
76
|
bus.emit("agent:processing-start", {});
|
|
81
77
|
|
|
82
78
|
let fullResponseText = "";
|
|
@@ -84,7 +80,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
84
80
|
|
|
85
81
|
try {
|
|
86
82
|
activeQuery = query({
|
|
87
|
-
prompt,
|
|
83
|
+
prompt: userQuery,
|
|
88
84
|
options: {
|
|
89
85
|
cwd: process.cwd(),
|
|
90
86
|
systemPrompt: {
|
|
@@ -92,9 +88,9 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
92
88
|
preset: "claude_code",
|
|
93
89
|
append:
|
|
94
90
|
"You are running inside agent-sh, a terminal wrapper.\n" +
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
91
|
+
"Use your standard tools (Read, Edit, Write, Bash, Glob, Grep) for investigation.\n" +
|
|
92
|
+
"Use mcp__agent-sh__user_shell to run commands in the user's live shell when they ask to see output or need lasting effects (cd, install, start servers).\n" +
|
|
93
|
+
"Default to standard tools. Use user_shell when the user is the intended audience for the output or the command has real effects.",
|
|
98
94
|
},
|
|
99
95
|
mcpServers: { "agent-sh": shellServer },
|
|
100
96
|
allowedTools: [
|
|
@@ -48,17 +48,16 @@ function createUserShellToolDef(bus: EventBus) {
|
|
|
48
48
|
name: "user_shell",
|
|
49
49
|
label: "user_shell",
|
|
50
50
|
description:
|
|
51
|
-
"Run a command in the user's live shell (
|
|
52
|
-
"
|
|
51
|
+
"Run a command with lasting effects in the user's live shell (cd, export, " +
|
|
52
|
+
"install packages, start servers) or show output the user wants to see. " +
|
|
53
53
|
"Output is shown directly to the user. Set return_output=true only " +
|
|
54
54
|
"if you need to inspect the result.",
|
|
55
|
-
promptSnippet: "Execute commands in the user's live terminal (PTY).
|
|
55
|
+
promptSnippet: "Execute commands in the user's live terminal (PTY).",
|
|
56
56
|
promptGuidelines: [
|
|
57
|
-
"You are running inside agent-sh, a terminal wrapper
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"user_shell executes in the user's actual shell (their aliases, env vars, cwd). Use bash for background work.",
|
|
57
|
+
"You are running inside agent-sh, a terminal wrapper.",
|
|
58
|
+
"Use your standard tools (bash, file ops) for investigation — output goes to you, not the user.",
|
|
59
|
+
"Use user_shell to run commands in the user's live shell when they ask to see output or need lasting effects (cd, install, start servers).",
|
|
60
|
+
"Default to standard tools. Use user_shell when the user is the intended audience for the output or the command has real effects.",
|
|
62
61
|
],
|
|
63
62
|
parameters: schema,
|
|
64
63
|
|
|
@@ -203,7 +202,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
203
202
|
const listeners: Array<{ event: string; fn: Function }> = [];
|
|
204
203
|
|
|
205
204
|
const wireListeners = () => {
|
|
206
|
-
const onSubmit = async ({ query
|
|
205
|
+
const onSubmit = async ({ query }: any) => {
|
|
207
206
|
if (!session) {
|
|
208
207
|
bus.emit("agent:error", {
|
|
209
208
|
message: booting ? "pi is still starting up..." : "pi session not initialized",
|
|
@@ -212,12 +211,11 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
212
211
|
return;
|
|
213
212
|
}
|
|
214
213
|
|
|
215
|
-
|
|
216
|
-
bus.emit("agent:query", { query, modeLabel });
|
|
214
|
+
bus.emit("agent:query", { query });
|
|
217
215
|
bus.emit("agent:processing-start", {});
|
|
218
216
|
|
|
219
217
|
try {
|
|
220
|
-
await session.prompt(
|
|
218
|
+
await session.prompt(query);
|
|
221
219
|
} catch (err) {
|
|
222
220
|
bus.emit("agent:error", {
|
|
223
221
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret guard extension.
|
|
3
|
+
*
|
|
4
|
+
* Redacts sensitive patterns (API keys, tokens, passwords) from tool output
|
|
5
|
+
* — both the streamed terminal display and the content sent back to the LLM.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* agent-sh -e ./examples/extensions/secret-guard.ts
|
|
9
|
+
*
|
|
10
|
+
* # Or install permanently:
|
|
11
|
+
* cp examples/extensions/secret-guard.ts ~/.agent-sh/extensions/
|
|
12
|
+
*
|
|
13
|
+
* Configuration (~/.agent-sh/settings.json):
|
|
14
|
+
* {
|
|
15
|
+
* "secret-guard": {
|
|
16
|
+
* "extraPatterns": ["CUSTOM_\\w+=\\S+"],
|
|
17
|
+
* "redactText": "***REDACTED***"
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
22
|
+
|
|
23
|
+
// Common secret patterns — each matches key=value or key: value formats
|
|
24
|
+
const DEFAULT_PATTERNS = [
|
|
25
|
+
// API keys and tokens (generic)
|
|
26
|
+
/(?:api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token|secret[_-]?key|private[_-]?key)\s*[=:]\s*\S+/gi,
|
|
27
|
+
// AWS
|
|
28
|
+
/(?:AKIA|ASIA)[A-Z0-9]{16}/g,
|
|
29
|
+
/(?:aws_secret_access_key|aws_session_token)\s*[=:]\s*\S+/gi,
|
|
30
|
+
// Bearer tokens
|
|
31
|
+
/Bearer\s+[A-Za-z0-9\-._~+/]+=*/g,
|
|
32
|
+
// GitHub tokens
|
|
33
|
+
/gh[pousr]_[A-Za-z0-9_]{36,}/g,
|
|
34
|
+
// Anthropic / OpenAI keys
|
|
35
|
+
/sk-(?:ant-)?[A-Za-z0-9\-_]{10,}/g,
|
|
36
|
+
// Generic long hex/base64 secrets (env var assignment)
|
|
37
|
+
/(?:SECRET|TOKEN|PASSWORD|PASSWD|API_KEY|PRIVATE_KEY)\s*[=:]\s*\S+/gi,
|
|
38
|
+
// Connection strings with passwords
|
|
39
|
+
/[a-z+]+:\/\/[^:]+:[^@\s]+@/gi,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export default function activate(ctx: ExtensionContext) {
|
|
43
|
+
const { bus } = ctx;
|
|
44
|
+
const config = ctx.getExtensionSettings("secret-guard", {
|
|
45
|
+
extraPatterns: [] as string[],
|
|
46
|
+
redactText: "***REDACTED***",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const patterns = [
|
|
50
|
+
...DEFAULT_PATTERNS,
|
|
51
|
+
...config.extraPatterns.map((p: string) => new RegExp(p, "gi")),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function redact(text: string): string {
|
|
55
|
+
let result = text;
|
|
56
|
+
for (const pattern of patterns) {
|
|
57
|
+
// Reset lastIndex for stateful regex (global flag)
|
|
58
|
+
pattern.lastIndex = 0;
|
|
59
|
+
result = result.replace(pattern, config.redactText);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Redact the dynamic context (shell history, cwd, etc.) before it's sent
|
|
65
|
+
// to the LLM. This is the chokepoint — everything the model sees passes
|
|
66
|
+
// through dynamic-context:build.
|
|
67
|
+
ctx.advise("dynamic-context:build", (next) => {
|
|
68
|
+
return redact(next());
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Advise tool:execute to wrap both streaming output and final result.
|
|
72
|
+
// Chunks from child processes arrive at arbitrary byte boundaries, so a
|
|
73
|
+
// secret like "sk-ant-abc123" could be split across two chunks. We
|
|
74
|
+
// line-buffer: accumulate until we see '\n', redact complete lines, flush.
|
|
75
|
+
ctx.advise("tool:execute", async (next, toolCtx) => {
|
|
76
|
+
const origOnChunk = toolCtx.onChunk;
|
|
77
|
+
if (origOnChunk) {
|
|
78
|
+
let buf = "";
|
|
79
|
+
toolCtx.onChunk = (chunk: string) => {
|
|
80
|
+
buf += chunk;
|
|
81
|
+
const lastNl = buf.lastIndexOf("\n");
|
|
82
|
+
if (lastNl !== -1) {
|
|
83
|
+
// Flush all complete lines, redacted
|
|
84
|
+
origOnChunk(redact(buf.slice(0, lastNl + 1)));
|
|
85
|
+
buf = buf.slice(lastNl + 1);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const result = await next(toolCtx);
|
|
90
|
+
|
|
91
|
+
// Flush any remaining partial line
|
|
92
|
+
if (buf) origOnChunk(redact(buf));
|
|
93
|
+
|
|
94
|
+
return { ...result, content: redact(result.content) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = await next(toolCtx);
|
|
98
|
+
return { ...result, content: redact(result.content) };
|
|
99
|
+
});
|
|
100
|
+
}
|