agent-sh 0.12.1 → 0.12.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 +10 -4
- package/dist/agent/agent-loop.js +26 -11
- package/dist/agent/conversation-state.d.ts +4 -0
- package/dist/agent/conversation-state.js +44 -0
- package/dist/agent/tools/bash.js +10 -3
- package/dist/agent/types.d.ts +3 -1
- package/dist/core.d.ts +2 -0
- package/dist/core.js +1 -0
- package/dist/index.js +1 -0
- package/dist/shell/output-parser.d.ts +2 -1
- package/dist/shell/output-parser.js +33 -18
- package/dist/shell/shell.d.ts +1 -0
- package/dist/shell/shell.js +8 -6
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -23,16 +23,22 @@ I still use Claude Code and pi for serious coding work — this doesn't replace
|
|
|
23
23
|
|
|
24
24
|
## Quick Start
|
|
25
25
|
|
|
26
|
-
Install
|
|
26
|
+
Install from npm:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npm install -g
|
|
29
|
+
npm install -g agent-sh
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Re-run the same command to update. Patch releases ship frequently; `npm update -g agent-sh` works too.
|
|
33
|
+
|
|
34
|
+
For unreleased changes on `main`, clone and link locally — this avoids `npm install -g github:...`, which builds on your machine and requires a working TypeScript toolchain:
|
|
33
35
|
|
|
34
36
|
```bash
|
|
35
|
-
|
|
37
|
+
git clone https://github.com/guanyilun/agent-sh.git
|
|
38
|
+
cd agent-sh
|
|
39
|
+
npm install # installs devDependencies (typescript, etc.)
|
|
40
|
+
npm run build # produces dist/
|
|
41
|
+
npm link # exposes `agent-sh` globally
|
|
36
42
|
```
|
|
37
43
|
|
|
38
44
|
Pick one of the zero-config paths below — no settings file needed. agent-sh auto-activates a built-in provider when it sees a known key.
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -29,6 +29,16 @@ import { discoverGlobalSkills, discoverProjectSkills } from "./skills.js";
|
|
|
29
29
|
* the LLM via the API `tools` param (or via load_tool in deferred-
|
|
30
30
|
* lookup mode) — this only trims the always-visible catalog.
|
|
31
31
|
*/
|
|
32
|
+
/** Reject on abort; orphaned `p` keeps running but its result is dropped. */
|
|
33
|
+
function raceAbort(p, signal) {
|
|
34
|
+
if (signal.aborted)
|
|
35
|
+
return Promise.reject(new Error("cancelled"));
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const onAbort = () => reject(new Error("cancelled"));
|
|
38
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
39
|
+
p.then((v) => { signal.removeEventListener("abort", onAbort); resolve(v); }, (e) => { signal.removeEventListener("abort", onAbort); reject(e); });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
32
42
|
function summarizeDescription(desc) {
|
|
33
43
|
const firstLine = desc.split("\n", 1)[0];
|
|
34
44
|
const sentenceEnd = firstLine.search(/[.!?](\s|$)/);
|
|
@@ -817,12 +827,11 @@ export class AgentLoop {
|
|
|
817
827
|
this.conversation.addSystemNote(text);
|
|
818
828
|
this.bus.emit("conversation:message-appended", { role: "system", content: text });
|
|
819
829
|
});
|
|
830
|
+
// Fires on user-abort; extensions advise per tool name for cleanup.
|
|
831
|
+
h.define("tool:cancel", (_ctx) => { });
|
|
820
832
|
// Wraps each tool call: permission → execute → emit events.
|
|
821
|
-
// Extensions advise to add safe-mode, logging, metrics, custom policies.
|
|
822
|
-
// The ctx.onChunk callback is exposed so advisors can wrap it to
|
|
823
|
-
// intercept/transform streamed tool output (e.g. secret redaction).
|
|
824
833
|
h.define("tool:execute", async (ctx) => {
|
|
825
|
-
const { name, id, args, tool } = ctx;
|
|
834
|
+
const { name, id, args, tool, signal } = ctx;
|
|
826
835
|
// Validate required input fields before display/permission/execute.
|
|
827
836
|
// Some models emit wrong arg names (e.g. `file_path` instead of `path`),
|
|
828
837
|
// and downstream helpers assume required strings are present.
|
|
@@ -918,16 +927,21 @@ export class AgentLoop {
|
|
|
918
927
|
const onChunk = (tool.showOutput !== false && !diffShown)
|
|
919
928
|
? ctx.onChunk
|
|
920
929
|
: undefined;
|
|
921
|
-
const toolCtx =
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
// instead of the throw killing the whole turn.
|
|
930
|
+
const toolCtx = { signal };
|
|
931
|
+
if (this.compositor) {
|
|
932
|
+
toolCtx.ui = createToolUI(this.bus, this.compositor.surface("agent"));
|
|
933
|
+
}
|
|
926
934
|
let result;
|
|
927
935
|
try {
|
|
928
|
-
result = await tool.execute(args, onChunk, toolCtx);
|
|
936
|
+
result = await raceAbort(tool.execute(args, onChunk, toolCtx), signal);
|
|
929
937
|
}
|
|
930
938
|
catch (err) {
|
|
939
|
+
if (signal.aborted) {
|
|
940
|
+
try {
|
|
941
|
+
this.handlers.call("tool:cancel", { name, args, reason: "user-aborted" });
|
|
942
|
+
}
|
|
943
|
+
catch { }
|
|
944
|
+
}
|
|
931
945
|
const message = err instanceof Error ? err.message : String(err);
|
|
932
946
|
result = { content: message, exitCode: 1, isError: true };
|
|
933
947
|
}
|
|
@@ -1169,7 +1183,8 @@ export class AgentLoop {
|
|
|
1169
1183
|
this.bus.emit("agent:tool-output-chunk", { chunk });
|
|
1170
1184
|
};
|
|
1171
1185
|
const result = await this.handlers.call("tool:execute", { name: tc.name, id: tc.id, args, tool, onChunk: defaultOnChunk,
|
|
1172
|
-
batchIndex, batchTotal: batchTotal > 1 ? batchTotal : undefined
|
|
1186
|
+
batchIndex, batchTotal: batchTotal > 1 ? batchTotal : undefined,
|
|
1187
|
+
signal });
|
|
1173
1188
|
// Truncate large outputs to avoid blowing context
|
|
1174
1189
|
let content = result.content;
|
|
1175
1190
|
const maxBytes = 16_384; // ~4k tokens
|
|
@@ -38,6 +38,7 @@ export declare class ConversationState {
|
|
|
38
38
|
private nextSeq;
|
|
39
39
|
private lastApiTokenCount;
|
|
40
40
|
private lastApiMessageCount;
|
|
41
|
+
private pendingNotes;
|
|
41
42
|
constructor(handlers?: HandlerFunctions, instanceId?: string);
|
|
42
43
|
/** Get JSON.stringify of messages, cached until next mutation. */
|
|
43
44
|
private getMessagesJson;
|
|
@@ -53,7 +54,10 @@ export declare class ConversationState {
|
|
|
53
54
|
addToolResult(toolCallId: string, content: string, isError?: boolean): void;
|
|
54
55
|
/** Add tool results as a user message (for inline tool protocol). */
|
|
55
56
|
addToolResultInline(content: string): void;
|
|
57
|
+
/** Safe from any context: queues if mid-tool-pair, appends otherwise. */
|
|
56
58
|
addSystemNote(text: string): void;
|
|
59
|
+
private hasOpenToolCalls;
|
|
60
|
+
private flushPendingNotes;
|
|
57
61
|
getMessages(): ChatCompletionMessageParam[];
|
|
58
62
|
/**
|
|
59
63
|
* If a stream was interrupted mid-tool-execution, an assistant message
|
|
@@ -56,6 +56,10 @@ export class ConversationState {
|
|
|
56
56
|
nextSeq = 1;
|
|
57
57
|
lastApiTokenCount = null;
|
|
58
58
|
lastApiMessageCount = 0;
|
|
59
|
+
// Notes queued when addSystemNote fires mid-tool-pair; flushed once
|
|
60
|
+
// the trailing tool_result lands. Splicing into the gap breaks
|
|
61
|
+
// reasoning_content pairing and is rejected by strict providers.
|
|
62
|
+
pendingNotes = [];
|
|
59
63
|
constructor(handlers, instanceId = "0000") {
|
|
60
64
|
this.handlers = handlers ?? null;
|
|
61
65
|
this.instanceId = instanceId;
|
|
@@ -100,16 +104,54 @@ export class ConversationState {
|
|
|
100
104
|
if (isError)
|
|
101
105
|
this.toolErrors.add(toolCallId);
|
|
102
106
|
this.invalidateMessagesCache();
|
|
107
|
+
this.flushPendingNotes();
|
|
103
108
|
}
|
|
104
109
|
/** Add tool results as a user message (for inline tool protocol). */
|
|
105
110
|
addToolResultInline(content) {
|
|
106
111
|
this.messages.push({ role: "user", content });
|
|
107
112
|
this.invalidateMessagesCache();
|
|
113
|
+
this.flushPendingNotes();
|
|
108
114
|
}
|
|
115
|
+
/** Safe from any context: queues if mid-tool-pair, appends otherwise. */
|
|
109
116
|
addSystemNote(text) {
|
|
117
|
+
if (this.hasOpenToolCalls()) {
|
|
118
|
+
this.pendingNotes.push(text);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
110
121
|
this.messages.push({ role: "user", content: text });
|
|
111
122
|
this.invalidateMessagesCache();
|
|
112
123
|
}
|
|
124
|
+
hasOpenToolCalls() {
|
|
125
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
126
|
+
const msg = this.messages[i];
|
|
127
|
+
if (msg.role === "tool")
|
|
128
|
+
continue;
|
|
129
|
+
if (msg.role !== "assistant")
|
|
130
|
+
return false;
|
|
131
|
+
if (!("tool_calls" in msg) || !msg.tool_calls)
|
|
132
|
+
return false;
|
|
133
|
+
const answered = new Set();
|
|
134
|
+
for (let j = i + 1; j < this.messages.length; j++) {
|
|
135
|
+
const m = this.messages[j];
|
|
136
|
+
if (m.role !== "tool")
|
|
137
|
+
break;
|
|
138
|
+
answered.add(m.tool_call_id);
|
|
139
|
+
}
|
|
140
|
+
return msg.tool_calls.some((tc) => !answered.has(tc.id));
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
flushPendingNotes() {
|
|
145
|
+
if (this.pendingNotes.length === 0)
|
|
146
|
+
return;
|
|
147
|
+
if (this.hasOpenToolCalls())
|
|
148
|
+
return;
|
|
149
|
+
for (const text of this.pendingNotes) {
|
|
150
|
+
this.messages.push({ role: "user", content: text });
|
|
151
|
+
}
|
|
152
|
+
this.pendingNotes = [];
|
|
153
|
+
this.invalidateMessagesCache();
|
|
154
|
+
}
|
|
113
155
|
getMessages() {
|
|
114
156
|
return this.normalizeReasoningConsistency(this.stubDanglingToolCalls(this.messages));
|
|
115
157
|
}
|
|
@@ -175,6 +217,7 @@ export class ConversationState {
|
|
|
175
217
|
this.invalidateMessagesCache();
|
|
176
218
|
this.lastApiTokenCount = null;
|
|
177
219
|
this.lastApiMessageCount = 0;
|
|
220
|
+
this.flushPendingNotes();
|
|
178
221
|
}
|
|
179
222
|
pruneToolErrors() {
|
|
180
223
|
if (this.toolErrors.size === 0)
|
|
@@ -474,6 +517,7 @@ export class ConversationState {
|
|
|
474
517
|
this.nuclearEntries = [];
|
|
475
518
|
this.nuclearBySeq.clear();
|
|
476
519
|
this.recallArchive.clear();
|
|
520
|
+
this.pendingNotes = [];
|
|
477
521
|
this.invalidateMessagesCache();
|
|
478
522
|
this.lastApiTokenCount = null;
|
|
479
523
|
this.lastApiMessageCount = 0;
|
package/dist/agent/tools/bash.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { executeCommand } from "../../executor.js";
|
|
1
|
+
import { executeCommand, killSession } from "../../executor.js";
|
|
2
2
|
export function createBashTool(opts) {
|
|
3
3
|
return {
|
|
4
4
|
name: "bash",
|
|
@@ -33,7 +33,7 @@ export function createBashTool(opts) {
|
|
|
33
33
|
icon: "▶",
|
|
34
34
|
locations: [],
|
|
35
35
|
}),
|
|
36
|
-
async execute(args, onChunk) {
|
|
36
|
+
async execute(args, onChunk, ctx) {
|
|
37
37
|
const command = args.command;
|
|
38
38
|
const timeout = (args.timeout ?? 60) * 1000;
|
|
39
39
|
// Let extensions intercept before execution
|
|
@@ -57,7 +57,14 @@ export function createBashTool(opts) {
|
|
|
57
57
|
timeout,
|
|
58
58
|
onOutput: onChunk,
|
|
59
59
|
});
|
|
60
|
-
|
|
60
|
+
const onAbort = () => killSession(session);
|
|
61
|
+
ctx?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
62
|
+
try {
|
|
63
|
+
await done;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
ctx?.signal?.removeEventListener("abort", onAbort);
|
|
67
|
+
}
|
|
61
68
|
const content = session.truncated
|
|
62
69
|
? `[output truncated, showing last portion]\n${session.output}`
|
|
63
70
|
: session.output;
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -64,7 +64,9 @@ export interface ToolUI {
|
|
|
64
64
|
}
|
|
65
65
|
/** Context passed to tool execute() as optional third parameter. */
|
|
66
66
|
export interface ToolExecutionContext {
|
|
67
|
-
ui
|
|
67
|
+
ui?: ToolUI;
|
|
68
|
+
/** Aborted on Ctrl-C — tools with subprocess work should listen and clean up. */
|
|
69
|
+
signal?: AbortSignal;
|
|
68
70
|
}
|
|
69
71
|
export interface ToolDefinition {
|
|
70
72
|
name: string;
|
package/dist/core.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface AgentShellCore {
|
|
|
33
33
|
contextManager: ContextManager;
|
|
34
34
|
/** Handler registry for define/advise/call. */
|
|
35
35
|
handlers: HandlerRegistry;
|
|
36
|
+
/** Unique id for this agent process; used for shell-marker tagging and lineage tracking. */
|
|
37
|
+
instanceId: string;
|
|
36
38
|
/** Activate the agent backend (call after extensions load). */
|
|
37
39
|
activateBackend(): void;
|
|
38
40
|
/** Convenience: emit agent:submit and await the response. */
|
package/dist/core.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -218,6 +218,7 @@ async function main() {
|
|
|
218
218
|
rows,
|
|
219
219
|
shell: config.shell || process.env.SHELL || "/bin/bash",
|
|
220
220
|
cwd: process.cwd(),
|
|
221
|
+
instanceId: core.instanceId,
|
|
221
222
|
onShowAgentInfo: () => {
|
|
222
223
|
if (agentInfo) {
|
|
223
224
|
return { info: `${p.dim}${agentInfo.name}${agentInfo.model ? ` (${agentInfo.model})` : ""}${p.reset}` };
|
|
@@ -6,11 +6,12 @@ import type { EventBus } from "../event-bus.js";
|
|
|
6
6
|
export declare class OutputParser {
|
|
7
7
|
private bus;
|
|
8
8
|
private cwd;
|
|
9
|
+
private ownTag;
|
|
9
10
|
private currentOutputCapture;
|
|
10
11
|
private lastCommand;
|
|
11
12
|
private foregroundBusy;
|
|
12
13
|
private promptReady;
|
|
13
|
-
constructor(bus: EventBus, initialCwd: string);
|
|
14
|
+
constructor(bus: EventBus, initialCwd: string, ownTag: string);
|
|
14
15
|
/** Process a chunk of PTY output data. */
|
|
15
16
|
processData(data: string): void;
|
|
16
17
|
/** Called when user presses Enter on a non-empty line. */
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { stripAnsi } from "../utils/ansi.js";
|
|
2
|
+
// Self-emitted form: \e]<num>;id=<own>;<body>\a — only this is honored.
|
|
3
|
+
// Anything else (mismatched tag, untagged) is ignored as opaque foreground output.
|
|
4
|
+
const PROMPT_RE = /\x1b\]9999;(?:id=([a-f0-9]+);)?PROMPT\x07/;
|
|
5
|
+
const PREEXEC_RE = /\x1b\]9997;(?:id=([a-f0-9]+);)?([^\x07]*)\x07/;
|
|
6
|
+
const READY_RE = /\x1b\]9998;(?:id=([a-f0-9]+);)?READY\x07/;
|
|
2
7
|
/**
|
|
3
8
|
* Parses PTY output to detect command boundaries, track cwd,
|
|
4
9
|
* and emit shell events. Owns the command lifecycle state.
|
|
@@ -6,13 +11,16 @@ import { stripAnsi } from "../utils/ansi.js";
|
|
|
6
11
|
export class OutputParser {
|
|
7
12
|
bus;
|
|
8
13
|
cwd;
|
|
14
|
+
ownTag;
|
|
9
15
|
currentOutputCapture = "";
|
|
10
16
|
lastCommand = "";
|
|
11
17
|
foregroundBusy = false;
|
|
12
18
|
promptReady = false;
|
|
13
|
-
constructor(bus, initialCwd) {
|
|
19
|
+
constructor(bus, initialCwd, ownTag) {
|
|
14
20
|
this.bus = bus;
|
|
15
21
|
this.cwd = initialCwd;
|
|
22
|
+
// Strip the "id=" prefix; we compare the value alone.
|
|
23
|
+
this.ownTag = ownTag.startsWith("id=") ? ownTag.slice(3) : ownTag;
|
|
16
24
|
}
|
|
17
25
|
/** Process a chunk of PTY output data. */
|
|
18
26
|
processData(data) {
|
|
@@ -49,24 +57,22 @@ export class OutputParser {
|
|
|
49
57
|
* completion. Returns data with the OSC stripped out.
|
|
50
58
|
*/
|
|
51
59
|
handlePreexec(data) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
if (idx === -1)
|
|
60
|
+
const match = PREEXEC_RE.exec(data);
|
|
61
|
+
if (!match)
|
|
55
62
|
return data;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return data
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
if (match[1] !== this.ownTag) {
|
|
64
|
+
// Nested instance or untagged foreign emission — strip and ignore.
|
|
65
|
+
return data.slice(0, match.index) + data.slice(match.index + match[0].length);
|
|
66
|
+
}
|
|
67
|
+
const command = match[2];
|
|
61
68
|
this.lastCommand = command;
|
|
62
|
-
this.currentOutputCapture = ""; // discard
|
|
69
|
+
this.currentOutputCapture = ""; // discard echo accumulated before preexec
|
|
63
70
|
if (!this.foregroundBusy) {
|
|
64
71
|
this.foregroundBusy = true;
|
|
65
72
|
this.bus.emit("shell:foreground-busy", { busy: true });
|
|
66
73
|
}
|
|
67
74
|
this.bus.emit("shell:command-start", { command, cwd: this.cwd });
|
|
68
|
-
|
|
69
|
-
return data.slice(endIdx + 1);
|
|
75
|
+
return data.slice(match.index + match[0].length);
|
|
70
76
|
}
|
|
71
77
|
parseOSC7(data) {
|
|
72
78
|
const match = data.match(/\x1b\]7;file:\/\/[^/]*(\/[^\x07\x1b]*)/);
|
|
@@ -83,9 +89,15 @@ export class OutputParser {
|
|
|
83
89
|
* Each time a prompt appears, we finalize the previous command's output.
|
|
84
90
|
*/
|
|
85
91
|
parsePromptMarker(data) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
const match = PROMPT_RE.exec(data);
|
|
93
|
+
if (match) {
|
|
94
|
+
if (match[1] !== this.ownTag) {
|
|
95
|
+
// Nested instance or untagged foreign emission — treat as opaque
|
|
96
|
+
// foreground output, do not finalize our own command.
|
|
97
|
+
this.currentOutputCapture += data;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const markerIdx = match.index;
|
|
89
101
|
// Capture any output that arrived in the same chunk before the marker
|
|
90
102
|
if (markerIdx > 0) {
|
|
91
103
|
this.currentOutputCapture += data.slice(0, markerIdx);
|
|
@@ -125,9 +137,12 @@ export class OutputParser {
|
|
|
125
137
|
* and the shell is ready for input.
|
|
126
138
|
*/
|
|
127
139
|
parsePromptEnd(data) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
const match = READY_RE.exec(data);
|
|
141
|
+
if (!match)
|
|
142
|
+
return;
|
|
143
|
+
if (match[1] !== this.ownTag)
|
|
144
|
+
return;
|
|
145
|
+
this.promptReady = true;
|
|
131
146
|
}
|
|
132
147
|
removeEchoedCommand(output, command) {
|
|
133
148
|
const lines = output.split("\n");
|
package/dist/shell/shell.d.ts
CHANGED
package/dist/shell/shell.js
CHANGED
|
@@ -43,8 +43,10 @@ export class Shell {
|
|
|
43
43
|
}
|
|
44
44
|
const shellBin = (isZsh || isBash) ? opts.shell : "/bin/bash";
|
|
45
45
|
let shellArgs;
|
|
46
|
+
// Per-instance tag so nested agent-sh hooks don't cross-trigger.
|
|
47
|
+
const instanceTag = `id=${opts.instanceId}`;
|
|
46
48
|
const osc7Cmd = 'printf "\\e]7;file://%s%s\\a" "$(hostname)" "$PWD"';
|
|
47
|
-
const promptMarker =
|
|
49
|
+
const promptMarker = `printf "\\e]9999;${instanceTag};PROMPT\\a"`;
|
|
48
50
|
const titleCmd = 'printf "\\e]0;⚡ agent-sh: %s\\a" "${PWD/#$HOME/~}"';
|
|
49
51
|
this.isZsh = isZsh;
|
|
50
52
|
const settings = getSettings();
|
|
@@ -69,11 +71,11 @@ export class Shell {
|
|
|
69
71
|
"# Preexec hook: emit actual command text so agent-sh can track",
|
|
70
72
|
"# history-recalled and tab-completed commands accurately",
|
|
71
73
|
"__agent_sh_preexec() {",
|
|
72
|
-
|
|
74
|
+
` printf "\\e]9997;${instanceTag};%s\\a" "$1"`,
|
|
73
75
|
"}",
|
|
74
76
|
"preexec_functions+=(__agent_sh_preexec)",
|
|
75
77
|
];
|
|
76
|
-
zshrcLines.push("", "# End-of-prompt marker via zle-line-init (fires after prompt is rendered)", "# Chain onto existing widget (p10k uses zle-line-init) rather than clobbering", 'if (( ${+widgets[zle-line-init]} )); then', " zle -A zle-line-init __agent_sh_orig_line_init", " __agent_sh_line_init() {", " zle __agent_sh_orig_line_init",
|
|
78
|
+
zshrcLines.push("", "# End-of-prompt marker via zle-line-init (fires after prompt is rendered)", "# Chain onto existing widget (p10k uses zle-line-init) rather than clobbering", 'if (( ${+widgets[zle-line-init]} )); then', " zle -A zle-line-init __agent_sh_orig_line_init", " __agent_sh_line_init() {", " zle __agent_sh_orig_line_init", ` printf "\\e]9998;${instanceTag};READY\\a"`, " }", "else", " __agent_sh_line_init() {", ` printf "\\e]9998;${instanceTag};READY\\a"`, " }", "fi", "zle -N zle-line-init __agent_sh_line_init", "", "# Hidden widget to trigger prompt redraw from Node.js side", "# Bound to an unused escape sequence that no real key produces", "__agent_sh_redraw() {", " zle reset-prompt", "}", "zle -N __agent_sh_redraw", "bindkey '\\e[9999~' __agent_sh_redraw");
|
|
77
79
|
fs.writeFileSync(path.join(this.tmpDir, ".zshrc"), zshrcLines.join("\n") + "\n");
|
|
78
80
|
env.ZDOTDIR = this.tmpDir;
|
|
79
81
|
shellArgs = ["--no-globalrcs"];
|
|
@@ -106,12 +108,12 @@ export class Shell {
|
|
|
106
108
|
" __agent_sh_preexec_ran=1",
|
|
107
109
|
" local this_cmd",
|
|
108
110
|
` this_cmd=$(HISTTIMEFORMAT='' builtin history 1 | command sed 's/^ *[0-9]* *//')`,
|
|
109
|
-
` printf '\\e]9997;%s\\a' "$this_cmd"`,
|
|
111
|
+
` printf '\\e]9997;${instanceTag};%s\\a' "$this_cmd"`,
|
|
110
112
|
"}",
|
|
111
113
|
"trap '__agent_sh_emit_preexec' DEBUG",
|
|
112
114
|
"",
|
|
113
115
|
"# End-of-prompt marker: append to PS1 (\\[...\\] marks it zero-width)",
|
|
114
|
-
|
|
116
|
+
`case "$PS1" in *9998*) ;; *) PS1="\${PS1}\\[\\e]9998;${instanceTag};READY\\a\\]";; esac`,
|
|
115
117
|
"",
|
|
116
118
|
"# Mirrors the zsh \\e[9999~ reset-prompt widget — used by agent-sh",
|
|
117
119
|
"# to repaint the prompt in place. All keymaps so `set -o vi` works.",
|
|
@@ -155,7 +157,7 @@ export class Shell {
|
|
|
155
157
|
}
|
|
156
158
|
this.bus = opts.bus;
|
|
157
159
|
this.handlers = opts.handlers;
|
|
158
|
-
this.outputParser = new OutputParser(opts.bus, opts.cwd);
|
|
160
|
+
this.outputParser = new OutputParser(opts.bus, opts.cwd, instanceTag);
|
|
159
161
|
// Ensure temp dir cleanup on abnormal exit (SIGKILL won't fire this,
|
|
160
162
|
// but it covers uncaught exceptions and normal process.exit paths)
|
|
161
163
|
if (this.tmpDir) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sh",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "A shell-first terminal where AI is one keystroke away",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/core.js",
|
|
@@ -127,6 +127,8 @@
|
|
|
127
127
|
"node": ">=18"
|
|
128
128
|
},
|
|
129
129
|
"dependencies": {
|
|
130
|
+
"@xterm/addon-serialize": "^0.13.0",
|
|
131
|
+
"@xterm/headless": "^5.5.0",
|
|
130
132
|
"cli-highlight": "^2.1.11",
|
|
131
133
|
"diff": "^9.0.0",
|
|
132
134
|
"marked": "^17.0.6",
|