codeam-cli 2.59.0 → 2.60.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/CHANGELOG.md +46 -0
- package/dist/index.js +777 -112
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,52 @@ All notable changes to `codeam-cli` are documented here.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [2.60.0] — 2026-07-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **cli:** Baton local-session gate + feature flag
|
|
12
|
+
- **cli:** BatonController turn-safe single-driver state machine
|
|
13
|
+
- **cli:** TranscriptMirror emits JSONL deltas for the read-only view
|
|
14
|
+
- **cli:** NativeTuiDriver over AgentService with idle turn-boundary
|
|
15
|
+
- **cli:** AcpDriver over AcpClient (spawn + loadSession resume)
|
|
16
|
+
- **cli:** PostBatonEvent publisher driver-state
|
|
17
|
+
- **cli:** Take_control/handback relay handlers
|
|
18
|
+
- **cli:** Wire local baton branch before the ACP fork (flag-gated)
|
|
19
|
+
- **cli:** Make the session baton drivable after a hand-off
|
|
20
|
+
- **cli:** Enable the session baton by default for local sessions (CODEAM_BATON=0 kill switch)
|
|
21
|
+
- **shared:** Add baton_state to USER_EVENTS (session baton wire type)
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **cli:** Extract shared ACP command-context assembler + proxy-relaunch helper
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **cli:** Mark SessionDriver.kind readonly (contract)
|
|
30
|
+
- **cli:** Baton switch-failure recovery + ack-on-failure; test cleanups
|
|
31
|
+
- **cli:** EncodeCwd underscore-collapse + ClaudeRuntimeStrategy.resolveHistoryFile (baton mirror)
|
|
32
|
+
- **cli:** AcpClient.start cleans up on handshake failure (baton take-control recovery)
|
|
33
|
+
|
|
34
|
+
### Tests
|
|
35
|
+
|
|
36
|
+
- **cli:** Prove noteOutput resets the idle window + cover throw/undefined branches
|
|
37
|
+
- **cli:** Gated cross-mode-resume integration test (real claude)
|
|
38
|
+
|
|
39
|
+
## [2.59.0] — 2026-07-08
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **vsc-plugin:** Route stop_task through AgentStrategyRegistry, alias escape_key
|
|
44
|
+
- **vsc-plugin:** CopilotLmStrategy.stop() cancels the active LM stream
|
|
45
|
+
- **jetbrains-plugin:** Alias escape_key to stop_task
|
|
46
|
+
- **jetbrains-plugin:** Best-effort surface-interrupt helper for GUI agents
|
|
47
|
+
- **jetbrains-plugin:** Concrete strategies interrupt their surface on stop()
|
|
48
|
+
|
|
49
|
+
### Tests
|
|
50
|
+
|
|
51
|
+
- **cli:** Regression-guard stopTaskH cancels ACP turn and acks once
|
|
52
|
+
|
|
7
53
|
## [2.58.1] — 2026-07-08
|
|
8
54
|
|
|
9
55
|
### CI
|
package/dist/index.js
CHANGED
|
@@ -559,7 +559,8 @@ var USER_EVENTS = {
|
|
|
559
559
|
AGENT_INSTALL_PROGRESS: "agent_install_progress",
|
|
560
560
|
AGENT_INSTALL_FAILED: "agent_install_failed",
|
|
561
561
|
CLI_UPDATE_PROGRESS: "cli_update_progress",
|
|
562
|
-
CLI_UPDATE_FAILED: "cli_update_failed"
|
|
562
|
+
CLI_UPDATE_FAILED: "cli_update_failed",
|
|
563
|
+
BATON_STATE: "baton_state"
|
|
563
564
|
};
|
|
564
565
|
|
|
565
566
|
// ../../packages/shared/src/preview-prompts.ts
|
|
@@ -5652,7 +5653,7 @@ function readAnonId() {
|
|
|
5652
5653
|
}
|
|
5653
5654
|
function superProperties() {
|
|
5654
5655
|
return {
|
|
5655
|
-
cliVersion: true ? "2.
|
|
5656
|
+
cliVersion: true ? "2.60.1" : "0.0.0-dev",
|
|
5656
5657
|
nodeVersion: process.version,
|
|
5657
5658
|
platform: process.platform,
|
|
5658
5659
|
arch: process.arch,
|
|
@@ -5833,7 +5834,7 @@ var os4 = __toESM(require("os"));
|
|
|
5833
5834
|
// package.json
|
|
5834
5835
|
var package_default = {
|
|
5835
5836
|
name: "codeam-cli",
|
|
5836
|
-
version: "2.
|
|
5837
|
+
version: "2.60.1",
|
|
5837
5838
|
description: "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device \u2014 async. The terminal companion for CodeAgent Mobile.",
|
|
5838
5839
|
type: "commonjs",
|
|
5839
5840
|
main: "dist/index.js",
|
|
@@ -6114,6 +6115,29 @@ async function postPreviewEvent(input) {
|
|
|
6114
6115
|
};
|
|
6115
6116
|
}
|
|
6116
6117
|
}
|
|
6118
|
+
async function postBatonEvent(input) {
|
|
6119
|
+
try {
|
|
6120
|
+
await _transport.postJsonAuthed(
|
|
6121
|
+
`${API_BASE}/api/baton/events`,
|
|
6122
|
+
{
|
|
6123
|
+
sessionId: input.sessionId,
|
|
6124
|
+
pluginId: input.pluginId,
|
|
6125
|
+
state: input.state,
|
|
6126
|
+
driver: input.driver,
|
|
6127
|
+
conversationId: input.conversationId
|
|
6128
|
+
},
|
|
6129
|
+
input.pluginAuthToken
|
|
6130
|
+
);
|
|
6131
|
+
return { ok: true };
|
|
6132
|
+
} catch (err) {
|
|
6133
|
+
const e = err;
|
|
6134
|
+
return {
|
|
6135
|
+
ok: false,
|
|
6136
|
+
status: typeof e.statusCode === "number" ? e.statusCode : 0,
|
|
6137
|
+
message: e.message || "unknown"
|
|
6138
|
+
};
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6117
6141
|
async function postHeadroomEvent(input) {
|
|
6118
6142
|
try {
|
|
6119
6143
|
await _transport.postJsonAuthed(
|
|
@@ -6881,7 +6905,7 @@ var CommandRelayService = class {
|
|
|
6881
6905
|
// fresh + clear the "CLI update available" banner after a self-update
|
|
6882
6906
|
// (a codespace that reinstalls @latest reconnects via heartbeat, not
|
|
6883
6907
|
// pair/reconnect). Older backends ignore the extra field.
|
|
6884
|
-
..."2.
|
|
6908
|
+
..."2.60.1" ? { ideVersion: "2.60.1" } : {}
|
|
6885
6909
|
}).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
|
|
6886
6910
|
}
|
|
6887
6911
|
/**
|
|
@@ -12097,7 +12121,7 @@ var fs16 = __toESM(require("fs"));
|
|
|
12097
12121
|
var path19 = __toESM(require("path"));
|
|
12098
12122
|
var os14 = __toESM(require("os"));
|
|
12099
12123
|
function encodeCwd(cwd) {
|
|
12100
|
-
return cwd.replace(/[\\/:]/g, "-");
|
|
12124
|
+
return cwd.replace(/[\\/:_]/g, "-");
|
|
12101
12125
|
}
|
|
12102
12126
|
function resolveHistoryDir(cwd, projectsRoot) {
|
|
12103
12127
|
const root = projectsRoot ?? path19.join(os14.homedir(), ".claude", "projects");
|
|
@@ -12117,6 +12141,12 @@ function resolveHistoryDir(cwd, projectsRoot) {
|
|
|
12117
12141
|
}
|
|
12118
12142
|
return null;
|
|
12119
12143
|
}
|
|
12144
|
+
function resolveHistoryFile(cwd, sessionId, projectsRoot) {
|
|
12145
|
+
const dir = resolveHistoryDir(cwd, projectsRoot);
|
|
12146
|
+
if (!dir) return null;
|
|
12147
|
+
const filePath = path19.join(dir, `${sessionId}.jsonl`);
|
|
12148
|
+
return fs16.existsSync(filePath) ? filePath : null;
|
|
12149
|
+
}
|
|
12120
12150
|
function getCurrentUsage(historyDir, bootTimeMs = 0) {
|
|
12121
12151
|
const GRACE_MS = 5e3;
|
|
12122
12152
|
const cutoff = bootTimeMs > 0 ? bootTimeMs - GRACE_MS : 0;
|
|
@@ -12350,6 +12380,9 @@ var ClaudeRuntimeStrategy = class {
|
|
|
12350
12380
|
resolveHistoryDir(cwd) {
|
|
12351
12381
|
return resolveHistoryDir(cwd);
|
|
12352
12382
|
}
|
|
12383
|
+
resolveHistoryFile(cwd, sessionId) {
|
|
12384
|
+
return resolveHistoryFile(cwd, sessionId);
|
|
12385
|
+
}
|
|
12353
12386
|
parseHistoryFile(filePath) {
|
|
12354
12387
|
return parseHistoryFile(filePath);
|
|
12355
12388
|
}
|
|
@@ -12794,7 +12827,7 @@ function listResumableSessions2(cwd, homeOverride) {
|
|
|
12794
12827
|
out2.sort((a, b) => b.timestamp - a.timestamp);
|
|
12795
12828
|
return out2;
|
|
12796
12829
|
}
|
|
12797
|
-
function
|
|
12830
|
+
function resolveHistoryFile2(cwd, sessionId, homeOverride) {
|
|
12798
12831
|
const home = homeOverride ?? import_node_os.default.homedir();
|
|
12799
12832
|
const sessionsRoot = import_node_path2.default.join(home, ".codex", "sessions");
|
|
12800
12833
|
if (!import_node_fs3.default.existsSync(sessionsRoot)) return null;
|
|
@@ -13017,7 +13050,7 @@ var CodexRuntimeStrategy = class {
|
|
|
13017
13050
|
return parseHistoryFile2(filePath);
|
|
13018
13051
|
}
|
|
13019
13052
|
resolveHistoryFile(cwd, sessionId) {
|
|
13020
|
-
return
|
|
13053
|
+
return resolveHistoryFile2(cwd, sessionId);
|
|
13021
13054
|
}
|
|
13022
13055
|
getCurrentUsage(historyDir) {
|
|
13023
13056
|
return getCurrentUsage2(historyDir);
|
|
@@ -15888,7 +15921,7 @@ async function autoUpgradeBeforeCriticalCommand() {
|
|
|
15888
15921
|
if (process.env.NODE_ENV === "test") return;
|
|
15889
15922
|
if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
|
|
15890
15923
|
if (process.env.CI) return;
|
|
15891
|
-
const current = true ? "2.
|
|
15924
|
+
const current = true ? "2.60.1" : null;
|
|
15892
15925
|
if (!current) return;
|
|
15893
15926
|
const cache = readCache();
|
|
15894
15927
|
const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
|
|
@@ -15905,7 +15938,7 @@ function checkForUpdates() {
|
|
|
15905
15938
|
if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
|
|
15906
15939
|
if (process.env.CI) return;
|
|
15907
15940
|
if (!process.stdout.isTTY) return;
|
|
15908
|
-
const current = true ? "2.
|
|
15941
|
+
const current = true ? "2.60.1" : null;
|
|
15909
15942
|
if (!current) return;
|
|
15910
15943
|
const cache = readCache();
|
|
15911
15944
|
const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
|
|
@@ -15925,7 +15958,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
|
15925
15958
|
var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
|
|
15926
15959
|
var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
|
|
15927
15960
|
function currentCliVersion() {
|
|
15928
|
-
return true ? "2.
|
|
15961
|
+
return true ? "2.60.1" : null;
|
|
15929
15962
|
}
|
|
15930
15963
|
function runCmd(cmd, args2, timeoutMs) {
|
|
15931
15964
|
return new Promise((resolve7) => {
|
|
@@ -19663,6 +19696,32 @@ var envWriteH = async (ctx, cmd, parsed) => {
|
|
|
19663
19696
|
await ctx.relay.sendResult(cmd.id, "failed", { error: err.message });
|
|
19664
19697
|
}
|
|
19665
19698
|
};
|
|
19699
|
+
var takeControlH = async (ctx, cmd) => {
|
|
19700
|
+
if (!ctx.baton) {
|
|
19701
|
+
await ctx.relay.sendResult(cmd.id, "failed", { code: "NO_BATON" });
|
|
19702
|
+
return;
|
|
19703
|
+
}
|
|
19704
|
+
try {
|
|
19705
|
+
await ctx.baton.takeControl();
|
|
19706
|
+
} catch {
|
|
19707
|
+
await ctx.relay.sendResult(cmd.id, "failed", { code: "BATON_SWITCH_FAILED" });
|
|
19708
|
+
return;
|
|
19709
|
+
}
|
|
19710
|
+
await ctx.relay.sendResult(cmd.id, "completed", { state: ctx.baton.state });
|
|
19711
|
+
};
|
|
19712
|
+
var handbackH = async (ctx, cmd) => {
|
|
19713
|
+
if (!ctx.baton) {
|
|
19714
|
+
await ctx.relay.sendResult(cmd.id, "failed", { code: "NO_BATON" });
|
|
19715
|
+
return;
|
|
19716
|
+
}
|
|
19717
|
+
try {
|
|
19718
|
+
await ctx.baton.handback();
|
|
19719
|
+
} catch {
|
|
19720
|
+
await ctx.relay.sendResult(cmd.id, "failed", { code: "BATON_SWITCH_FAILED" });
|
|
19721
|
+
return;
|
|
19722
|
+
}
|
|
19723
|
+
await ctx.relay.sendResult(cmd.id, "completed", { state: ctx.baton.state });
|
|
19724
|
+
};
|
|
19666
19725
|
var _activeReporter = null;
|
|
19667
19726
|
var _headroomEmitChain = Promise.resolve();
|
|
19668
19727
|
var headroomConfigureH = async (ctx, cmd, parsed) => {
|
|
@@ -20503,6 +20562,8 @@ var handlers = {
|
|
|
20503
20562
|
save_preview_config: savePreviewConfigH,
|
|
20504
20563
|
env_read: envReadH,
|
|
20505
20564
|
env_write: envWriteH,
|
|
20565
|
+
take_control: takeControlH,
|
|
20566
|
+
handback: handbackH,
|
|
20506
20567
|
headroom_configure: headroomConfigureH,
|
|
20507
20568
|
headroom_budget: headroomBudgetH,
|
|
20508
20569
|
beads_configure: beadsConfigureH,
|
|
@@ -24816,62 +24877,85 @@ var AcpClient = class {
|
|
|
24816
24877
|
this.opts.onStderr?.(`Failed to launch ${adapter.command}: ${code}${hint}`);
|
|
24817
24878
|
this.opts.onUnexpectedExit?.(null, null);
|
|
24818
24879
|
});
|
|
24819
|
-
if (!child.stdin || !child.stdout) {
|
|
24820
|
-
throw new Error("Spawned ACP adapter is missing stdio handles");
|
|
24821
|
-
}
|
|
24822
|
-
const input = import_node_stream.Readable.toWeb(child.stdout);
|
|
24823
|
-
const output = import_node_stream.Writable.toWeb(child.stdin);
|
|
24824
|
-
const stream = ndJsonStream(output, input);
|
|
24825
|
-
this.connection = new ClientSideConnection(
|
|
24826
|
-
(_agent) => this.buildClient(),
|
|
24827
|
-
stream
|
|
24828
|
-
);
|
|
24829
|
-
log.info("acpClient", "initialize \u2192 sending");
|
|
24830
|
-
const initialize = await this.connection.initialize({
|
|
24831
|
-
protocolVersion: PROTOCOL_VERSION2,
|
|
24832
|
-
clientCapabilities: CLIENT_CAPABILITIES
|
|
24833
|
-
});
|
|
24834
|
-
log.info(
|
|
24835
|
-
"acpClient",
|
|
24836
|
-
`initialize \u2190 ok protocolVersion=${initialize.protocolVersion} agentCaps=${JSON.stringify(initialize.agentCapabilities ?? {}).slice(0, 200)}`
|
|
24837
|
-
);
|
|
24838
|
-
log.info("acpClient", "newSession \u2192 sending");
|
|
24839
|
-
let startupTimer;
|
|
24840
|
-
const startupFailure = new Promise((_2, reject) => {
|
|
24841
|
-
this.startupFailureReject = reject;
|
|
24842
|
-
startupTimer = setTimeout(() => {
|
|
24843
|
-
const tail = this.recentStderr.slice(-4).join(" | ");
|
|
24844
|
-
reject(
|
|
24845
|
-
new Error(
|
|
24846
|
-
`AGENT_STARTUP_TIMEOUT: ${this.opts.adapter.requiresAgentBinary} did not create a session within ${Math.round(
|
|
24847
|
-
NEWSESSION_TIMEOUT_MS / 1e3
|
|
24848
|
-
)}s${tail ? ` \u2014 last output: ${tail}` : ""}`
|
|
24849
|
-
)
|
|
24850
|
-
);
|
|
24851
|
-
}, NEWSESSION_TIMEOUT_MS);
|
|
24852
|
-
});
|
|
24853
|
-
let newSession;
|
|
24854
24880
|
try {
|
|
24855
|
-
|
|
24856
|
-
|
|
24857
|
-
|
|
24858
|
-
|
|
24859
|
-
|
|
24860
|
-
|
|
24861
|
-
this.
|
|
24881
|
+
if (!child.stdin || !child.stdout) {
|
|
24882
|
+
throw new Error("Spawned ACP adapter is missing stdio handles");
|
|
24883
|
+
}
|
|
24884
|
+
const input = import_node_stream.Readable.toWeb(child.stdout);
|
|
24885
|
+
const output = import_node_stream.Writable.toWeb(child.stdin);
|
|
24886
|
+
const stream = ndJsonStream(output, input);
|
|
24887
|
+
this.connection = new ClientSideConnection(
|
|
24888
|
+
(_agent) => this.buildClient(),
|
|
24889
|
+
stream
|
|
24890
|
+
);
|
|
24891
|
+
log.info("acpClient", "initialize \u2192 sending");
|
|
24892
|
+
const initialize = await this.connection.initialize({
|
|
24893
|
+
protocolVersion: PROTOCOL_VERSION2,
|
|
24894
|
+
clientCapabilities: CLIENT_CAPABILITIES
|
|
24895
|
+
});
|
|
24896
|
+
log.info(
|
|
24897
|
+
"acpClient",
|
|
24898
|
+
`initialize \u2190 ok protocolVersion=${initialize.protocolVersion} agentCaps=${JSON.stringify(initialize.agentCapabilities ?? {}).slice(0, 200)}`
|
|
24899
|
+
);
|
|
24900
|
+
log.info("acpClient", "newSession \u2192 sending");
|
|
24901
|
+
let startupTimer;
|
|
24902
|
+
const startupFailure = new Promise((_2, reject) => {
|
|
24903
|
+
this.startupFailureReject = reject;
|
|
24904
|
+
startupTimer = setTimeout(() => {
|
|
24905
|
+
const tail = this.recentStderr.slice(-4).join(" | ");
|
|
24906
|
+
reject(
|
|
24907
|
+
new Error(
|
|
24908
|
+
`AGENT_STARTUP_TIMEOUT: ${this.opts.adapter.requiresAgentBinary} did not create a session within ${Math.round(
|
|
24909
|
+
NEWSESSION_TIMEOUT_MS / 1e3
|
|
24910
|
+
)}s${tail ? ` \u2014 last output: ${tail}` : ""}`
|
|
24911
|
+
)
|
|
24912
|
+
);
|
|
24913
|
+
}, NEWSESSION_TIMEOUT_MS);
|
|
24914
|
+
});
|
|
24915
|
+
let newSession;
|
|
24916
|
+
try {
|
|
24917
|
+
newSession = await Promise.race([
|
|
24918
|
+
this.connection.newSession({ cwd, mcpServers: [] }),
|
|
24919
|
+
startupFailure
|
|
24920
|
+
]);
|
|
24921
|
+
} finally {
|
|
24922
|
+
if (startupTimer) clearTimeout(startupTimer);
|
|
24923
|
+
this.startupFailureReject = null;
|
|
24924
|
+
}
|
|
24925
|
+
this.sessionId = newSession.sessionId;
|
|
24926
|
+
const newSessionMeta = newSession;
|
|
24927
|
+
log.info(
|
|
24928
|
+
"acpClient",
|
|
24929
|
+
`newSession \u2190 ok sessionId=${newSession.sessionId.slice(0, 8)} model=${newSessionMeta.currentModelId ?? "?"} tier=${newSessionMeta.currentServiceTier ?? "?"}`
|
|
24930
|
+
);
|
|
24931
|
+
return {
|
|
24932
|
+
sessionId: newSession.sessionId,
|
|
24933
|
+
initialize,
|
|
24934
|
+
model: newSessionMeta.currentModelId,
|
|
24935
|
+
tier: newSessionMeta.currentServiceTier
|
|
24936
|
+
};
|
|
24937
|
+
} catch (err) {
|
|
24938
|
+
this.cleanupAfterFailedStart(child);
|
|
24939
|
+
throw err;
|
|
24862
24940
|
}
|
|
24863
|
-
|
|
24864
|
-
|
|
24865
|
-
|
|
24866
|
-
|
|
24867
|
-
|
|
24868
|
-
|
|
24869
|
-
|
|
24870
|
-
|
|
24871
|
-
|
|
24872
|
-
|
|
24873
|
-
|
|
24874
|
-
|
|
24941
|
+
}
|
|
24942
|
+
/**
|
|
24943
|
+
* Undo the partial state `start()` set before its handshake threw, so a
|
|
24944
|
+
* fresh `start()` call is retryable instead of permanently hard-throwing
|
|
24945
|
+
* 'AcpClient already started'. Removes OUR `exit`/`error` listeners first
|
|
24946
|
+
* so killing the half-started child doesn't also fire
|
|
24947
|
+
* `onUnexpectedExit` — the baton's `onUnexpectedExit` calls
|
|
24948
|
+
* `process.exit`, which must never fire for a failure the caller already
|
|
24949
|
+
* received as a thrown error from `start()`.
|
|
24950
|
+
*/
|
|
24951
|
+
cleanupAfterFailedStart(child) {
|
|
24952
|
+
child.removeAllListeners("exit");
|
|
24953
|
+
child.removeAllListeners("error");
|
|
24954
|
+
killQuiet(child, "SIGKILL");
|
|
24955
|
+
this.child = null;
|
|
24956
|
+
this.connection = null;
|
|
24957
|
+
this.sessionId = null;
|
|
24958
|
+
this.startupFailureReject = null;
|
|
24875
24959
|
}
|
|
24876
24960
|
/**
|
|
24877
24961
|
* Send a user prompt to the active session. Returns the
|
|
@@ -25177,6 +25261,38 @@ function expandPathForAgentBinaries(existingPath) {
|
|
|
25177
25261
|
return [...additions, existingPath].filter((p2) => p2.length > 0).join(path58.delimiter);
|
|
25178
25262
|
}
|
|
25179
25263
|
|
|
25264
|
+
// src/agents/acp/headroom-budget-proxy.ts
|
|
25265
|
+
function buildRelaunchProxyEnv(baseEnv) {
|
|
25266
|
+
const env = { ...baseEnv, HEADROOM_KOMPRESS_BACKEND: "onnx_cpu" };
|
|
25267
|
+
delete env["HEADROOM_BUDGET"];
|
|
25268
|
+
delete env["HEADROOM_BUDGET_PERIOD"];
|
|
25269
|
+
return env;
|
|
25270
|
+
}
|
|
25271
|
+
var relaunchProxyWithoutBudget = async () => {
|
|
25272
|
+
const { spawn: spawn38 } = await import("child_process");
|
|
25273
|
+
killHeadroomProxy();
|
|
25274
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
25275
|
+
const proxyEnv = buildRelaunchProxyEnv(process.env);
|
|
25276
|
+
try {
|
|
25277
|
+
const proxy = spawn38(
|
|
25278
|
+
"headroom",
|
|
25279
|
+
["proxy", "--port", "8787"],
|
|
25280
|
+
{ stdio: "ignore", detached: true, env: proxyEnv }
|
|
25281
|
+
);
|
|
25282
|
+
proxy.once("error", (e) => {
|
|
25283
|
+
log.warn("acpRunner", `budget recovery proxy relaunch error (best-effort): ${e.message}`);
|
|
25284
|
+
});
|
|
25285
|
+
proxy.unref();
|
|
25286
|
+
writeHeadroomProxyPidfile(proxy.pid);
|
|
25287
|
+
} catch (e) {
|
|
25288
|
+
log.warn(
|
|
25289
|
+
"acpRunner",
|
|
25290
|
+
`budget recovery proxy relaunch failed (best-effort): ${e instanceof Error ? e.message : String(e)}`
|
|
25291
|
+
);
|
|
25292
|
+
}
|
|
25293
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
25294
|
+
};
|
|
25295
|
+
|
|
25180
25296
|
// src/services/streaming/transport.ts
|
|
25181
25297
|
var http7 = __toESM(require("http"));
|
|
25182
25298
|
var https8 = __toESM(require("https"));
|
|
@@ -26063,7 +26179,7 @@ function defaultRunGit(cwd, args2) {
|
|
|
26063
26179
|
});
|
|
26064
26180
|
}
|
|
26065
26181
|
async function discoverRepos(workingDir, maxDepth = 4) {
|
|
26066
|
-
const
|
|
26182
|
+
const fs61 = await import("fs/promises");
|
|
26067
26183
|
const out2 = [];
|
|
26068
26184
|
await walk(workingDir, 0);
|
|
26069
26185
|
return out2;
|
|
@@ -26071,7 +26187,7 @@ async function discoverRepos(workingDir, maxDepth = 4) {
|
|
|
26071
26187
|
if (depth > maxDepth) return;
|
|
26072
26188
|
let entries = [];
|
|
26073
26189
|
try {
|
|
26074
|
-
const dirents = await
|
|
26190
|
+
const dirents = await fs61.readdir(dir, { withFileTypes: true });
|
|
26075
26191
|
entries = dirents.filter((d3) => !d3.name.startsWith(".") || d3.name === ".git").map((d3) => ({ name: d3.name, isDirectory: d3.isDirectory() }));
|
|
26076
26192
|
} catch {
|
|
26077
26193
|
return;
|
|
@@ -26710,6 +26826,9 @@ function formatAgentReplyLine(text) {
|
|
|
26710
26826
|
}
|
|
26711
26827
|
|
|
26712
26828
|
// src/agents/acp/command-handlers.ts
|
|
26829
|
+
function assembleAcpCommandContext(session, cmd) {
|
|
26830
|
+
return { ...session, cmd };
|
|
26831
|
+
}
|
|
26713
26832
|
var ACP_QUICK_REPLIES = ["Continue", "Yes, go ahead", "Explain"];
|
|
26714
26833
|
async function recoverFromFailedTurn(client2, streaming) {
|
|
26715
26834
|
await cancelStuckTurn(client2);
|
|
@@ -27680,12 +27799,6 @@ async function surfaceStartupFailure(opts) {
|
|
|
27680
27799
|
);
|
|
27681
27800
|
errRelay.start();
|
|
27682
27801
|
}
|
|
27683
|
-
function buildRelaunchProxyEnv(baseEnv) {
|
|
27684
|
-
const env = { ...baseEnv, HEADROOM_KOMPRESS_BACKEND: "onnx_cpu" };
|
|
27685
|
-
delete env["HEADROOM_BUDGET"];
|
|
27686
|
-
delete env["HEADROOM_BUDGET_PERIOD"];
|
|
27687
|
-
return env;
|
|
27688
|
-
}
|
|
27689
27802
|
function computeAdapterExtraEnv(params) {
|
|
27690
27803
|
const env = {};
|
|
27691
27804
|
if (params.disable1mContext) env.CLAUDE_CODE_DISABLE_1M_CONTEXT = "1";
|
|
@@ -27802,30 +27915,6 @@ async function runAcpSession(opts) {
|
|
|
27802
27915
|
};
|
|
27803
27916
|
const client2 = new AcpClient(clientOptions);
|
|
27804
27917
|
let _budgetReachedPosted = false;
|
|
27805
|
-
const relaunchProxyWithoutBudget = async () => {
|
|
27806
|
-
const { spawn: spawn38 } = await import("child_process");
|
|
27807
|
-
killHeadroomProxy();
|
|
27808
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
27809
|
-
const proxyEnv = buildRelaunchProxyEnv(process.env);
|
|
27810
|
-
try {
|
|
27811
|
-
const proxy = spawn38(
|
|
27812
|
-
"headroom",
|
|
27813
|
-
["proxy", "--port", "8787"],
|
|
27814
|
-
{ stdio: "ignore", detached: true, env: proxyEnv }
|
|
27815
|
-
);
|
|
27816
|
-
proxy.once("error", (e) => {
|
|
27817
|
-
log.warn("acpRunner", `budget recovery proxy relaunch error (best-effort): ${e.message}`);
|
|
27818
|
-
});
|
|
27819
|
-
proxy.unref();
|
|
27820
|
-
writeHeadroomProxyPidfile(proxy.pid);
|
|
27821
|
-
} catch (e) {
|
|
27822
|
-
log.warn(
|
|
27823
|
-
"acpRunner",
|
|
27824
|
-
`budget recovery proxy relaunch failed (best-effort): ${e instanceof Error ? e.message : String(e)}`
|
|
27825
|
-
);
|
|
27826
|
-
}
|
|
27827
|
-
await new Promise((r) => setTimeout(r, 3e3));
|
|
27828
|
-
};
|
|
27829
27918
|
const budgetRecovery = createBudgetRecovery({
|
|
27830
27919
|
publishText: (text) => publisher.publishOutput({ type: "text", content: text, done: true }),
|
|
27831
27920
|
publishSelectPrompt: (question, options) => publisher.publishOutput({
|
|
@@ -27993,8 +28082,7 @@ async function runAcpSession(opts) {
|
|
|
27993
28082
|
});
|
|
27994
28083
|
}
|
|
27995
28084
|
async function handleCommand(cmd, client2, relay, acpSessionId, models, streaming, opts, history, jsonlHistory, agentCaps, turnFiles, getBeads, publisher, recentStderr, budgetRecovery, budgetReachedFlag) {
|
|
27996
|
-
|
|
27997
|
-
cmd,
|
|
28085
|
+
const session = {
|
|
27998
28086
|
client: client2,
|
|
27999
28087
|
relay,
|
|
28000
28088
|
acpSessionId,
|
|
@@ -28010,7 +28098,8 @@ async function handleCommand(cmd, client2, relay, acpSessionId, models, streamin
|
|
|
28010
28098
|
recentStderr,
|
|
28011
28099
|
budgetRecovery,
|
|
28012
28100
|
budgetReachedFlag
|
|
28013
|
-
}
|
|
28101
|
+
};
|
|
28102
|
+
await dispatchAcpCommand(assembleAcpCommandContext(session, cmd));
|
|
28014
28103
|
}
|
|
28015
28104
|
function buildBannerSubtitle(agentId, acpSessionId, model, tier) {
|
|
28016
28105
|
const meta = AGENT_REGISTRY[agentId];
|
|
@@ -29017,8 +29106,568 @@ function fetchQuotaUsage(runtime, historySvc) {
|
|
|
29017
29106
|
});
|
|
29018
29107
|
}
|
|
29019
29108
|
|
|
29020
|
-
// src/
|
|
29109
|
+
// src/baton/gate.ts
|
|
29110
|
+
function isLocalSession(env = process.env) {
|
|
29111
|
+
return env.CODESPACES !== "true" && env.CODEAM_AUTO_APPROVE !== "1" && env.HEADROOM_ENABLED !== "1" && !env.CODEAM_AUTO_TOKEN && !env.CODEAM_ENROLL_TOKEN;
|
|
29112
|
+
}
|
|
29113
|
+
|
|
29114
|
+
// src/baton/baton-controller.ts
|
|
29115
|
+
var BatonController = class {
|
|
29116
|
+
constructor(deps) {
|
|
29117
|
+
this.deps = deps;
|
|
29118
|
+
}
|
|
29119
|
+
deps;
|
|
29120
|
+
_state = "LOCAL_DRIVE";
|
|
29121
|
+
_active = "local_tui";
|
|
29122
|
+
_conversationId = null;
|
|
29123
|
+
get state() {
|
|
29124
|
+
return this._state;
|
|
29125
|
+
}
|
|
29126
|
+
get activeDriver() {
|
|
29127
|
+
return this._active;
|
|
29128
|
+
}
|
|
29129
|
+
/** The driver object currently holding the baton. The baton router forwards
|
|
29130
|
+
* non-baton commands to `activeSessionDriver.dispatch(cmd)`, so whichever side
|
|
29131
|
+
* holds the baton is the one that actually runs the command. Reads `_active`
|
|
29132
|
+
* live, so mid-turn commands during a `SWITCHING` window still target the
|
|
29133
|
+
* pre-switch driver (the switch waits for the turn boundary before flipping). */
|
|
29134
|
+
get activeSessionDriver() {
|
|
29135
|
+
return this._active === "local_tui" ? this.deps.local : this.deps.mobile;
|
|
29136
|
+
}
|
|
29137
|
+
get conversationId() {
|
|
29138
|
+
return this._conversationId;
|
|
29139
|
+
}
|
|
29140
|
+
async begin() {
|
|
29141
|
+
this._conversationId = await this.deps.local.start(void 0);
|
|
29142
|
+
this._active = "local_tui";
|
|
29143
|
+
this.setState("LOCAL_DRIVE");
|
|
29144
|
+
}
|
|
29145
|
+
async takeControl() {
|
|
29146
|
+
await this.switchDriver(
|
|
29147
|
+
"LOCAL_DRIVE",
|
|
29148
|
+
this.deps.local,
|
|
29149
|
+
this.deps.mobile,
|
|
29150
|
+
"MOBILE_DRIVE",
|
|
29151
|
+
"mobile_acp"
|
|
29152
|
+
);
|
|
29153
|
+
}
|
|
29154
|
+
async handback() {
|
|
29155
|
+
await this.switchDriver(
|
|
29156
|
+
"MOBILE_DRIVE",
|
|
29157
|
+
this.deps.mobile,
|
|
29158
|
+
this.deps.local,
|
|
29159
|
+
"LOCAL_DRIVE",
|
|
29160
|
+
"local_tui"
|
|
29161
|
+
);
|
|
29162
|
+
}
|
|
29163
|
+
async shutdown() {
|
|
29164
|
+
await Promise.allSettled([this.deps.local.stop(), this.deps.mobile.stop()]);
|
|
29165
|
+
}
|
|
29166
|
+
async switchDriver(from, current, next, to, nextKind) {
|
|
29167
|
+
if (this._state !== from) return;
|
|
29168
|
+
this.setState("SWITCHING");
|
|
29169
|
+
const priorActive = this._active;
|
|
29170
|
+
const priorConversationId = this._conversationId;
|
|
29171
|
+
try {
|
|
29172
|
+
await current.whenSafeToYield();
|
|
29173
|
+
await current.stop();
|
|
29174
|
+
this._conversationId = await next.start(this._conversationId ?? void 0);
|
|
29175
|
+
this._active = nextKind;
|
|
29176
|
+
this.setState(to);
|
|
29177
|
+
} catch (err) {
|
|
29178
|
+
this._active = priorActive;
|
|
29179
|
+
this._conversationId = priorConversationId;
|
|
29180
|
+
this.setState(from);
|
|
29181
|
+
throw err;
|
|
29182
|
+
}
|
|
29183
|
+
}
|
|
29184
|
+
setState(state) {
|
|
29185
|
+
this._state = state;
|
|
29186
|
+
this.deps.publishState(state, this._active, this._conversationId);
|
|
29187
|
+
}
|
|
29188
|
+
};
|
|
29189
|
+
|
|
29190
|
+
// src/baton/native-tui-driver.ts
|
|
29191
|
+
var NativeTuiDriver = class {
|
|
29192
|
+
constructor(deps) {
|
|
29193
|
+
this.deps = deps;
|
|
29194
|
+
this.agent = deps.agent;
|
|
29195
|
+
this.idleMs = deps.idleMs ?? 750;
|
|
29196
|
+
this.now = deps.now ?? Date.now;
|
|
29197
|
+
this.lastOutput = this.now();
|
|
29198
|
+
this.historySvc = new HistoryService(deps.runtime, deps.opts.pluginId, deps.opts.cwd, {
|
|
29199
|
+
pluginAuthToken: deps.opts.pluginAuthToken
|
|
29200
|
+
});
|
|
29201
|
+
this.outputSvc = new OutputService(
|
|
29202
|
+
deps.opts.sessionId,
|
|
29203
|
+
deps.opts.pluginId,
|
|
29204
|
+
(conversationId) => this.historySvc.setCurrentConversationId(conversationId),
|
|
29205
|
+
(reset) => this.historySvc.setRateLimitReset(reset),
|
|
29206
|
+
void 0,
|
|
29207
|
+
void 0,
|
|
29208
|
+
deps.opts.pluginAuthToken,
|
|
29209
|
+
deps.runtime
|
|
29210
|
+
);
|
|
29211
|
+
this.keepAliveCtx = { inCodespace: false, codespaceName: void 0 };
|
|
29212
|
+
this.setKeepAlive = buildKeepAlive(this.keepAliveCtx).apply;
|
|
29213
|
+
}
|
|
29214
|
+
deps;
|
|
29215
|
+
kind = "local_tui";
|
|
29216
|
+
agent;
|
|
29217
|
+
idleMs;
|
|
29218
|
+
now;
|
|
29219
|
+
lastOutput;
|
|
29220
|
+
outputSvc;
|
|
29221
|
+
historySvc;
|
|
29222
|
+
setKeepAlive;
|
|
29223
|
+
keepAliveCtx;
|
|
29224
|
+
async start(resumeId) {
|
|
29225
|
+
if (resumeId !== void 0) {
|
|
29226
|
+
await this.agent.restart(resumeId, false);
|
|
29227
|
+
return resumeId;
|
|
29228
|
+
}
|
|
29229
|
+
await this.agent.spawn();
|
|
29230
|
+
const id = this.agent.spawnedSessionId;
|
|
29231
|
+
if (!id) throw new Error("NativeTuiDriver: agent did not expose a session id after spawn");
|
|
29232
|
+
return id;
|
|
29233
|
+
}
|
|
29234
|
+
async stop() {
|
|
29235
|
+
this.agent.kill();
|
|
29236
|
+
}
|
|
29237
|
+
async dispatch(cmd) {
|
|
29238
|
+
const ctx = {
|
|
29239
|
+
outputSvc: this.outputSvc,
|
|
29240
|
+
agent: this.agent,
|
|
29241
|
+
historySvc: this.historySvc,
|
|
29242
|
+
runtime: this.deps.runtime,
|
|
29243
|
+
relay: this.deps.getRelay(),
|
|
29244
|
+
setKeepAlive: this.setKeepAlive,
|
|
29245
|
+
keepAliveCtx: this.keepAliveCtx,
|
|
29246
|
+
pluginId: this.deps.opts.pluginId,
|
|
29247
|
+
sessionId: this.deps.opts.sessionId,
|
|
29248
|
+
agentId: this.deps.opts.agentId,
|
|
29249
|
+
pluginAuthToken: this.deps.opts.pluginAuthToken,
|
|
29250
|
+
beads: this.deps.getBeads()
|
|
29251
|
+
};
|
|
29252
|
+
await dispatchCommand(ctx, cmd);
|
|
29253
|
+
}
|
|
29254
|
+
/** Call on every PTY data chunk: reset the idle timer AND feed the output
|
|
29255
|
+
* pipe so a mobile-routed turn's reply streams back to the app. */
|
|
29256
|
+
handlePtyData(raw) {
|
|
29257
|
+
this.noteOutput();
|
|
29258
|
+
this.outputSvc.push(raw);
|
|
29259
|
+
}
|
|
29260
|
+
/** Reset the idle timer only. Retained for callers that just need the boundary
|
|
29261
|
+
* clock nudged without routing bytes through the output pipe. */
|
|
29262
|
+
noteOutput() {
|
|
29263
|
+
this.lastOutput = this.now();
|
|
29264
|
+
}
|
|
29265
|
+
whenSafeToYield() {
|
|
29266
|
+
return new Promise((resolve7) => {
|
|
29267
|
+
const tick = () => {
|
|
29268
|
+
const quietFor = this.now() - this.lastOutput;
|
|
29269
|
+
if (quietFor >= this.idleMs) resolve7();
|
|
29270
|
+
else setTimeout(tick, this.idleMs - quietFor);
|
|
29271
|
+
};
|
|
29272
|
+
tick();
|
|
29273
|
+
});
|
|
29274
|
+
}
|
|
29275
|
+
};
|
|
29276
|
+
|
|
29277
|
+
// src/baton/acp-driver.ts
|
|
29278
|
+
var import_node_crypto8 = require("crypto");
|
|
29279
|
+
var AcpDriver = class {
|
|
29280
|
+
constructor(deps) {
|
|
29281
|
+
this.deps = deps;
|
|
29282
|
+
}
|
|
29283
|
+
deps;
|
|
29284
|
+
kind = "mobile_acp";
|
|
29285
|
+
turnActive = false;
|
|
29286
|
+
waiters = [];
|
|
29287
|
+
/** Set once the adapter has handshaked (in {@link start}); the shared
|
|
29288
|
+
* conversation id (resumed id, or the freshly minted one). */
|
|
29289
|
+
acpSessionId = null;
|
|
29290
|
+
agentCaps;
|
|
29291
|
+
/** Lazily-built, memoised per {@link start} — cleared when the conversation
|
|
29292
|
+
* changes so history/models rebind to the current conversation. */
|
|
29293
|
+
session = null;
|
|
29294
|
+
budgetReachedPosted = false;
|
|
29295
|
+
async start(resumeId) {
|
|
29296
|
+
let started;
|
|
29297
|
+
try {
|
|
29298
|
+
started = await this.deps.client.start();
|
|
29299
|
+
if (resumeId !== void 0) {
|
|
29300
|
+
await this.deps.client.loadSession(resumeId);
|
|
29301
|
+
}
|
|
29302
|
+
} catch (err) {
|
|
29303
|
+
await this.deps.client.stop().catch(() => void 0);
|
|
29304
|
+
throw err;
|
|
29305
|
+
}
|
|
29306
|
+
const conversationId = resumeId !== void 0 ? resumeId : started.sessionId;
|
|
29307
|
+
this.acpSessionId = conversationId;
|
|
29308
|
+
this.agentCaps = started.initialize.agentCapabilities;
|
|
29309
|
+
this.session = null;
|
|
29310
|
+
this.budgetReachedPosted = false;
|
|
29311
|
+
return conversationId;
|
|
29312
|
+
}
|
|
29313
|
+
async stop() {
|
|
29314
|
+
await this.deps.client.stop();
|
|
29315
|
+
this.acpSessionId = null;
|
|
29316
|
+
this.session = null;
|
|
29317
|
+
}
|
|
29318
|
+
async dispatch(cmd) {
|
|
29319
|
+
const relay = this.deps.getRelay();
|
|
29320
|
+
const acpSessionId = this.acpSessionId;
|
|
29321
|
+
if (acpSessionId === null) {
|
|
29322
|
+
await relay.sendResult(cmd.id, "failed", { code: "BATON_MOBILE_NOT_STARTED" });
|
|
29323
|
+
return;
|
|
29324
|
+
}
|
|
29325
|
+
const session = await this.ensureSession(relay, acpSessionId, this.agentCaps);
|
|
29326
|
+
this.beginTurn();
|
|
29327
|
+
try {
|
|
29328
|
+
await dispatchAcpCommand(assembleAcpCommandContext(session, cmd));
|
|
29329
|
+
} finally {
|
|
29330
|
+
this.endTurn();
|
|
29331
|
+
}
|
|
29332
|
+
}
|
|
29333
|
+
/** Build (once per conversation) the session-scoped ACP context — the same
|
|
29334
|
+
* shape `runAcpSession` assembles. Memoised on {@link session}. */
|
|
29335
|
+
async ensureSession(relay, acpSessionId, agentCaps) {
|
|
29336
|
+
if (this.session) return this.session;
|
|
29337
|
+
const { opts, publisher, streaming, runtime, recentStderr } = this.deps;
|
|
29338
|
+
const models = await runtime.listModels();
|
|
29339
|
+
const jsonlHistory = new HistoryService(runtime, opts.pluginId, opts.cwd, {
|
|
29340
|
+
pluginAuthToken: opts.pluginAuthToken
|
|
29341
|
+
});
|
|
29342
|
+
const history = new AcpHistory(publisher, { agent: opts.agent, acpSessionId });
|
|
29343
|
+
const turnFiles = new TurnFileAggregator({
|
|
29344
|
+
workingDir: opts.cwd,
|
|
29345
|
+
sessionId: opts.sessionId,
|
|
29346
|
+
pluginId: opts.pluginId,
|
|
29347
|
+
pluginAuthToken: opts.pluginAuthToken,
|
|
29348
|
+
agentId: opts.agent
|
|
29349
|
+
});
|
|
29350
|
+
const budgetReachedFlag = {
|
|
29351
|
+
get: () => this.budgetReachedPosted,
|
|
29352
|
+
set: (v) => {
|
|
29353
|
+
this.budgetReachedPosted = v;
|
|
29354
|
+
}
|
|
29355
|
+
};
|
|
29356
|
+
const budgetRecovery = createBudgetRecovery({
|
|
29357
|
+
publishText: (text) => publisher.publishOutput({ type: "text", content: text, done: true }),
|
|
29358
|
+
publishSelectPrompt: (question, options) => publisher.publishOutput({
|
|
29359
|
+
type: "select_prompt",
|
|
29360
|
+
content: question,
|
|
29361
|
+
options,
|
|
29362
|
+
optionDescriptions: options.map(() => ""),
|
|
29363
|
+
currentIndex: 0,
|
|
29364
|
+
done: true
|
|
29365
|
+
}),
|
|
29366
|
+
publishAwaitingAnswer: (prompt, options) => publisher.publishAwaitingAnswer({ questionId: (0, import_node_crypto8.randomUUID)(), prompt, options }),
|
|
29367
|
+
publishRawChunk: (chunk) => publisher.publishOutput(chunk),
|
|
29368
|
+
sendResult: (commandId, status2, result) => relay.sendResult(commandId, status2, result),
|
|
29369
|
+
appendAgentReply: (text) => history.appendAgentReply(text),
|
|
29370
|
+
flushHistory: () => void history.flush(),
|
|
29371
|
+
relaunchProxyWithoutBudget,
|
|
29372
|
+
agentId: opts.agent
|
|
29373
|
+
});
|
|
29374
|
+
this.session = {
|
|
29375
|
+
client: this.deps.client,
|
|
29376
|
+
relay,
|
|
29377
|
+
acpSessionId,
|
|
29378
|
+
models,
|
|
29379
|
+
streaming,
|
|
29380
|
+
opts,
|
|
29381
|
+
history,
|
|
29382
|
+
jsonlHistory,
|
|
29383
|
+
agentCaps,
|
|
29384
|
+
turnFiles,
|
|
29385
|
+
getBeads: opts.getBeads ?? (() => null),
|
|
29386
|
+
publisher,
|
|
29387
|
+
recentStderr,
|
|
29388
|
+
budgetRecovery,
|
|
29389
|
+
budgetReachedFlag
|
|
29390
|
+
};
|
|
29391
|
+
return this.session;
|
|
29392
|
+
}
|
|
29393
|
+
beginTurn() {
|
|
29394
|
+
this.turnActive = true;
|
|
29395
|
+
}
|
|
29396
|
+
endTurn() {
|
|
29397
|
+
this.turnActive = false;
|
|
29398
|
+
const waiters = this.waiters;
|
|
29399
|
+
this.waiters = [];
|
|
29400
|
+
waiters.forEach((resolve7) => resolve7());
|
|
29401
|
+
}
|
|
29402
|
+
whenSafeToYield() {
|
|
29403
|
+
if (!this.turnActive) return Promise.resolve();
|
|
29404
|
+
return new Promise((resolve7) => this.waiters.push(resolve7));
|
|
29405
|
+
}
|
|
29406
|
+
};
|
|
29407
|
+
|
|
29408
|
+
// src/baton/transcript-mirror.ts
|
|
29021
29409
|
var fs58 = __toESM(require("fs"));
|
|
29410
|
+
var TranscriptMirror = class {
|
|
29411
|
+
constructor(deps) {
|
|
29412
|
+
this.deps = deps;
|
|
29413
|
+
}
|
|
29414
|
+
deps;
|
|
29415
|
+
emitted = 0;
|
|
29416
|
+
unwatch = null;
|
|
29417
|
+
start() {
|
|
29418
|
+
const file = this.deps.runtime.resolveHistoryFile?.(this.deps.cwd, this.deps.conversationId);
|
|
29419
|
+
if (!file) return;
|
|
29420
|
+
this.emit(file);
|
|
29421
|
+
const watch2 = this.deps.watch ?? defaultWatch;
|
|
29422
|
+
this.unwatch = watch2(file, () => this.emit(file));
|
|
29423
|
+
}
|
|
29424
|
+
stop() {
|
|
29425
|
+
this.unwatch?.();
|
|
29426
|
+
this.unwatch = null;
|
|
29427
|
+
}
|
|
29428
|
+
emit(file) {
|
|
29429
|
+
let all;
|
|
29430
|
+
try {
|
|
29431
|
+
all = this.deps.runtime.parseHistoryFile(file);
|
|
29432
|
+
} catch {
|
|
29433
|
+
return;
|
|
29434
|
+
}
|
|
29435
|
+
if (all.length <= this.emitted) return;
|
|
29436
|
+
const delta = all.slice(this.emitted);
|
|
29437
|
+
this.emitted = all.length;
|
|
29438
|
+
this.deps.onNewMessages(delta);
|
|
29439
|
+
}
|
|
29440
|
+
};
|
|
29441
|
+
function defaultWatch(file, onChange) {
|
|
29442
|
+
const w3 = fs58.watch(file, { persistent: false }, () => onChange());
|
|
29443
|
+
return () => w3.close();
|
|
29444
|
+
}
|
|
29445
|
+
|
|
29446
|
+
// src/baton/wire-baton.ts
|
|
29447
|
+
function makeOnCommand(deps) {
|
|
29448
|
+
return async function onCommand(cmd) {
|
|
29449
|
+
if (cmd.type === "take_control") {
|
|
29450
|
+
try {
|
|
29451
|
+
await deps.controller.takeControl();
|
|
29452
|
+
} catch (err) {
|
|
29453
|
+
await deps.ack(cmd.id, "failed", {
|
|
29454
|
+
code: "BATON_SWITCH_FAILED",
|
|
29455
|
+
message: err instanceof Error ? err.message : String(err)
|
|
29456
|
+
});
|
|
29457
|
+
return;
|
|
29458
|
+
}
|
|
29459
|
+
await deps.ack(cmd.id, "completed", { state: deps.controller.state });
|
|
29460
|
+
return;
|
|
29461
|
+
}
|
|
29462
|
+
if (cmd.type === "handback") {
|
|
29463
|
+
try {
|
|
29464
|
+
await deps.controller.handback();
|
|
29465
|
+
} catch (err) {
|
|
29466
|
+
await deps.ack(cmd.id, "failed", {
|
|
29467
|
+
code: "BATON_SWITCH_FAILED",
|
|
29468
|
+
message: err instanceof Error ? err.message : String(err)
|
|
29469
|
+
});
|
|
29470
|
+
return;
|
|
29471
|
+
}
|
|
29472
|
+
await deps.ack(cmd.id, "completed", { state: deps.controller.state });
|
|
29473
|
+
return;
|
|
29474
|
+
}
|
|
29475
|
+
await deps.dispatchActive(cmd);
|
|
29476
|
+
};
|
|
29477
|
+
}
|
|
29478
|
+
async function publishMirroredTurnsLive(publisher, messages) {
|
|
29479
|
+
for (const m of messages) {
|
|
29480
|
+
if (m.role === "user") {
|
|
29481
|
+
await publisher.publishOutput({ type: "clear" });
|
|
29482
|
+
await publisher.publishOutput({ type: "user_message", content: m.text, done: true });
|
|
29483
|
+
await publisher.publishOutput({ type: "new_turn", done: false });
|
|
29484
|
+
} else {
|
|
29485
|
+
await publisher.publishOutput({ type: "text", content: m.text, done: true });
|
|
29486
|
+
}
|
|
29487
|
+
}
|
|
29488
|
+
}
|
|
29489
|
+
function makeMirrorOnNewMessages(deps) {
|
|
29490
|
+
let isFirstEmit = true;
|
|
29491
|
+
let publishChain = Promise.resolve();
|
|
29492
|
+
return (messages) => {
|
|
29493
|
+
const relevant = messages.filter((m) => m.role !== "system");
|
|
29494
|
+
if (relevant.length === 0) return;
|
|
29495
|
+
const mapped = relevant.map((m) => ({
|
|
29496
|
+
id: m.id,
|
|
29497
|
+
role: m.role === "user" ? "user" : "agent",
|
|
29498
|
+
text: m.text,
|
|
29499
|
+
timestamp: toEpochMs(m.timestamp)
|
|
29500
|
+
}));
|
|
29501
|
+
void deps.publisher.pushConversation({
|
|
29502
|
+
agentId: deps.agentId,
|
|
29503
|
+
sessionId: deps.conversationId,
|
|
29504
|
+
messages: mapped
|
|
29505
|
+
});
|
|
29506
|
+
if (!isFirstEmit) {
|
|
29507
|
+
publishChain = publishChain.then(() => publishMirroredTurnsLive(deps.publisher, relevant)).catch((err) => {
|
|
29508
|
+
log.warn(
|
|
29509
|
+
"wireBaton",
|
|
29510
|
+
`mirror live-publish failed: ${err instanceof Error ? err.message : String(err)}`
|
|
29511
|
+
);
|
|
29512
|
+
});
|
|
29513
|
+
}
|
|
29514
|
+
isFirstEmit = false;
|
|
29515
|
+
};
|
|
29516
|
+
}
|
|
29517
|
+
async function runBatonSession(opts) {
|
|
29518
|
+
const publisher = new AcpPublisher({
|
|
29519
|
+
sessionId: opts.sessionId,
|
|
29520
|
+
pluginId: opts.pluginId,
|
|
29521
|
+
pluginAuthToken: opts.pluginAuthToken,
|
|
29522
|
+
refreshAuthToken: () => fetchCurrentPluginAuthToken(opts.sessionId, opts.pluginId, opts.pollSecret)
|
|
29523
|
+
});
|
|
29524
|
+
const runtime = createRuntimeStrategy(opts.agent);
|
|
29525
|
+
const streaming = new StreamingState(publisher);
|
|
29526
|
+
const recentStderr = [];
|
|
29527
|
+
const client2 = new AcpClient({
|
|
29528
|
+
adapter: opts.adapter,
|
|
29529
|
+
cwd: opts.cwd,
|
|
29530
|
+
onSessionUpdate: (notification) => {
|
|
29531
|
+
for (const delta of mapSessionUpdate(notification)) streaming.append(delta);
|
|
29532
|
+
},
|
|
29533
|
+
onRequestPermission: async (request) => {
|
|
29534
|
+
const { event, optionIdByLabel } = mapPermissionRequest(request);
|
|
29535
|
+
await publisher.publishAwaitingAnswer(event);
|
|
29536
|
+
return streaming.registerPermission({
|
|
29537
|
+
questionId: event.questionId,
|
|
29538
|
+
labels: event.options ?? [],
|
|
29539
|
+
optionIdByLabel
|
|
29540
|
+
});
|
|
29541
|
+
},
|
|
29542
|
+
onStderr: (line) => {
|
|
29543
|
+
recentStderr.push(line);
|
|
29544
|
+
if (recentStderr.length > 40) recentStderr.shift();
|
|
29545
|
+
},
|
|
29546
|
+
onUnexpectedExit: (code, signal) => {
|
|
29547
|
+
log.warn("baton", `ACP adapter exited code=${code} signal=${signal}; flushing`);
|
|
29548
|
+
void streaming.closeAll().finally(() => process.exit(code ?? 1));
|
|
29549
|
+
}
|
|
29550
|
+
});
|
|
29551
|
+
let relay;
|
|
29552
|
+
const acpOpts = {
|
|
29553
|
+
agent: opts.agent,
|
|
29554
|
+
sessionId: opts.sessionId,
|
|
29555
|
+
pluginId: opts.pluginId,
|
|
29556
|
+
pluginAuthToken: opts.pluginAuthToken,
|
|
29557
|
+
adapter: opts.adapter,
|
|
29558
|
+
cwd: opts.cwd,
|
|
29559
|
+
getBeads: opts.getBeads,
|
|
29560
|
+
pollSecret: opts.pollSecret
|
|
29561
|
+
};
|
|
29562
|
+
const mobileDriver = new AcpDriver({
|
|
29563
|
+
client: client2,
|
|
29564
|
+
publisher,
|
|
29565
|
+
streaming,
|
|
29566
|
+
runtime,
|
|
29567
|
+
recentStderr,
|
|
29568
|
+
opts: acpOpts,
|
|
29569
|
+
getRelay: () => relay
|
|
29570
|
+
});
|
|
29571
|
+
let nativeDriver;
|
|
29572
|
+
const agent = new AgentService(runtime, {
|
|
29573
|
+
cwd: opts.cwd,
|
|
29574
|
+
onData(raw) {
|
|
29575
|
+
nativeDriver.handlePtyData(raw);
|
|
29576
|
+
},
|
|
29577
|
+
onExit(code) {
|
|
29578
|
+
teardown();
|
|
29579
|
+
process.exit(code);
|
|
29580
|
+
}
|
|
29581
|
+
});
|
|
29582
|
+
nativeDriver = new NativeTuiDriver({
|
|
29583
|
+
agent,
|
|
29584
|
+
runtime,
|
|
29585
|
+
opts: {
|
|
29586
|
+
sessionId: opts.sessionId,
|
|
29587
|
+
pluginId: opts.pluginId,
|
|
29588
|
+
agentId: opts.agent,
|
|
29589
|
+
pluginAuthToken: opts.pluginAuthToken,
|
|
29590
|
+
cwd: opts.cwd
|
|
29591
|
+
},
|
|
29592
|
+
getRelay: () => relay,
|
|
29593
|
+
getBeads: opts.getBeads ?? (() => null)
|
|
29594
|
+
});
|
|
29595
|
+
let mirror = null;
|
|
29596
|
+
const startMirror = (conversationId) => {
|
|
29597
|
+
mirror?.stop();
|
|
29598
|
+
mirror = new TranscriptMirror({
|
|
29599
|
+
runtime,
|
|
29600
|
+
cwd: opts.cwd,
|
|
29601
|
+
conversationId,
|
|
29602
|
+
onNewMessages: makeMirrorOnNewMessages({
|
|
29603
|
+
publisher,
|
|
29604
|
+
agentId: opts.agent,
|
|
29605
|
+
conversationId
|
|
29606
|
+
})
|
|
29607
|
+
});
|
|
29608
|
+
mirror.start();
|
|
29609
|
+
};
|
|
29610
|
+
const controller = new BatonController({
|
|
29611
|
+
local: nativeDriver,
|
|
29612
|
+
mobile: mobileDriver,
|
|
29613
|
+
publishState: (state, driver, conversationId) => {
|
|
29614
|
+
void postBatonEvent({
|
|
29615
|
+
sessionId: opts.sessionId,
|
|
29616
|
+
pluginId: opts.pluginId,
|
|
29617
|
+
pluginAuthToken: opts.pluginAuthToken,
|
|
29618
|
+
state,
|
|
29619
|
+
driver,
|
|
29620
|
+
conversationId
|
|
29621
|
+
});
|
|
29622
|
+
if (state === "LOCAL_DRIVE" && conversationId) startMirror(conversationId);
|
|
29623
|
+
else if (state !== "LOCAL_DRIVE") mirror?.stop();
|
|
29624
|
+
}
|
|
29625
|
+
});
|
|
29626
|
+
const dispatchActive = (cmd) => controller.activeSessionDriver.dispatch(cmd);
|
|
29627
|
+
relay = new CommandRelayService(
|
|
29628
|
+
opts.pluginId,
|
|
29629
|
+
makeOnCommand({
|
|
29630
|
+
controller,
|
|
29631
|
+
dispatchActive,
|
|
29632
|
+
ack: (id, status2, result) => relay.sendResult(id, status2, result)
|
|
29633
|
+
}),
|
|
29634
|
+
runtime.meta
|
|
29635
|
+
);
|
|
29636
|
+
let torn = false;
|
|
29637
|
+
function teardown() {
|
|
29638
|
+
if (torn) return;
|
|
29639
|
+
torn = true;
|
|
29640
|
+
process.removeListener("SIGINT", onSignal);
|
|
29641
|
+
process.removeListener("SIGTERM", onSignal);
|
|
29642
|
+
process.removeListener("SIGHUP", onSignal);
|
|
29643
|
+
mirror?.stop();
|
|
29644
|
+
relay.stop();
|
|
29645
|
+
void controller.shutdown();
|
|
29646
|
+
}
|
|
29647
|
+
const onSignal = () => {
|
|
29648
|
+
teardown();
|
|
29649
|
+
process.exit(0);
|
|
29650
|
+
};
|
|
29651
|
+
process.once("SIGINT", onSignal);
|
|
29652
|
+
process.once("SIGTERM", onSignal);
|
|
29653
|
+
process.once("SIGHUP", onSignal);
|
|
29654
|
+
showInfo(`Starting ${opts.agent} baton (local) \u2014 native TUI + mobile take-control\u2026`);
|
|
29655
|
+
await controller.begin();
|
|
29656
|
+
relay.start();
|
|
29657
|
+
showSuccess(`${opts.agent} baton online \u2014 you're driving locally; mobile can take control.`);
|
|
29658
|
+
showRelayNotice();
|
|
29659
|
+
await new Promise(() => {
|
|
29660
|
+
});
|
|
29661
|
+
}
|
|
29662
|
+
function toEpochMs(ts) {
|
|
29663
|
+
const asNum = Number(ts);
|
|
29664
|
+
if (Number.isFinite(asNum) && asNum > 0) return asNum;
|
|
29665
|
+
const parsed = Date.parse(ts);
|
|
29666
|
+
return Number.isFinite(parsed) ? parsed : Date.now();
|
|
29667
|
+
}
|
|
29668
|
+
|
|
29669
|
+
// src/agents/claude/onboarding.ts
|
|
29670
|
+
var fs59 = __toESM(require("fs"));
|
|
29022
29671
|
var os45 = __toESM(require("os"));
|
|
29023
29672
|
var path62 = __toESM(require("path"));
|
|
29024
29673
|
function ensureClaudeOnboarded() {
|
|
@@ -29026,7 +29675,7 @@ function ensureClaudeOnboarded() {
|
|
|
29026
29675
|
const file = path62.join(os45.homedir(), ".claude.json");
|
|
29027
29676
|
let config = {};
|
|
29028
29677
|
try {
|
|
29029
|
-
config = JSON.parse(
|
|
29678
|
+
config = JSON.parse(fs59.readFileSync(file, "utf8"));
|
|
29030
29679
|
} catch {
|
|
29031
29680
|
}
|
|
29032
29681
|
if (config.hasCompletedOnboarding === true && typeof config.theme === "string") {
|
|
@@ -29037,8 +29686,8 @@ function ensureClaudeOnboarded() {
|
|
|
29037
29686
|
if (typeof config.lastOnboardingVersion !== "string") {
|
|
29038
29687
|
config.lastOnboardingVersion = "2.1.177";
|
|
29039
29688
|
}
|
|
29040
|
-
|
|
29041
|
-
|
|
29689
|
+
fs59.mkdirSync(path62.dirname(file), { recursive: true });
|
|
29690
|
+
fs59.writeFileSync(file, JSON.stringify(config, null, 2));
|
|
29042
29691
|
log.info("claude", "pre-completed Claude onboarding (skip first-run theme picker)");
|
|
29043
29692
|
} catch (err) {
|
|
29044
29693
|
log.warn("claude", `ensureClaudeOnboarded failed (non-fatal): ${err.message}`);
|
|
@@ -29162,6 +29811,22 @@ async function start(requestedAgent) {
|
|
|
29162
29811
|
`agent-spawn gate released \u2014 beads ${beads ? "ready" : "pending"}; project deps provisioned`
|
|
29163
29812
|
);
|
|
29164
29813
|
}
|
|
29814
|
+
if (isLocalSession() && requiresAcp(session.agent)) {
|
|
29815
|
+
const adapter = getAcpAdapter(session.agent);
|
|
29816
|
+
if (adapter && session.pluginAuthToken) {
|
|
29817
|
+
await runBatonSession({
|
|
29818
|
+
agent: session.agent,
|
|
29819
|
+
sessionId: session.id,
|
|
29820
|
+
pluginId,
|
|
29821
|
+
pluginAuthToken: session.pluginAuthToken,
|
|
29822
|
+
pollSecret: session.pollSecret,
|
|
29823
|
+
cwd,
|
|
29824
|
+
adapter,
|
|
29825
|
+
getBeads
|
|
29826
|
+
});
|
|
29827
|
+
return;
|
|
29828
|
+
}
|
|
29829
|
+
}
|
|
29165
29830
|
if (requiresAcp(session.agent)) {
|
|
29166
29831
|
const adapter = getAcpAdapter(session.agent);
|
|
29167
29832
|
if (!adapter || !session.pluginAuthToken) {
|
|
@@ -31739,8 +32404,8 @@ async function invite() {
|
|
|
31739
32404
|
// src/commands/doctor.ts
|
|
31740
32405
|
var import_node_dns = require("dns");
|
|
31741
32406
|
var import_node_util5 = require("util");
|
|
31742
|
-
var
|
|
31743
|
-
var
|
|
32407
|
+
var import_node_crypto9 = require("crypto");
|
|
32408
|
+
var fs60 = __toESM(require("fs"));
|
|
31744
32409
|
var path67 = __toESM(require("path"));
|
|
31745
32410
|
var import_picocolors14 = __toESM(require("picocolors"));
|
|
31746
32411
|
var dnsResolveP = (0, import_node_util5.promisify)(import_node_dns.resolve);
|
|
@@ -31799,11 +32464,11 @@ async function checkHealth(apiBase2) {
|
|
|
31799
32464
|
function checkConfigDir() {
|
|
31800
32465
|
const dir = path67.join(require("os").homedir(), ".codeam");
|
|
31801
32466
|
try {
|
|
31802
|
-
|
|
32467
|
+
fs60.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
31803
32468
|
const probe = path67.join(dir, ".doctor-probe");
|
|
31804
|
-
|
|
31805
|
-
const read2 =
|
|
31806
|
-
|
|
32469
|
+
fs60.writeFileSync(probe, "ok", { mode: 384 });
|
|
32470
|
+
const read2 = fs60.readFileSync(probe, "utf8");
|
|
32471
|
+
fs60.unlinkSync(probe);
|
|
31807
32472
|
if (read2 !== "ok") throw new Error("write/read round-trip mismatch");
|
|
31808
32473
|
return {
|
|
31809
32474
|
id: "config-dir",
|
|
@@ -31909,9 +32574,9 @@ function checkChokidar() {
|
|
|
31909
32574
|
}
|
|
31910
32575
|
async function doctor(args2 = []) {
|
|
31911
32576
|
const json = args2.includes("--json");
|
|
31912
|
-
const cliVersion = true ? "2.
|
|
32577
|
+
const cliVersion = true ? "2.60.1" : "0.0.0-dev";
|
|
31913
32578
|
const apiBase2 = resolveApiBaseUrl();
|
|
31914
|
-
const diagnosticId = (0,
|
|
32579
|
+
const diagnosticId = (0, import_node_crypto9.randomUUID)();
|
|
31915
32580
|
log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
|
|
31916
32581
|
const [dns, health] = await Promise.all([
|
|
31917
32582
|
checkDns(apiBase2),
|
|
@@ -32108,7 +32773,7 @@ async function completion(args2) {
|
|
|
32108
32773
|
// src/commands/version.ts
|
|
32109
32774
|
var import_picocolors15 = __toESM(require("picocolors"));
|
|
32110
32775
|
function version2() {
|
|
32111
|
-
const v = true ? "2.
|
|
32776
|
+
const v = true ? "2.60.1" : "unknown";
|
|
32112
32777
|
console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
|
|
32113
32778
|
}
|
|
32114
32779
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeam-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.60.1",
|
|
4
4
|
"description": "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device — async. The terminal companion for CodeAgent Mobile.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/index.js",
|