@yagni-app/code-staging 0.2.1-staging.1032.1 → 0.2.1-staging.1034.1
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/dist/extension/branding.d.ts +11 -1
- package/dist/extension/branding.js +49 -4
- package/dist/extension/cmux/dispatcher.d.ts +25 -0
- package/dist/extension/cmux/dispatcher.js +266 -0
- package/dist/extension/cmux/hooks.d.ts +12 -0
- package/dist/extension/cmux/hooks.js +192 -0
- package/dist/extension/cmux/index.d.ts +3 -0
- package/dist/extension/cmux/index.js +155 -0
- package/dist/extension/cmux/naming.d.ts +5 -0
- package/dist/extension/cmux/naming.js +23 -0
- package/dist/extension/cmux/state.d.ts +33 -0
- package/dist/extension/cmux/state.js +142 -0
- package/dist/extension/index.js +13 -2
- package/package.json +2 -2
|
@@ -69,11 +69,21 @@ export interface MastheadTheme {
|
|
|
69
69
|
bold(s: string): string;
|
|
70
70
|
fg(color: string, s: string): string;
|
|
71
71
|
}
|
|
72
|
+
export interface MastheadOptions {
|
|
73
|
+
/** CLI version (e.g. "0.2.1") shown next to the title; omitted when absent. */
|
|
74
|
+
version?: string;
|
|
75
|
+
/** Working directory shown on the third line (already home-collapsed). */
|
|
76
|
+
cwd?: string;
|
|
77
|
+
}
|
|
72
78
|
/**
|
|
73
79
|
* Build the YAGNI Code startup masthead string, rendered into a header that
|
|
74
80
|
* REPLACES pi's built-in startup banner (which otherwise shows "pi v<version>"
|
|
75
81
|
* and a "Pi can explain its own features…" onboarding line). Kept here as a
|
|
76
82
|
* pure string builder so its content is unit-testable without a terminal.
|
|
83
|
+
*
|
|
84
|
+
* Layout: the YAGNI art on the left, and to its right a three-line block —
|
|
85
|
+
* bold white "YAGNI Code" + version, the tagline, and the cwd. Art rows are
|
|
86
|
+
* padded to a uniform width so the right column stays straight.
|
|
77
87
|
*/
|
|
78
|
-
export declare function buildMastheadString(theme: MastheadTheme): string;
|
|
88
|
+
export declare function buildMastheadString(theme: MastheadTheme, opts?: MastheadOptions): string;
|
|
79
89
|
//# sourceMappingURL=branding.d.ts.map
|
|
@@ -142,15 +142,60 @@ export function brandSystemPrompt(original, opts = {}) {
|
|
|
142
142
|
const CLOSING_REMINDER = "Reminder: you are YAGNI Code. If any text above names another coding agent, " +
|
|
143
143
|
"assistant, or harness, it is not what you are or what you run on.";
|
|
144
144
|
const BRIEF_HEADER = "=== HOW THIS COMPANY WORKS (live context from the YAGNI app) ===";
|
|
145
|
+
/** The YAGNI brand blue used for the masthead art (truecolor #345cb8). */
|
|
146
|
+
const BRAND_HEX = "#345cb8";
|
|
147
|
+
/**
|
|
148
|
+
* Wrap a string in a 24-bit truecolor foreground. The pi Theme only paints
|
|
149
|
+
* named palette colors and can't express our exact brand hex, so the masthead
|
|
150
|
+
* art emits its own ANSI truecolor escape (matching every modern terminal).
|
|
151
|
+
*/
|
|
152
|
+
function paintBrand(s) {
|
|
153
|
+
const n = BRAND_HEX.replace("#", "");
|
|
154
|
+
const r = parseInt(n.slice(0, 2), 16);
|
|
155
|
+
const g = parseInt(n.slice(2, 4), 16);
|
|
156
|
+
const b = parseInt(n.slice(4, 6), 16);
|
|
157
|
+
return `\u001b[38;2;${r};${g};${b}m${s}\u001b[0m`;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The YAGNI wordmark as filled block-art: solid letter interiors (like the
|
|
161
|
+
* Claude robot silhouette), four rows, hand-tuned spacing. Each letter keeps
|
|
162
|
+
* its full form — A's carved counter, G's bowl, N's diagonal.
|
|
163
|
+
*/
|
|
164
|
+
const MASTHEAD_ART = [
|
|
165
|
+
"█ █ ▄▀▀▀▄ ▄▀▀▀▄ █▄ █ ▀█▀",
|
|
166
|
+
" ▀▄▀ █▄▄▄█ █ ▄▄▄ █ █ █ █",
|
|
167
|
+
" █ █ █ ▀▄▄▄▀ █ ██ ▄█▄",
|
|
168
|
+
];
|
|
145
169
|
/**
|
|
146
170
|
* Build the YAGNI Code startup masthead string, rendered into a header that
|
|
147
171
|
* REPLACES pi's built-in startup banner (which otherwise shows "pi v<version>"
|
|
148
172
|
* and a "Pi can explain its own features…" onboarding line). Kept here as a
|
|
149
173
|
* pure string builder so its content is unit-testable without a terminal.
|
|
174
|
+
*
|
|
175
|
+
* Layout: the YAGNI art on the left, and to its right a three-line block —
|
|
176
|
+
* bold white "YAGNI Code" + version, the tagline, and the cwd. Art rows are
|
|
177
|
+
* padded to a uniform width so the right column stays straight.
|
|
150
178
|
*/
|
|
151
|
-
export function buildMastheadString(theme) {
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
179
|
+
export function buildMastheadString(theme, opts = {}) {
|
|
180
|
+
const title = theme.bold(theme.fg("text", "YAGNI Code"));
|
|
181
|
+
const version = opts.version?.trim();
|
|
182
|
+
const line1 = version ? `${title} ${theme.fg("dim", `v${version}`)}` : title;
|
|
183
|
+
const right = [
|
|
184
|
+
line1,
|
|
185
|
+
theme.fg("dim", "a terminal coding agent that knows your company"),
|
|
186
|
+
...(opts.cwd ? [theme.fg("dim", opts.cwd)] : []),
|
|
187
|
+
];
|
|
188
|
+
const artW = Math.max(...MASTHEAD_ART.map((l) => [...l].length));
|
|
189
|
+
const gap = " ";
|
|
190
|
+
const rows = Math.max(MASTHEAD_ART.length, right.length);
|
|
191
|
+
const lines = [];
|
|
192
|
+
for (let i = 0; i < rows; i++) {
|
|
193
|
+
const rawArt = MASTHEAD_ART[i] ?? "";
|
|
194
|
+
const pad = " ".repeat(Math.max(0, artW - [...rawArt].length));
|
|
195
|
+
const left = rawArt ? paintBrand(rawArt) + pad : " ".repeat(artW);
|
|
196
|
+
const r = right[i] ?? "";
|
|
197
|
+
lines.push(r ? `${left}${gap}${r}` : left);
|
|
198
|
+
}
|
|
199
|
+
return lines.join("\n");
|
|
155
200
|
}
|
|
156
201
|
//# sourceMappingURL=branding.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PiExtensionContextSnapshot } from "./state.js";
|
|
2
|
+
export interface CommandResult {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
status: number | null;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
error?: unknown;
|
|
8
|
+
surfaceUnavailable?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function normalizedLaunchArgv(): string[];
|
|
11
|
+
export declare function shouldPreserveEnvKey(key: string): boolean;
|
|
12
|
+
export declare function hookEnvironment(cwd: string, includeSocketPassword?: boolean): NodeJS.ProcessEnv;
|
|
13
|
+
export declare class CmuxDispatcher {
|
|
14
|
+
private static readonly surfaceUnavailableExitCode;
|
|
15
|
+
private controlQueues;
|
|
16
|
+
private unavailableSessions;
|
|
17
|
+
canDispatch(sessionId: string | null): boolean;
|
|
18
|
+
releaseSession(sessionId: string): void;
|
|
19
|
+
run(args: string[], cwd: string, input: string | undefined, context: PiExtensionContextSnapshot): Promise<CommandResult>;
|
|
20
|
+
private execute;
|
|
21
|
+
private spawnCmux;
|
|
22
|
+
private isSurfaceResolutionFailure;
|
|
23
|
+
private surfaceUnavailableResult;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=dispatcher.d.ts.map
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
function resolveExecutable(name) {
|
|
5
|
+
const pathEnv = process.env.PATH || "";
|
|
6
|
+
for (const dir of pathEnv.split(path.delimiter)) {
|
|
7
|
+
if (!dir)
|
|
8
|
+
continue;
|
|
9
|
+
const candidate = path.join(dir, name);
|
|
10
|
+
try {
|
|
11
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
12
|
+
if (fs.statSync(candidate).isFile())
|
|
13
|
+
return candidate;
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
}
|
|
17
|
+
return name;
|
|
18
|
+
}
|
|
19
|
+
function looksLikePiExecutable(value) {
|
|
20
|
+
const base = path.basename(value).toLowerCase();
|
|
21
|
+
return base === "pi" || base === "pi-coding-agent";
|
|
22
|
+
}
|
|
23
|
+
function looksLikePiScript(value) {
|
|
24
|
+
const normalized = value.replaceAll("\\", "/").toLowerCase();
|
|
25
|
+
const base = path.basename(normalized);
|
|
26
|
+
return (normalized.includes("/@earendil-works/pi-coding-agent/") ||
|
|
27
|
+
normalized.includes("/@mariozechner/pi-coding-agent/") ||
|
|
28
|
+
normalized.includes("/packages/coding-agent/") ||
|
|
29
|
+
((base === "cli.js" || base === "cli.ts") &&
|
|
30
|
+
(normalized.includes("pi-coding-agent") || normalized.includes("coding-agent"))));
|
|
31
|
+
}
|
|
32
|
+
export function normalizedLaunchArgv() {
|
|
33
|
+
const raw = Array.isArray(process.argv) ? process.argv.map((value) => String(value)) : [];
|
|
34
|
+
if (raw.length === 0)
|
|
35
|
+
return [resolveExecutable("pi")];
|
|
36
|
+
if (looksLikePiExecutable(raw[0]))
|
|
37
|
+
return raw;
|
|
38
|
+
if (raw.length > 1 && looksLikePiScript(raw[1])) {
|
|
39
|
+
return [resolveExecutable("pi"), ...raw.slice(2)];
|
|
40
|
+
}
|
|
41
|
+
return [resolveExecutable("pi"), ...raw.slice(1)];
|
|
42
|
+
}
|
|
43
|
+
function base64NulSeparated(values) {
|
|
44
|
+
const bytes = [];
|
|
45
|
+
for (const value of values) {
|
|
46
|
+
bytes.push(Buffer.from(String(value), "utf8"));
|
|
47
|
+
bytes.push(Buffer.from([0]));
|
|
48
|
+
}
|
|
49
|
+
return Buffer.concat(bytes).toString("base64");
|
|
50
|
+
}
|
|
51
|
+
function secretLikeEnvKey(key) {
|
|
52
|
+
return /(TOKEN|SECRET|PASSWORD|PASSWD|API[_-]?KEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|CREDENTIAL|AUTHORIZATION|COOKIE)/i.test(key);
|
|
53
|
+
}
|
|
54
|
+
function safePiEnvKey(key) {
|
|
55
|
+
return (key === "PI_CODING_AGENT_DIR" ||
|
|
56
|
+
key === "PI_CONFIG_DIR" ||
|
|
57
|
+
key === "PI_CODING_AGENT_SESSION_DIR" ||
|
|
58
|
+
(key.startsWith("PI_CODING_AGENT_") && !secretLikeEnvKey(key)));
|
|
59
|
+
}
|
|
60
|
+
function safeNodeEnvKey(key) {
|
|
61
|
+
return (key === "NODE_ENV" ||
|
|
62
|
+
key === "NODE_OPTIONS" ||
|
|
63
|
+
key === "NODE_PATH" ||
|
|
64
|
+
key === "NODE_NO_WARNINGS" ||
|
|
65
|
+
key === "NODE_EXTRA_CA_CERTS");
|
|
66
|
+
}
|
|
67
|
+
function safeCmuxEnvKey(key) {
|
|
68
|
+
if (key.startsWith("CMUX_TEST_PI_"))
|
|
69
|
+
return !secretLikeEnvKey(key);
|
|
70
|
+
if (key.startsWith("CMUX_AGENT_LAUNCH_"))
|
|
71
|
+
return !secretLikeEnvKey(key);
|
|
72
|
+
if (key === "CMUX_AGENT_HOOK_STATE_DIR")
|
|
73
|
+
return true;
|
|
74
|
+
if (key === "CMUX_PI_CMUX_BIN" || key === "CMUX_PI_HOOKS_DISABLED")
|
|
75
|
+
return true;
|
|
76
|
+
if (key === "CMUX_SURFACE_ID" || key === "CMUX_WORKSPACE_ID" || key === "CMUX_WINDOW_ID")
|
|
77
|
+
return true;
|
|
78
|
+
if (key === "CMUX_PANE_ID" || key === "CMUX_TAB_ID" || key === "CMUX_PANEL_ID")
|
|
79
|
+
return true;
|
|
80
|
+
if (key === "CMUX_SOCKET" || key === "CMUX_SOCKET_PATH")
|
|
81
|
+
return true;
|
|
82
|
+
if (key === "CMUX_BUNDLE_ID" || key === "CMUX_BUNDLED_CLI_PATH")
|
|
83
|
+
return true;
|
|
84
|
+
if (key === "CMUX_CLI_SENTRY_DISABLED" || key === "CMUX_DEBUG_LOG")
|
|
85
|
+
return true;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
export function shouldPreserveEnvKey(key) {
|
|
89
|
+
if (safeCmuxEnvKey(key))
|
|
90
|
+
return true;
|
|
91
|
+
if (safePiEnvKey(key))
|
|
92
|
+
return true;
|
|
93
|
+
if (safeNodeEnvKey(key))
|
|
94
|
+
return true;
|
|
95
|
+
if (key === "PATH" || key === "HOME" || key === "PWD" || key === "SHELL")
|
|
96
|
+
return true;
|
|
97
|
+
if (key === "USER" || key === "LOGNAME" || key === "TMPDIR" || key === "TZ")
|
|
98
|
+
return true;
|
|
99
|
+
if (key === "LANG" || key.startsWith("LC_"))
|
|
100
|
+
return true;
|
|
101
|
+
if (key === "TERM" || key === "TERM_PROGRAM" || key === "TERM_PROGRAM_VERSION" || key === "COLORTERM")
|
|
102
|
+
return true;
|
|
103
|
+
if (key === "SSH_AUTH_SOCK")
|
|
104
|
+
return true;
|
|
105
|
+
if (key.startsWith("PI_") || key.startsWith("NODE_"))
|
|
106
|
+
return !secretLikeEnvKey(key);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
export function hookEnvironment(cwd, includeSocketPassword = false) {
|
|
110
|
+
const env = {};
|
|
111
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
112
|
+
if (value === undefined)
|
|
113
|
+
continue;
|
|
114
|
+
if (shouldPreserveEnvKey(key))
|
|
115
|
+
env[key] = value;
|
|
116
|
+
}
|
|
117
|
+
if (includeSocketPassword) {
|
|
118
|
+
const socketPassword = process.env.CMUX_SOCKET_PASSWORD;
|
|
119
|
+
if (socketPassword)
|
|
120
|
+
env.CMUX_SOCKET_PASSWORD = socketPassword;
|
|
121
|
+
}
|
|
122
|
+
if (!env.CMUX_AGENT_LAUNCH_ARGV_B64) {
|
|
123
|
+
const argv = normalizedLaunchArgv();
|
|
124
|
+
env.CMUX_AGENT_LAUNCH_KIND = "pi";
|
|
125
|
+
env.CMUX_AGENT_LAUNCH_EXECUTABLE = argv[0] || resolveExecutable("pi");
|
|
126
|
+
env.CMUX_AGENT_LAUNCH_ARGV_B64 = base64NulSeparated(argv);
|
|
127
|
+
env.CMUX_AGENT_LAUNCH_CWD = cwd || process.cwd();
|
|
128
|
+
}
|
|
129
|
+
return env;
|
|
130
|
+
}
|
|
131
|
+
function cmuxExecutable() {
|
|
132
|
+
return process.env.CMUX_PI_CMUX_BIN || "cmux";
|
|
133
|
+
}
|
|
134
|
+
export class CmuxDispatcher {
|
|
135
|
+
static surfaceUnavailableExitCode = 69;
|
|
136
|
+
controlQueues = new Map();
|
|
137
|
+
unavailableSessions = new Set();
|
|
138
|
+
canDispatch(sessionId) {
|
|
139
|
+
return !sessionId || !this.unavailableSessions.has(sessionId);
|
|
140
|
+
}
|
|
141
|
+
releaseSession(sessionId) {
|
|
142
|
+
this.unavailableSessions.delete(sessionId);
|
|
143
|
+
}
|
|
144
|
+
run(args, cwd, input, context) {
|
|
145
|
+
const sessionId = context.sessionId;
|
|
146
|
+
const previous = this.controlQueues.get(sessionId) || Promise.resolve();
|
|
147
|
+
const scheduled = previous.then(() => this.execute(args, cwd, input, context));
|
|
148
|
+
let tail;
|
|
149
|
+
tail = scheduled
|
|
150
|
+
.then(() => undefined, () => undefined)
|
|
151
|
+
.finally(() => {
|
|
152
|
+
if (this.controlQueues.get(sessionId) === tail)
|
|
153
|
+
this.controlQueues.delete(sessionId);
|
|
154
|
+
});
|
|
155
|
+
this.controlQueues.set(sessionId, tail);
|
|
156
|
+
return scheduled;
|
|
157
|
+
}
|
|
158
|
+
async execute(args, cwd, input, context) {
|
|
159
|
+
const sessionId = context.sessionId;
|
|
160
|
+
if (!this.canDispatch(sessionId)) {
|
|
161
|
+
return this.surfaceUnavailableResult();
|
|
162
|
+
}
|
|
163
|
+
const result = await this.spawnCmux(args, cwd, input);
|
|
164
|
+
if (this.isSurfaceResolutionFailure(result)) {
|
|
165
|
+
if (sessionId)
|
|
166
|
+
this.unavailableSessions.add(sessionId);
|
|
167
|
+
return { ...result, surfaceUnavailable: true };
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
spawnCmux(args, cwd, input) {
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
let settled = false;
|
|
174
|
+
let stdout = "";
|
|
175
|
+
let stderr = "";
|
|
176
|
+
let inputError;
|
|
177
|
+
let timeout = null;
|
|
178
|
+
let terminateGrace = null;
|
|
179
|
+
let forceSettleTimeout = null;
|
|
180
|
+
let terminationError;
|
|
181
|
+
const appendOutput = (current, chunk) => {
|
|
182
|
+
const limit = 1024 * 1024;
|
|
183
|
+
if (current.length >= limit)
|
|
184
|
+
return current;
|
|
185
|
+
return current + String(chunk).slice(0, limit - current.length);
|
|
186
|
+
};
|
|
187
|
+
const settle = (result) => {
|
|
188
|
+
if (settled)
|
|
189
|
+
return;
|
|
190
|
+
settled = true;
|
|
191
|
+
if (timeout)
|
|
192
|
+
clearTimeout(timeout);
|
|
193
|
+
if (terminateGrace)
|
|
194
|
+
clearTimeout(terminateGrace);
|
|
195
|
+
if (forceSettleTimeout)
|
|
196
|
+
clearTimeout(forceSettleTimeout);
|
|
197
|
+
resolve(result);
|
|
198
|
+
};
|
|
199
|
+
const terminatedResult = () => ({
|
|
200
|
+
ok: false,
|
|
201
|
+
status: null,
|
|
202
|
+
stdout,
|
|
203
|
+
stderr,
|
|
204
|
+
error: terminationError,
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
const child = spawn(cmuxExecutable(), args, {
|
|
208
|
+
env: hookEnvironment(cwd, true),
|
|
209
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
210
|
+
});
|
|
211
|
+
child.stdout.setEncoding("utf8");
|
|
212
|
+
child.stderr.setEncoding("utf8");
|
|
213
|
+
child.stdout.on("data", (chunk) => { stdout = appendOutput(stdout, chunk); });
|
|
214
|
+
child.stderr.on("data", (chunk) => { stderr = appendOutput(stderr, chunk); });
|
|
215
|
+
child.stdin.on("error", (error) => { inputError = error; });
|
|
216
|
+
const beginTermination = (error) => {
|
|
217
|
+
if (terminationError)
|
|
218
|
+
return;
|
|
219
|
+
terminationError = error;
|
|
220
|
+
child.stdin.destroy();
|
|
221
|
+
try {
|
|
222
|
+
child.kill("SIGTERM");
|
|
223
|
+
}
|
|
224
|
+
catch { }
|
|
225
|
+
terminateGrace = setTimeout(() => {
|
|
226
|
+
try {
|
|
227
|
+
child.kill("SIGKILL");
|
|
228
|
+
}
|
|
229
|
+
catch { }
|
|
230
|
+
forceSettleTimeout = setTimeout(() => {
|
|
231
|
+
child.stdout.destroy();
|
|
232
|
+
child.stderr.destroy();
|
|
233
|
+
child.unref();
|
|
234
|
+
settle(terminatedResult());
|
|
235
|
+
}, 250);
|
|
236
|
+
}, 250);
|
|
237
|
+
};
|
|
238
|
+
child.on("error", (error) => {
|
|
239
|
+
settle(terminationError ? terminatedResult() : { ok: false, status: null, stdout, stderr, error });
|
|
240
|
+
});
|
|
241
|
+
child.on("close", (code) => {
|
|
242
|
+
if (terminationError) {
|
|
243
|
+
settle(terminatedResult());
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const status = typeof code === "number" ? code : null;
|
|
247
|
+
settle({ ok: status === 0 && inputError === undefined, status, stdout, stderr, error: inputError });
|
|
248
|
+
});
|
|
249
|
+
timeout = setTimeout(() => {
|
|
250
|
+
beginTermination(new Error("cmux command timed out after 5000ms"));
|
|
251
|
+
}, 5000);
|
|
252
|
+
child.stdin.end(input);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
settle({ ok: false, status: null, stdout, stderr, error });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
isSurfaceResolutionFailure(result) {
|
|
260
|
+
return !result.ok && result.status === CmuxDispatcher.surfaceUnavailableExitCode;
|
|
261
|
+
}
|
|
262
|
+
surfaceUnavailableResult() {
|
|
263
|
+
return { ok: false, status: null, stdout: "", stderr: "", surfaceUnavailable: true };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=dispatcher.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CmuxDispatcher, CommandResult } from "./dispatcher.js";
|
|
2
|
+
import type { PiExtensionContextSnapshot } from "./state.js";
|
|
3
|
+
type HookExtra = Record<string, unknown>;
|
|
4
|
+
export declare function eventName(subcommand: string): string;
|
|
5
|
+
export declare function surfaceTargetArgs(dispatcher: CmuxDispatcher, sessionId: string): string[] | null;
|
|
6
|
+
export declare function rememberSurfaceTarget(dispatcher: CmuxDispatcher, sessionId: string, result: CommandResult): void;
|
|
7
|
+
export declare function releaseSessionRuntime(dispatcher: CmuxDispatcher, sessionStates: Map<string, unknown>, sessionId: string): void;
|
|
8
|
+
export declare function sendHook(dispatcher: CmuxDispatcher, subcommand: string, context: PiExtensionContextSnapshot, extra?: HookExtra): Promise<boolean>;
|
|
9
|
+
export declare function ensureResumeBinding(dispatcher: CmuxDispatcher, context: PiExtensionContextSnapshot, sessionId: string): Promise<void>;
|
|
10
|
+
export declare function clearResumeBinding(dispatcher: CmuxDispatcher, context: PiExtensionContextSnapshot, sessionId: string): Promise<void>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { normalizedLaunchArgv } from "./dispatcher.js";
|
|
2
|
+
import { firstString, warn } from "./state.js";
|
|
3
|
+
export function eventName(subcommand) {
|
|
4
|
+
switch (subcommand) {
|
|
5
|
+
case "session-start": return "SessionStart";
|
|
6
|
+
case "prompt-submit": return "UserPromptSubmit";
|
|
7
|
+
case "stop": return "Stop";
|
|
8
|
+
case "notification": return "Notification";
|
|
9
|
+
default: return subcommand;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const resolvedSurfaceTargets = new WeakMap();
|
|
13
|
+
function surfaceTargetsFor(dispatcher) {
|
|
14
|
+
let targets = resolvedSurfaceTargets.get(dispatcher);
|
|
15
|
+
if (!targets) {
|
|
16
|
+
targets = new Map();
|
|
17
|
+
resolvedSurfaceTargets.set(dispatcher, targets);
|
|
18
|
+
}
|
|
19
|
+
return targets;
|
|
20
|
+
}
|
|
21
|
+
export function surfaceTargetArgs(dispatcher, sessionId) {
|
|
22
|
+
const resolved = surfaceTargetsFor(dispatcher).get(sessionId);
|
|
23
|
+
if (resolved)
|
|
24
|
+
return [...resolved];
|
|
25
|
+
const surfaceId = firstString(process.env.CMUX_SURFACE_ID);
|
|
26
|
+
if (!surfaceId)
|
|
27
|
+
return null;
|
|
28
|
+
const args = [];
|
|
29
|
+
const workspaceId = firstString(process.env.CMUX_WORKSPACE_ID);
|
|
30
|
+
if (workspaceId)
|
|
31
|
+
args.push("--workspace", workspaceId);
|
|
32
|
+
args.push("--surface", surfaceId);
|
|
33
|
+
return args;
|
|
34
|
+
}
|
|
35
|
+
export function rememberSurfaceTarget(dispatcher, sessionId, result) {
|
|
36
|
+
const payload = parseJSONOutput(result);
|
|
37
|
+
const workspaceId = firstString(payload?.workspace_id);
|
|
38
|
+
const surfaceId = firstString(payload?.surface_id);
|
|
39
|
+
if (!workspaceId || !surfaceId)
|
|
40
|
+
return;
|
|
41
|
+
surfaceTargetsFor(dispatcher).set(sessionId, ["--workspace", workspaceId, "--surface", surfaceId]);
|
|
42
|
+
}
|
|
43
|
+
export function releaseSessionRuntime(dispatcher, sessionStates, sessionId) {
|
|
44
|
+
dispatcher.releaseSession(sessionId);
|
|
45
|
+
sessionStates.delete(sessionId);
|
|
46
|
+
surfaceTargetsFor(dispatcher).delete(sessionId);
|
|
47
|
+
}
|
|
48
|
+
function parseJSONOutput(result) {
|
|
49
|
+
if (!result.ok)
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(result.stdout);
|
|
53
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function sendHook(dispatcher, subcommand, context, extra = {}) {
|
|
60
|
+
if (process.env.CMUX_PI_HOOKS_DISABLED === "1")
|
|
61
|
+
return true;
|
|
62
|
+
const sessionId = context.sessionId;
|
|
63
|
+
if (!sessionId)
|
|
64
|
+
return true;
|
|
65
|
+
const target = surfaceTargetArgs(dispatcher, sessionId);
|
|
66
|
+
if (!target)
|
|
67
|
+
return !firstString(process.env.CMUX_PANEL_ID);
|
|
68
|
+
const cwd = context.cwd;
|
|
69
|
+
const payload = {
|
|
70
|
+
session_id: sessionId,
|
|
71
|
+
cwd,
|
|
72
|
+
hook_event_name: eventName(subcommand),
|
|
73
|
+
event: eventName(subcommand),
|
|
74
|
+
...extra,
|
|
75
|
+
};
|
|
76
|
+
const result = await dispatcher.run(["hooks", "pi", subcommand, ...target], cwd, JSON.stringify(payload), context);
|
|
77
|
+
if (result.ok)
|
|
78
|
+
rememberSurfaceTarget(dispatcher, sessionId, result);
|
|
79
|
+
if (!result.ok && !result.surfaceUnavailable) {
|
|
80
|
+
warn(context, "cmux hook command failed", {
|
|
81
|
+
subcommand,
|
|
82
|
+
status: result.status,
|
|
83
|
+
stderr_available: result.stderr.trim().length > 0,
|
|
84
|
+
error_available: result.error !== undefined,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return result.ok;
|
|
88
|
+
}
|
|
89
|
+
// --- Resume bindings ---
|
|
90
|
+
const piOptionsWithValue = new Set([
|
|
91
|
+
"--model", "-m", "--thinking", "--provider", "--extension", "-e",
|
|
92
|
+
"--skill", "--mcp-config", "--permission-mode", "--session-dir",
|
|
93
|
+
"--config", "--profile", "--system-prompt", "--append-system-prompt",
|
|
94
|
+
"--cwd", "--dir", "--trust", "--sandbox",
|
|
95
|
+
]);
|
|
96
|
+
const piOptionsWithoutValue = new Set([
|
|
97
|
+
"--no-color", "--dangerously-skip-permissions", "--yolo",
|
|
98
|
+
]);
|
|
99
|
+
const piSelectorsToDrop = new Set([
|
|
100
|
+
"--session", "-s", "--resume", "--fork", "--api-key", "--prompt", "--print",
|
|
101
|
+
]);
|
|
102
|
+
function sanitizedResumeArgv(sessionId) {
|
|
103
|
+
const raw = normalizedLaunchArgv();
|
|
104
|
+
const executable = raw[0] || "pi";
|
|
105
|
+
const out = [executable, "--session", sessionId];
|
|
106
|
+
for (let index = 1; index < raw.length; index += 1) {
|
|
107
|
+
const arg = raw[index];
|
|
108
|
+
if (!arg)
|
|
109
|
+
continue;
|
|
110
|
+
if (piSelectorsToDrop.has(arg)) {
|
|
111
|
+
if (index + 1 < raw.length && !raw[index + 1].startsWith("-"))
|
|
112
|
+
index += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (arg.startsWith("--session=") || arg.startsWith("--resume=") ||
|
|
116
|
+
arg.startsWith("--fork=") || arg.startsWith("--api-key=") ||
|
|
117
|
+
arg.startsWith("--prompt=")) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (piOptionsWithValue.has(arg)) {
|
|
121
|
+
out.push(arg);
|
|
122
|
+
if (index + 1 < raw.length) {
|
|
123
|
+
out.push(raw[index + 1]);
|
|
124
|
+
index += 1;
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if ([...piOptionsWithValue].some((option) => arg.startsWith(`${option}=`)) || piOptionsWithoutValue.has(arg)) {
|
|
129
|
+
out.push(arg);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function resumeBindingMatches(payload, sessionId) {
|
|
135
|
+
const binding = payload?.resume_binding;
|
|
136
|
+
if (!binding || typeof binding !== "object")
|
|
137
|
+
return false;
|
|
138
|
+
const typed = binding;
|
|
139
|
+
return firstString(typed.kind) === "pi" &&
|
|
140
|
+
firstString(typed.checkpoint_id, typed.checkpointId) === sessionId;
|
|
141
|
+
}
|
|
142
|
+
export async function ensureResumeBinding(dispatcher, context, sessionId) {
|
|
143
|
+
if (process.env.CMUX_PI_HOOKS_DISABLED === "1")
|
|
144
|
+
return;
|
|
145
|
+
const target = surfaceTargetArgs(dispatcher, sessionId);
|
|
146
|
+
if (!target)
|
|
147
|
+
return;
|
|
148
|
+
const cwd = context.cwd;
|
|
149
|
+
const resumeArgv = sanitizedResumeArgv(sessionId);
|
|
150
|
+
const set = await dispatcher.run([
|
|
151
|
+
"--json", "surface", "resume", "set", ...target,
|
|
152
|
+
"--name", "Pi", "--kind", "pi", "--checkpoint-id", sessionId,
|
|
153
|
+
"--source", "agent-hook", "--cwd", cwd, "--", ...resumeArgv,
|
|
154
|
+
], cwd, undefined, context);
|
|
155
|
+
if (!set.ok && !set.surfaceUnavailable) {
|
|
156
|
+
warn(context, "failed to set Pi resume binding", {
|
|
157
|
+
status: set.status,
|
|
158
|
+
stderr_available: set.stderr.trim().length > 0,
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (set.surfaceUnavailable)
|
|
163
|
+
return;
|
|
164
|
+
const verification = await dispatcher.run(["--json", "surface", "resume", "get", ...target], cwd, undefined, context);
|
|
165
|
+
if (verification.surfaceUnavailable)
|
|
166
|
+
return;
|
|
167
|
+
const verified = parseJSONOutput(verification);
|
|
168
|
+
if (!resumeBindingMatches(verified, sessionId)) {
|
|
169
|
+
warn(context, "Pi resume binding did not verify after write", { session_id: sessionId });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export async function clearResumeBinding(dispatcher, context, sessionId) {
|
|
173
|
+
if (process.env.CMUX_PI_HOOKS_DISABLED === "1")
|
|
174
|
+
return;
|
|
175
|
+
const target = surfaceTargetArgs(dispatcher, sessionId);
|
|
176
|
+
if (!target)
|
|
177
|
+
return;
|
|
178
|
+
const cwd = context.cwd;
|
|
179
|
+
const result = await dispatcher.run([
|
|
180
|
+
"--json", "surface", "resume", "clear", ...target,
|
|
181
|
+
"--checkpoint-id", sessionId, "--source", "agent-hook",
|
|
182
|
+
], cwd, undefined, context);
|
|
183
|
+
if (result.surfaceUnavailable)
|
|
184
|
+
return;
|
|
185
|
+
if (!result.ok) {
|
|
186
|
+
warn(context, "failed to clear Pi resume binding", {
|
|
187
|
+
status: result.status,
|
|
188
|
+
stderr_available: result.stderr.trim().length > 0,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CmuxDispatcher } from "./dispatcher.js";
|
|
4
|
+
import { stateFor, snapshotContext, beginTurn, currentTurnId, finishTurn, settleTurn, lastAssistantMessage, firstString, objectValue, warn, } from "./state.js";
|
|
5
|
+
import { sendHook, ensureResumeBinding, clearResumeBinding, releaseSessionRuntime, } from "./hooks.js";
|
|
6
|
+
import { renameWorkspaceFromPrompt } from "./naming.js";
|
|
7
|
+
/** Remove a stale cmux-installed extension so hooks don't double-fire. */
|
|
8
|
+
function removeStaleManagedExtension() {
|
|
9
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR || process.env.YAGNI_CODING_AGENT_DIR;
|
|
10
|
+
if (!agentDir)
|
|
11
|
+
return;
|
|
12
|
+
const managed = join(agentDir, "extensions", "cmux-session.ts");
|
|
13
|
+
try {
|
|
14
|
+
if (!existsSync(managed))
|
|
15
|
+
return;
|
|
16
|
+
const head = readFileSync(managed, "utf8").slice(0, 200);
|
|
17
|
+
if (head.includes("cmux-pi-session-extension-marker")) {
|
|
18
|
+
unlinkSync(managed);
|
|
19
|
+
try {
|
|
20
|
+
unlinkSync(join(agentDir, "extensions", ".cmux-session.lock"));
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
}
|
|
27
|
+
export function registerCmuxBridge(pi) {
|
|
28
|
+
if (!process.env.CMUX_SURFACE_ID)
|
|
29
|
+
return;
|
|
30
|
+
if (process.env.CMUX_PI_HOOKS_DISABLED === "1")
|
|
31
|
+
return;
|
|
32
|
+
const dispatcher = new CmuxDispatcher();
|
|
33
|
+
const sessionStates = new Map();
|
|
34
|
+
const lifecycleTails = new Map();
|
|
35
|
+
const enqueueLifecycleTask = (sessionId, context, operation) => {
|
|
36
|
+
const previous = lifecycleTails.get(sessionId) || Promise.resolve();
|
|
37
|
+
let tracked;
|
|
38
|
+
tracked = previous
|
|
39
|
+
.then(operation)
|
|
40
|
+
.then(() => undefined)
|
|
41
|
+
.catch((error) => {
|
|
42
|
+
const msg = error instanceof Error ? error.message : undefined;
|
|
43
|
+
warn(context, "cmux lifecycle task failed", { error_message: msg?.slice(0, 512) }, true);
|
|
44
|
+
})
|
|
45
|
+
.finally(() => {
|
|
46
|
+
if (lifecycleTails.get(sessionId) === tracked)
|
|
47
|
+
lifecycleTails.delete(sessionId);
|
|
48
|
+
});
|
|
49
|
+
lifecycleTails.set(sessionId, tracked);
|
|
50
|
+
return tracked;
|
|
51
|
+
};
|
|
52
|
+
pi.on("session_start", (_event, ctx) => {
|
|
53
|
+
removeStaleManagedExtension();
|
|
54
|
+
const context = snapshotContext(ctx);
|
|
55
|
+
const sessionId = context.sessionId;
|
|
56
|
+
if (!sessionId)
|
|
57
|
+
return;
|
|
58
|
+
const state = stateFor(sessionStates, sessionId);
|
|
59
|
+
state.pendingCompletion = undefined;
|
|
60
|
+
state.stopped = false;
|
|
61
|
+
enqueueLifecycleTask(sessionId, context, async () => {
|
|
62
|
+
const ok = await sendHook(dispatcher, "session-start", context);
|
|
63
|
+
if (ok)
|
|
64
|
+
await ensureResumeBinding(dispatcher, context, sessionId);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
68
|
+
const context = snapshotContext(ctx);
|
|
69
|
+
const sessionId = context.sessionId;
|
|
70
|
+
if (!sessionId)
|
|
71
|
+
return;
|
|
72
|
+
const turnId = beginTurn(sessionStates, sessionId, event);
|
|
73
|
+
const st = stateFor(sessionStates, sessionId);
|
|
74
|
+
if (typeof event.prompt === "string" && event.prompt.trim()) {
|
|
75
|
+
st.lastPrompt = event.prompt.trim();
|
|
76
|
+
}
|
|
77
|
+
enqueueLifecycleTask(sessionId, context, () => sendHook(dispatcher, "prompt-submit", context, { prompt: event.prompt, turn_id: turnId }));
|
|
78
|
+
});
|
|
79
|
+
pi.on("agent_end", (event, ctx) => {
|
|
80
|
+
const context = snapshotContext(ctx);
|
|
81
|
+
const sessionId = context.sessionId;
|
|
82
|
+
if (!sessionId)
|
|
83
|
+
return;
|
|
84
|
+
const state = stateFor(sessionStates, sessionId);
|
|
85
|
+
const message = lastAssistantMessage(event);
|
|
86
|
+
state.pendingCompletion = {
|
|
87
|
+
lastAssistantMessage: message || state.pendingCompletion?.lastAssistantMessage,
|
|
88
|
+
notificationType: firstString(objectValue(event, ["stopReason", "reason", "terminationReason"])) || "completed",
|
|
89
|
+
turnId: currentTurnId(sessionStates, sessionId, event),
|
|
90
|
+
};
|
|
91
|
+
// Pi >= 0.80.5 always emits agent_settled, so we wait for it there.
|
|
92
|
+
});
|
|
93
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
94
|
+
const context = snapshotContext(ctx);
|
|
95
|
+
try {
|
|
96
|
+
if (!ctx.isIdle())
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const sessionId = context.sessionId;
|
|
103
|
+
if (!sessionId)
|
|
104
|
+
return;
|
|
105
|
+
const completion = settleTurn(sessionStates, sessionId);
|
|
106
|
+
if (completion) {
|
|
107
|
+
enqueueLifecycleTask(sessionId, context, () => publishCompletion(dispatcher, sessionStates, context, sessionId, completion));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
111
|
+
const context = snapshotContext(ctx);
|
|
112
|
+
const sessionId = context.sessionId;
|
|
113
|
+
if (!sessionId)
|
|
114
|
+
return;
|
|
115
|
+
const state = stateFor(sessionStates, sessionId);
|
|
116
|
+
let stopPayload;
|
|
117
|
+
if (!state.stopped) {
|
|
118
|
+
const turnId = finishTurn(sessionStates, sessionId, event);
|
|
119
|
+
stopPayload = {
|
|
120
|
+
turn_id: turnId,
|
|
121
|
+
terminationReason: firstString(objectValue(event, ["reason"])) || "session_shutdown",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
await enqueueLifecycleTask(sessionId, context, async () => {
|
|
125
|
+
if (stopPayload)
|
|
126
|
+
await sendHook(dispatcher, "stop", context, stopPayload);
|
|
127
|
+
try {
|
|
128
|
+
await clearResumeBinding(dispatcher, context, sessionId);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
releaseSessionRuntime(dispatcher, sessionStates, sessionId);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function publishCompletion(dispatcher, sessionStates, context, sessionId, completion) {
|
|
137
|
+
const stopPayload = {
|
|
138
|
+
last_assistant_message: completion.lastAssistantMessage,
|
|
139
|
+
turn_id: completion.turnId,
|
|
140
|
+
};
|
|
141
|
+
const notificationRouted = await sendHook(dispatcher, "notification", context, {
|
|
142
|
+
message: completion.lastAssistantMessage || "Task completed",
|
|
143
|
+
turn_id: completion.turnId,
|
|
144
|
+
notification: { type: completion.notificationType },
|
|
145
|
+
});
|
|
146
|
+
if (notificationRouted)
|
|
147
|
+
stopPayload.cmux_notification_routed = true;
|
|
148
|
+
await sendHook(dispatcher, "stop", context, stopPayload);
|
|
149
|
+
const state = stateFor(sessionStates, sessionId);
|
|
150
|
+
if (!state.hasNamed) {
|
|
151
|
+
renameWorkspaceFromPrompt(dispatcher, sessionStates, context, sessionId);
|
|
152
|
+
state.hasNamed = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CmuxDispatcher } from "./dispatcher.js";
|
|
2
|
+
import { type PiExtensionContextSnapshot, type SessionState } from "./state.js";
|
|
3
|
+
export declare function titleFromPrompt(prompt: string): string | undefined;
|
|
4
|
+
export declare function renameWorkspaceFromPrompt(dispatcher: CmuxDispatcher, sessionStates: Map<string, SessionState>, context: PiExtensionContextSnapshot, sessionId: string): void;
|
|
5
|
+
//# sourceMappingURL=naming.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { firstString } from "./state.js";
|
|
2
|
+
export function titleFromPrompt(prompt) {
|
|
3
|
+
const words = prompt.trim().split(/\s+/).filter(Boolean);
|
|
4
|
+
if (words.length === 0)
|
|
5
|
+
return undefined;
|
|
6
|
+
return words.slice(0, 8).join(" ").slice(0, 60) || undefined;
|
|
7
|
+
}
|
|
8
|
+
export function renameWorkspaceFromPrompt(dispatcher, sessionStates, context, sessionId) {
|
|
9
|
+
const prompt = sessionStates.get(sessionId)?.lastPrompt;
|
|
10
|
+
if (!prompt)
|
|
11
|
+
return;
|
|
12
|
+
const title = titleFromPrompt(prompt);
|
|
13
|
+
if (!title)
|
|
14
|
+
return;
|
|
15
|
+
// Use the workspace-scoped rename form: cmux workspace rename <ws> --title <title>
|
|
16
|
+
// (NOT rename-workspace --workspace ... --surface ... which misinterprets --surface
|
|
17
|
+
// as part of the title).
|
|
18
|
+
const workspaceId = firstString(process.env.CMUX_WORKSPACE_ID);
|
|
19
|
+
if (!workspaceId)
|
|
20
|
+
return;
|
|
21
|
+
void dispatcher.run(["workspace", "rename", workspaceId, "--title", title], context.cwd, undefined, context);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=naming.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export interface PendingCompletion {
|
|
3
|
+
lastAssistantMessage?: string;
|
|
4
|
+
notificationType: string;
|
|
5
|
+
turnId: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SessionState {
|
|
8
|
+
nextTurn: number;
|
|
9
|
+
activeTurnId?: string;
|
|
10
|
+
pendingCompletion?: PendingCompletion;
|
|
11
|
+
stopped: boolean;
|
|
12
|
+
lastPrompt?: string;
|
|
13
|
+
hasNamed?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface PiExtensionContextSnapshot {
|
|
16
|
+
readonly sessionId: string | null;
|
|
17
|
+
readonly cwd: string;
|
|
18
|
+
readonly notifyWarning?: () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function firstString(...values: unknown[]): string | null;
|
|
21
|
+
export declare function objectValue(value: unknown, keys: string[]): unknown;
|
|
22
|
+
export declare function textFromContent(content: unknown): string | null;
|
|
23
|
+
export declare function lastAssistantMessage(event: unknown): string | undefined;
|
|
24
|
+
export declare function sessionIdFrom(ctx: ExtensionContext): string | null;
|
|
25
|
+
export declare function cwdFrom(ctx: ExtensionContext): string;
|
|
26
|
+
export declare function snapshotContext(ctx: ExtensionContext): PiExtensionContextSnapshot;
|
|
27
|
+
export declare function stateFor(sessionStates: Map<string, SessionState>, sessionId: string): SessionState;
|
|
28
|
+
export declare function beginTurn(sessionStates: Map<string, SessionState>, sessionId: string, event: unknown): string;
|
|
29
|
+
export declare function currentTurnId(sessionStates: Map<string, SessionState>, sessionId: string, event: unknown): string;
|
|
30
|
+
export declare function finishTurn(sessionStates: Map<string, SessionState>, sessionId: string, event: unknown): string;
|
|
31
|
+
export declare function settleTurn(sessionStates: Map<string, SessionState>, sessionId: string): PendingCompletion | undefined;
|
|
32
|
+
export declare function warn(ctx: PiExtensionContextSnapshot | null, message: string, details?: Record<string, unknown>, notifyUser?: boolean): void;
|
|
33
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export function firstString(...values) {
|
|
2
|
+
for (const value of values) {
|
|
3
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
4
|
+
return value.trim();
|
|
5
|
+
}
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
export function objectValue(value, keys) {
|
|
9
|
+
if (!value || typeof value !== "object")
|
|
10
|
+
return undefined;
|
|
11
|
+
const typed = value;
|
|
12
|
+
for (const key of keys) {
|
|
13
|
+
if (typed[key] !== undefined && typed[key] !== null)
|
|
14
|
+
return typed[key];
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
export function textFromContent(content) {
|
|
19
|
+
if (typeof content === "string")
|
|
20
|
+
return content;
|
|
21
|
+
if (!Array.isArray(content))
|
|
22
|
+
return null;
|
|
23
|
+
const parts = [];
|
|
24
|
+
for (const block of content) {
|
|
25
|
+
if (!block || typeof block !== "object")
|
|
26
|
+
continue;
|
|
27
|
+
const typed = block;
|
|
28
|
+
if (typed.type === "text" && typeof typed.text === "string")
|
|
29
|
+
parts.push(typed.text);
|
|
30
|
+
}
|
|
31
|
+
return parts.join("\n") || null;
|
|
32
|
+
}
|
|
33
|
+
export function lastAssistantMessage(event) {
|
|
34
|
+
const messagesValue = objectValue(event, ["messages"]);
|
|
35
|
+
const messages = Array.isArray(messagesValue) ? messagesValue : [];
|
|
36
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
37
|
+
const message = messages[index];
|
|
38
|
+
if (!message || typeof message !== "object")
|
|
39
|
+
continue;
|
|
40
|
+
const typed = message;
|
|
41
|
+
if (typed.role !== "assistant")
|
|
42
|
+
continue;
|
|
43
|
+
const text = firstString(textFromContent(typed.content));
|
|
44
|
+
if (text)
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
export function sessionIdFrom(ctx) {
|
|
50
|
+
try {
|
|
51
|
+
return firstString(ctx.sessionManager?.getSessionId?.());
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function cwdFrom(ctx) {
|
|
58
|
+
try {
|
|
59
|
+
return firstString(ctx.cwd, process.cwd()) || process.cwd();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return process.cwd();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function snapshotContext(ctx) {
|
|
66
|
+
if (!ctx || typeof ctx !== "object") {
|
|
67
|
+
return { sessionId: null, cwd: process.cwd() };
|
|
68
|
+
}
|
|
69
|
+
let notifyWarning;
|
|
70
|
+
try {
|
|
71
|
+
const ui = ctx.ui;
|
|
72
|
+
if (typeof ui?.notify === "function") {
|
|
73
|
+
notifyWarning = () => ui.notify?.("cmux integration warning — check the terminal for details", "warning");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
return { sessionId: sessionIdFrom(ctx), cwd: cwdFrom(ctx), notifyWarning };
|
|
78
|
+
}
|
|
79
|
+
export function stateFor(sessionStates, sessionId) {
|
|
80
|
+
let state = sessionStates.get(sessionId);
|
|
81
|
+
if (!state) {
|
|
82
|
+
state = { nextTurn: 0, stopped: false };
|
|
83
|
+
sessionStates.set(sessionId, state);
|
|
84
|
+
}
|
|
85
|
+
return state;
|
|
86
|
+
}
|
|
87
|
+
function eventTurnId(event) {
|
|
88
|
+
return firstString(objectValue(event, ["turn_id", "turnId", "turnID"]));
|
|
89
|
+
}
|
|
90
|
+
export function beginTurn(sessionStates, sessionId, event) {
|
|
91
|
+
const state = stateFor(sessionStates, sessionId);
|
|
92
|
+
const turnId = eventTurnId(event) || `${sessionId}:turn-${state.nextTurn + 1}`;
|
|
93
|
+
if (!eventTurnId(event))
|
|
94
|
+
state.nextTurn += 1;
|
|
95
|
+
state.activeTurnId = turnId;
|
|
96
|
+
state.pendingCompletion = undefined;
|
|
97
|
+
state.stopped = false;
|
|
98
|
+
return turnId;
|
|
99
|
+
}
|
|
100
|
+
export function currentTurnId(sessionStates, sessionId, event) {
|
|
101
|
+
const state = stateFor(sessionStates, sessionId);
|
|
102
|
+
const turnId = eventTurnId(event) || state.activeTurnId || `${sessionId}:turn-${state.nextTurn + 1}`;
|
|
103
|
+
if (!eventTurnId(event) && !state.activeTurnId)
|
|
104
|
+
state.nextTurn += 1;
|
|
105
|
+
return turnId;
|
|
106
|
+
}
|
|
107
|
+
export function finishTurn(sessionStates, sessionId, event) {
|
|
108
|
+
const state = stateFor(sessionStates, sessionId);
|
|
109
|
+
const turnId = eventTurnId(event) || state.activeTurnId || `${sessionId}:turn-${state.nextTurn + 1}`;
|
|
110
|
+
if (!eventTurnId(event) && !state.activeTurnId)
|
|
111
|
+
state.nextTurn += 1;
|
|
112
|
+
state.activeTurnId = undefined;
|
|
113
|
+
state.pendingCompletion = undefined;
|
|
114
|
+
state.stopped = true;
|
|
115
|
+
return turnId;
|
|
116
|
+
}
|
|
117
|
+
export function settleTurn(sessionStates, sessionId) {
|
|
118
|
+
const state = sessionStates.get(sessionId);
|
|
119
|
+
const completion = state?.pendingCompletion;
|
|
120
|
+
if (!state || !completion || state.stopped)
|
|
121
|
+
return undefined;
|
|
122
|
+
state.activeTurnId = undefined;
|
|
123
|
+
state.pendingCompletion = undefined;
|
|
124
|
+
state.stopped = true;
|
|
125
|
+
return completion;
|
|
126
|
+
}
|
|
127
|
+
export function warn(ctx, message, details = {}, notifyUser = false) {
|
|
128
|
+
const payload = { source: "yagni-cmux-bridge", level: "warning", message, ...details };
|
|
129
|
+
try {
|
|
130
|
+
console.warn(JSON.stringify(payload));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
console.warn(`[yagni-cmux-bridge] ${message}`);
|
|
134
|
+
}
|
|
135
|
+
if (notifyUser) {
|
|
136
|
+
try {
|
|
137
|
+
ctx?.notifyWarning?.();
|
|
138
|
+
}
|
|
139
|
+
catch { }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=state.js.map
|
package/dist/extension/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from
|
|
|
5
5
|
import { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
6
6
|
import { makeAskYagniTool } from "./askYagniTool.js";
|
|
7
7
|
import { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
8
|
+
import { registerCmuxBridge } from "./cmux/index.js";
|
|
8
9
|
import { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
9
10
|
import { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
10
11
|
import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
|
|
@@ -14,7 +15,7 @@ import { registerCostCommand } from "./costHud.js";
|
|
|
14
15
|
import { isDebug } from "./diagnostics.js";
|
|
15
16
|
import { droppedSessionRuns, sessionRunIds } from "./sessionRuns.js";
|
|
16
17
|
import { codeStateHome } from "./stateHome.js";
|
|
17
|
-
import { createYagniFooterFactory } from "./footer.js";
|
|
18
|
+
import { createYagniFooterFactory, formatCwd } from "./footer.js";
|
|
18
19
|
import { RerouteNotifier } from "./rerouteNotice.js";
|
|
19
20
|
import { isFreshWorkspace, registerTeamSetupCommand, runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
20
21
|
import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone } from "./initDone.js";
|
|
@@ -481,7 +482,11 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
481
482
|
ctx.ui?.setStatus?.("brand", BRAND_NAME);
|
|
482
483
|
// setHeader replaces the built-in header in place (verified against pi
|
|
483
484
|
// 0.80.2 setExtensionHeader); the factory returns a simple Text component.
|
|
484
|
-
|
|
485
|
+
// Pass the launcher-forwarded CLI version (YAGNI_CODE_VERSION) and the
|
|
486
|
+
// home-collapsed cwd so the masthead shows them like the reference.
|
|
487
|
+
const mastheadVersion = process.env.YAGNI_CODE_VERSION?.trim() || undefined;
|
|
488
|
+
const mastheadCwd = formatCwd(process.cwd(), process.env.HOME);
|
|
489
|
+
ctx.ui?.setHeader?.((_tui, theme) => new Text(buildMastheadString(theme, { version: mastheadVersion, cwd: mastheadCwd })));
|
|
485
490
|
// Replace the built-in footer with the YAGNI status bar: folder +
|
|
486
491
|
// [worktree] + branch on line 1, model + token/cost stats + integer
|
|
487
492
|
// context % on line 2, and extension statuses (brand, todos, mode) on
|
|
@@ -543,6 +548,12 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
543
548
|
}
|
|
544
549
|
export default async function (pi) {
|
|
545
550
|
await registerYagni(pi);
|
|
551
|
+
// cmux session bridge: report lifecycle state (running/idle), workspace
|
|
552
|
+
// naming, and resume bindings to cmux when running inside it. Complete
|
|
553
|
+
// no-op outside cmux — guarded by CMUX_SURFACE_ID. Registered in the
|
|
554
|
+
// default export (not registerYagni) so tests that call registerYagni
|
|
555
|
+
// directly don't get the bridge's handlers.
|
|
556
|
+
registerCmuxBridge(pi);
|
|
546
557
|
}
|
|
547
558
|
export { makeAskYagniTool } from "./askYagniTool.js";
|
|
548
559
|
export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "0.2.1-staging.
|
|
3
|
+
"version": "0.2.1-staging.1034.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"@earendil-works/pi-tui": "0.83.0",
|
|
39
39
|
"typebox": "^1.1.38"
|
|
40
40
|
},
|
|
41
|
-
"yagniSourceSha": "
|
|
41
|
+
"yagniSourceSha": "18b902bf72fff144de95d7949d32025875798a1e"
|
|
42
42
|
}
|