@yagni-app/code 0.2.1 → 0.3.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.
Files changed (98) hide show
  1. package/dist/cli.d.ts +30 -0
  2. package/dist/cli.js +147 -3
  3. package/dist/connectClaudeCode.d.ts +77 -0
  4. package/dist/connectClaudeCode.js +228 -0
  5. package/dist/connectCodex.d.ts +75 -0
  6. package/dist/connectCodex.js +201 -0
  7. package/dist/doctor.d.ts +1 -1
  8. package/dist/doctor.js +1 -1
  9. package/dist/extension/advisor.d.ts +4 -4
  10. package/dist/extension/advisor.js +6 -7
  11. package/dist/extension/approvedPrefixes.d.ts +103 -0
  12. package/dist/extension/approvedPrefixes.js +282 -0
  13. package/dist/extension/askAdvisorTool.d.ts +20 -5
  14. package/dist/extension/askAdvisorTool.js +126 -20
  15. package/dist/extension/askYagniTool.d.ts +23 -0
  16. package/dist/extension/askYagniTool.js +91 -2
  17. package/dist/extension/branding.d.ts +34 -3
  18. package/dist/extension/branding.js +114 -13
  19. package/dist/extension/chipEditor.d.ts +30 -9
  20. package/dist/extension/chipEditor.js +173 -59
  21. package/dist/extension/claudeRules.d.ts +0 -2
  22. package/dist/extension/claudeRules.js +0 -8
  23. package/dist/extension/cmux/dispatcher.d.ts +25 -0
  24. package/dist/extension/cmux/dispatcher.js +266 -0
  25. package/dist/extension/cmux/hooks.d.ts +12 -0
  26. package/dist/extension/cmux/hooks.js +192 -0
  27. package/dist/extension/cmux/index.d.ts +3 -0
  28. package/dist/extension/cmux/index.js +155 -0
  29. package/dist/extension/cmux/naming.d.ts +5 -0
  30. package/dist/extension/cmux/naming.js +23 -0
  31. package/dist/extension/cmux/state.d.ts +33 -0
  32. package/dist/extension/cmux/state.js +142 -0
  33. package/dist/extension/config.d.ts +44 -1
  34. package/dist/extension/config.js +37 -4
  35. package/dist/extension/costHud.d.ts +16 -22
  36. package/dist/extension/costHud.js +8 -47
  37. package/dist/extension/crashReport.js +1 -3
  38. package/dist/extension/execPolicy.d.ts +135 -0
  39. package/dist/extension/execPolicy.js +936 -0
  40. package/dist/extension/flywheel.d.ts +44 -0
  41. package/dist/extension/flywheel.js +53 -0
  42. package/dist/extension/footer.d.ts +118 -0
  43. package/dist/extension/footer.js +308 -0
  44. package/dist/extension/guardian.d.ts +139 -0
  45. package/dist/extension/guardian.js +237 -0
  46. package/dist/extension/index.d.ts +35 -7
  47. package/dist/extension/index.js +338 -33
  48. package/dist/extension/mineBeat.d.ts +95 -0
  49. package/dist/extension/mineBeat.js +193 -0
  50. package/dist/extension/permission.d.ts +124 -10
  51. package/dist/extension/permission.js +603 -52
  52. package/dist/extension/pipeline/childRegistry.d.ts +41 -0
  53. package/dist/extension/pipeline/childRegistry.js +118 -0
  54. package/dist/extension/pipeline/finish.js +5 -1
  55. package/dist/extension/pipeline/goCommand.d.ts +1 -1
  56. package/dist/extension/pipeline/goCommand.js +40 -9
  57. package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
  58. package/dist/extension/pipeline/goStatusCommands.js +61 -1
  59. package/dist/extension/pipeline/personas.js +26 -1
  60. package/dist/extension/pipeline/resilience.d.ts +2 -1
  61. package/dist/extension/pipeline/resilience.js +21 -2
  62. package/dist/extension/pipeline/runRegistry.d.ts +23 -1
  63. package/dist/extension/pipeline/runRegistry.js +57 -1
  64. package/dist/extension/pipeline/runner.js +4 -0
  65. package/dist/extension/pipeline/verify.d.ts +4 -0
  66. package/dist/extension/pipeline/verify.js +48 -26
  67. package/dist/extension/recordDecisionTool.d.ts +8 -0
  68. package/dist/extension/recordDecisionTool.js +24 -0
  69. package/dist/extension/redact.d.ts +20 -0
  70. package/dist/extension/redact.js +64 -0
  71. package/dist/extension/rerouteNotice.d.ts +3 -4
  72. package/dist/extension/rerouteNotice.js +20 -11
  73. package/dist/extension/subagentRender.d.ts +129 -0
  74. package/dist/extension/subagentRender.js +441 -0
  75. package/dist/extension/subagents.d.ts +11 -8
  76. package/dist/extension/subagents.js +162 -37
  77. package/dist/extension/ticketTools.d.ts +37 -0
  78. package/dist/extension/ticketTools.js +117 -0
  79. package/dist/extension/todos.d.ts +28 -1
  80. package/dist/extension/todos.js +76 -1
  81. package/dist/extension/tokenProvider.js +46 -5
  82. package/dist/extension/ultra.d.ts +27 -0
  83. package/dist/extension/ultra.js +76 -0
  84. package/dist/launch.d.ts +7 -0
  85. package/dist/launch.js +24 -12
  86. package/dist/login.d.ts +4 -2
  87. package/dist/login.js +19 -4
  88. package/dist/padding.d.ts +22 -0
  89. package/dist/padding.js +25 -0
  90. package/dist/promptEnrichment.d.ts +40 -0
  91. package/dist/promptEnrichment.js +85 -0
  92. package/dist/signalForward.d.ts +60 -0
  93. package/dist/signalForward.js +130 -0
  94. package/dist/token.d.ts +25 -0
  95. package/dist/token.js +45 -0
  96. package/package.json +6 -5
  97. package/dist/extension/boostCommand.d.ts +0 -144
  98. package/dist/extension/boostCommand.js +0 -263
@@ -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,3 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export declare function registerCmuxBridge(pi: ExtensionAPI): void;
3
+ //# sourceMappingURL=index.d.ts.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