handmux 0.29.2 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connectors/claude/index.js +33 -562
- package/dist/connectors/codebuddy/index.js +49 -0
- package/dist/connectors/hookBridge.js +560 -0
- package/dist/hooks/handmux-codebuddy-notify.sh +116 -0
- package/dist/hooks/handmux-notify.sh +2 -0
- package/dist/hooks/handmux-write.cjs +72 -5
- package/dist/package.json +13 -13
- package/dist/public/assets/_basePickBy-BzC0P9Wg.js +1 -0
- package/dist/public/assets/_baseUniq-DNOfPYVY.js +1 -0
- package/dist/public/assets/abnfDiagram-O67JEVCF-BsRDIQEB.js +1 -0
- package/dist/public/assets/arc-DLtLfMnX.js +1 -0
- package/dist/public/assets/architectureDiagram-NJMV4G6O-DoIXhWqZ.js +36 -0
- package/dist/public/assets/blockDiagram-BEXU5L5S-W0HiOcVA.js +140 -0
- package/dist/public/assets/c4Diagram-YGBWAQC7-72uAUMvX.js +38 -0
- package/dist/public/assets/channel-CzALSnb0.js +1 -0
- package/dist/public/assets/chunk-2Q5K7J3B-B9wSRFQ0.js +1 -0
- package/dist/public/assets/chunk-3FUC2YCW-ryphp4s0.js +2 -0
- package/dist/public/assets/chunk-5VM5RSS4-Cm4KeKby.js +15 -0
- package/dist/public/assets/chunk-GTNCS2PH-Du4_i7r3.js +88 -0
- package/dist/public/assets/chunk-GWA4HPMP-CL1biVIA.js +1 -0
- package/dist/public/assets/chunk-JWPE2WC7-B8eAVfuB.js +1 -0
- package/dist/public/assets/chunk-NETBCI7D-D273GzYg.js +1 -0
- package/dist/public/assets/chunk-WEXAMYUT-DipTfRbc.js +1 -0
- package/dist/public/assets/chunk-XXDRQBXY-D4SqXrQ8.js +1 -0
- package/dist/public/assets/classDiagram-v2-NBCMYWYE-PrpnIXcA.js +217 -0
- package/dist/public/assets/cose-bilkent-JH36ORCC-BfcYMYTa.js +1 -0
- package/dist/public/assets/cynefin-OW5HDTMX-Du659bmz.js +178 -0
- package/dist/public/assets/cynefinDiagram-VND7K2PF-DR_2Copy.js +62 -0
- package/dist/public/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/dist/public/assets/dagre-6A5THRUB-COsChaV5.js +4 -0
- package/dist/public/assets/defaultLocale-CrowFXzY.js +1 -0
- package/dist/public/assets/diagram-22UHCM2B-Cx2T1HvL.js +200 -0
- package/dist/public/assets/diagram-3UASUU5V-Cp04LOka.js +24 -0
- package/dist/public/assets/diagram-ATOU4E4O-DlqFSshY.js +3 -0
- package/dist/public/assets/diagram-CDSNMT55-goq40ctA.js +30 -0
- package/dist/public/assets/diagram-MLGK6HIB-Bdzjsy4r.js +24 -0
- package/dist/public/assets/diagram-MPIPVDR6-DtU7cM6P.js +41 -0
- package/dist/public/assets/ebnfDiagram-ZINNZB2B-XyVCfmED.js +1 -0
- package/dist/public/assets/elk-276RUBZZ-aHVIw-2x.js +27 -0
- package/dist/public/assets/erDiagram-OPXOYQCR-Bq_PhTIL.js +99 -0
- package/dist/public/assets/flowDiagram-KWPJA3E3-WuqnDSiF.js +213 -0
- package/dist/public/assets/ganttDiagram-FUAMR5RP-DZ4lXzkC.js +292 -0
- package/dist/public/assets/gitGraphDiagram-X574FWY7-t6l07b9g.js +106 -0
- package/dist/public/assets/graph-jWsD5Ro-.js +1 -0
- package/dist/public/assets/index-DiYff8oR.css +32 -0
- package/dist/public/assets/index-YZNK723o.js +203 -0
- package/dist/public/assets/infoDiagram-VRGFBTTK-iUGqoOFp.js +2 -0
- package/dist/public/assets/init-Gi6I4Gst.js +1 -0
- package/dist/public/assets/ishikawaDiagram-OU5B5YK6-B0TmoUOK.js +70 -0
- package/dist/public/assets/journeyDiagram-ZHPQQLJL-CQE61MCu.js +139 -0
- package/dist/public/assets/kanban-definition-PNTS6WVX-D2MWCUyS.js +89 -0
- package/dist/public/assets/katex-C5jXJg4s.js +257 -0
- package/dist/public/assets/linear-WKGxVNP1.js +1 -0
- package/dist/public/assets/mermaid.core-DPEwUsaj.js +311 -0
- package/dist/public/assets/mindmap-definition-NLK3R4M7-DsYEsBsI.js +96 -0
- package/dist/public/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/public/assets/pegDiagram-GJSIUBJH-BS52Fbxt.js +1 -0
- package/dist/public/assets/pieDiagram-5QR66LMP-D4o8g7nW.js +39 -0
- package/dist/public/assets/quadrantDiagram-O4NWA36T-DnYUxWll.js +7 -0
- package/dist/public/assets/railroadDiagram-XR7U4H2S-BKy_7ULV.js +1 -0
- package/dist/public/assets/requirementDiagram-PLB6GJNP-Ce5F4u8K.js +84 -0
- package/dist/public/assets/sankeyDiagram-IPEJSGJF-CCJDrut_.js +40 -0
- package/dist/public/assets/sequenceDiagram-PO4LG4MO-D5E8ISM1.js +169 -0
- package/dist/public/assets/sizeCapture-INFHLROL-BW3KmxDN.js +1 -0
- package/dist/public/assets/stateDiagram-v2-GCMORJYK-Bw2ZzQOE.js +291 -0
- package/dist/public/assets/swimlanes-2SLR337P-CLJ-6Ex_.js +1 -0
- package/dist/public/assets/swimlanesDiagram-TC7HE7FX-BD8rQfV7.js +8 -0
- package/dist/public/assets/timeline-definition-EJHVYXUP-C_lFJ-4W.js +120 -0
- package/dist/public/assets/usecaseDiagram-POWQR4AR-DWXawL2A.js +370 -0
- package/dist/public/assets/vennDiagram-UO4OBE2U-D4O5oZal.js +34 -0
- package/dist/public/assets/wardleyDiagram-VNRHLVJA-B6HElk__.js +78 -0
- package/dist/public/assets/xychartDiagram-PMCCYNJV-C5OAXnJy.js +7 -0
- package/dist/public/index.html +2 -2
- package/dist/src/agent-runtime/adapter.js +54 -10
- package/dist/src/agent-runtime/builtinRuntime.js +67 -6
- package/dist/src/agent-runtime/conversation.js +49 -9
- package/dist/src/agent-runtime/interaction.js +26 -12
- package/dist/src/agent-runtime/interactionLiveHub.js +4 -0
- package/dist/src/agent-runtime/runtime.js +83 -27
- package/dist/src/agent-runtime/tmuxRuntime.js +36 -6
- package/dist/src/agents/claude.js +2 -19
- package/dist/src/agents/claudeInteraction.js +2 -132
- package/dist/src/agents/claudePaneInput.js +22 -22
- package/dist/src/agents/codebuddy.js +100 -0
- package/dist/src/agents/codebuddyConversation.js +414 -0
- package/dist/src/agents/codebuddyInteraction.js +4 -0
- package/dist/src/agents/codebuddyNativeTail.js +206 -0
- package/dist/src/agents/codebuddyPaneInput.js +76 -0
- package/dist/src/agents/codebuddyTranscriptParse.js +178 -0
- package/dist/src/agents/hookEvents.js +143 -0
- package/dist/src/agents/hookInteraction.js +178 -0
- package/dist/src/agents/index.js +2 -1
- package/dist/src/claudeEvents.js +4 -5
- package/dist/src/cli/agentIntegration.js +31 -2
- package/dist/src/cli/codebuddyHooks.js +286 -0
- package/dist/src/cli/hookScaffold.js +25 -12
- package/dist/src/cli/i18n/en.js +1 -1
- package/dist/src/cli/i18n/zh.js +1 -1
- package/dist/src/cli/state.js +4 -0
- package/dist/src/cli/supervisor.js +2 -1
- package/dist/src/codebuddyEvents.js +151 -0
- package/dist/src/docs.js +21 -7
- package/dist/src/logDetail.js +27 -0
- package/dist/src/paneInput.js +107 -10
- package/dist/src/pendingPrompt.js +21 -5
- package/dist/src/routes/files.js +4 -1
- package/dist/src/routes/system.js +12 -2
- package/dist/src/server.js +69 -8
- package/dist/src/trimCapture.js +1 -1
- package/dist/src/workspace/checkpointer.js +48 -4
- package/dist/src/workspace/lock.js +54 -10
- package/dist/src/workspace/runtime.js +23 -8
- package/dist/src/workspaceProtocol.js +1 -0
- package/package.json +4 -4
- package/dist/public/assets/index-2ghkmGbT.js +0 -356
- package/dist/public/assets/index-BjnnFTLC.css +0 -32
|
@@ -1,134 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { parsePendingPrompt } from '../pendingPrompt.js';
|
|
3
|
-
function normalizedPrompt(text, pendingKind) {
|
|
4
|
-
const prompt = parsePendingPrompt(text);
|
|
5
|
-
if (!prompt) {
|
|
6
|
-
if (pendingKind !== 'permission')
|
|
7
|
-
return null;
|
|
8
|
-
const tail = text.split('\n').map((line) => line.trim()).filter(Boolean).slice(-3).join('\n');
|
|
9
|
-
const promptText = tail.slice(0, 2_000) || 'Claude is waiting for permission in the terminal.';
|
|
10
|
-
const id = `claude-permission:${createHash('sha256').update(promptText).digest('hex').slice(0, 24)}`;
|
|
11
|
-
return {
|
|
12
|
-
id, type: 'local_only', prompt: promptText,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
const signature = JSON.stringify(prompt);
|
|
16
|
-
const id = `claude-prompt:${createHash('sha256').update(signature).digest('hex').slice(0, 24)}`;
|
|
17
|
-
return {
|
|
18
|
-
id,
|
|
19
|
-
type: 'select',
|
|
20
|
-
prompt: prompt.leadIn ? `${prompt.leadIn}\n${prompt.title}` : prompt.title,
|
|
21
|
-
options: prompt.options.map((option) => ({
|
|
22
|
-
id: `choice:${option.n}`,
|
|
23
|
-
label: option.description ? `${option.label} — ${option.description}` : option.label,
|
|
24
|
-
})),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
1
|
+
import { createHookInteractionAdapter } from './hookInteraction.js';
|
|
27
2
|
export function createClaudeInteractionAdapter(control, pollMs = 750, reportHealth = () => { }) {
|
|
28
|
-
|
|
29
|
-
|| typeof control.sendChoice !== 'function' || !Number.isFinite(pollMs) || pollMs <= 0) {
|
|
30
|
-
throw new TypeError('Claude Interaction adapter requires pane capture and choice control');
|
|
31
|
-
}
|
|
32
|
-
const active = new Map();
|
|
33
|
-
return {
|
|
34
|
-
apiVersion: 1,
|
|
35
|
-
async observeNative(run, sink) {
|
|
36
|
-
let closed = false;
|
|
37
|
-
let cursor = 0;
|
|
38
|
-
let pending = normalizedPrompt(await control.capturePlain(run.ref.paneId), control.pendingKind?.(run.ref.paneId));
|
|
39
|
-
let timer;
|
|
40
|
-
let tail = Promise.resolve();
|
|
41
|
-
let failures = 0;
|
|
42
|
-
const binding = { run, pending };
|
|
43
|
-
active.set(run.ref.runId, binding);
|
|
44
|
-
const close = () => {
|
|
45
|
-
closed = true;
|
|
46
|
-
if (timer)
|
|
47
|
-
clearTimeout(timer);
|
|
48
|
-
timer = undefined;
|
|
49
|
-
if (active.get(run.ref.runId) === binding)
|
|
50
|
-
active.delete(run.ref.runId);
|
|
51
|
-
};
|
|
52
|
-
const schedule = (delay = pollMs) => {
|
|
53
|
-
if (closed || run.signal.aborted)
|
|
54
|
-
return;
|
|
55
|
-
timer = setTimeout(() => {
|
|
56
|
-
tail = tail.then(async () => {
|
|
57
|
-
if (closed || run.signal.aborted)
|
|
58
|
-
return;
|
|
59
|
-
const next = normalizedPrompt(await control.capturePlain(run.ref.paneId), control.pendingKind?.(run.ref.paneId));
|
|
60
|
-
if (failures > 0)
|
|
61
|
-
reportHealth('ready');
|
|
62
|
-
failures = 0;
|
|
63
|
-
if (pending?.id === next?.id)
|
|
64
|
-
return;
|
|
65
|
-
if (pending)
|
|
66
|
-
await sink({
|
|
67
|
-
type: 'resolved', sourceCursor: `claude-interaction:${++cursor}`,
|
|
68
|
-
interactionId: pending.id,
|
|
69
|
-
});
|
|
70
|
-
pending = next;
|
|
71
|
-
binding.pending = next;
|
|
72
|
-
if (next)
|
|
73
|
-
await sink({
|
|
74
|
-
type: 'opened', sourceCursor: `claude-interaction:${++cursor}`, interaction: next,
|
|
75
|
-
});
|
|
76
|
-
}).catch(async () => {
|
|
77
|
-
failures += 1;
|
|
78
|
-
if (failures >= 2) {
|
|
79
|
-
reportHealth('degraded', 'Interaction polling is temporarily unavailable');
|
|
80
|
-
if (pending) {
|
|
81
|
-
const abandoned = pending;
|
|
82
|
-
pending = null;
|
|
83
|
-
binding.pending = null;
|
|
84
|
-
await Promise.resolve(sink({
|
|
85
|
-
type: 'cancelled', sourceCursor: `claude-interaction:${++cursor}`,
|
|
86
|
-
interactionId: abandoned.id, reason: 'temporarily_unavailable',
|
|
87
|
-
})).catch(close);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}).finally(() => schedule(Math.min(10_000, pollMs * (2 ** Math.min(failures, 4)))));
|
|
91
|
-
}, delay);
|
|
92
|
-
timer.unref?.();
|
|
93
|
-
};
|
|
94
|
-
const onAbort = () => close();
|
|
95
|
-
run.signal.addEventListener('abort', onAbort, { once: true });
|
|
96
|
-
schedule();
|
|
97
|
-
return {
|
|
98
|
-
checkpoint: {
|
|
99
|
-
sourceCursor: `claude-interaction:${cursor}`,
|
|
100
|
-
pending: pending ? [pending] : [],
|
|
101
|
-
},
|
|
102
|
-
close() {
|
|
103
|
-
run.signal.removeEventListener('abort', onAbort);
|
|
104
|
-
close();
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
},
|
|
108
|
-
async dispatchResponse(run, request) {
|
|
109
|
-
const binding = active.get(run.ref.runId);
|
|
110
|
-
if (!binding || binding.run !== run || run.signal.aborted)
|
|
111
|
-
return { status: 'stale_run' };
|
|
112
|
-
if (!binding.pending || binding.pending.id !== request.interactionId) {
|
|
113
|
-
return { status: 'already_resolved' };
|
|
114
|
-
}
|
|
115
|
-
const optionIds = request.value.type === 'selection' ? request.value.optionIds
|
|
116
|
-
: request.value.type === 'approval' ? [request.value.optionId] : [];
|
|
117
|
-
if (optionIds.length !== 1) {
|
|
118
|
-
return { status: 'rejected', reason: 'invalid_value' };
|
|
119
|
-
}
|
|
120
|
-
const optionId = optionIds[0];
|
|
121
|
-
const match = optionId.match(/^choice:(\d+)$/);
|
|
122
|
-
if (!match || !binding.pending.options?.some((option) => option.id === optionId)) {
|
|
123
|
-
return { status: 'rejected', reason: 'invalid_value' };
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
await control.sendChoice(run.ref.paneId, match[1]);
|
|
127
|
-
return { status: 'accepted' };
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return { status: 'unknown', reason: 'temporarily_unavailable' };
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
};
|
|
3
|
+
return createHookInteractionAdapter(control, { id: 'claude', label: 'Claude' }, pollMs, reportHealth);
|
|
134
4
|
}
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { serializePaneInput } from '../paneInput.js';
|
|
1
|
+
import { describeEditorArea, serializePaneInput, singleLineDraft } from '../paneInput.js';
|
|
2
2
|
const SUBMIT_GAP_MS = 120;
|
|
3
3
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
|
-
//
|
|
5
|
-
// selection menus and unknown layouts fail closed. End is sent first: placeholder text is painted
|
|
6
|
-
// after the cursor but isn't part of the editor; actual text places the cursor at its end.
|
|
4
|
+
// Claude's editor is the shared one-line shape, with `❯` as its prompt.
|
|
7
5
|
export function claudeSingleLineDraft(screen, cursor) {
|
|
8
|
-
|
|
9
|
-
const line = lines[cursor.cursorY];
|
|
10
|
-
if (!line || !/^─{3,}$/.test(lines[cursor.cursorY - 1]?.trim() ?? '')
|
|
11
|
-
|| !/^─{3,}$/.test(lines[cursor.cursorY + 1]?.trim() ?? '')
|
|
12
|
-
|| !/^❯[ \u00a0]/.test(line))
|
|
13
|
-
return null;
|
|
14
|
-
const value = line.slice(2).trimEnd();
|
|
15
|
-
if (cursor.cursorX === 2)
|
|
16
|
-
return ''; // Native suggestions are painted here but End does not enter them.
|
|
17
|
-
if (!value || /[\x00-\x1f\x7f]/.test(value) || cursor.cursorX <= 2)
|
|
18
|
-
return null;
|
|
19
|
-
return value;
|
|
6
|
+
return singleLineDraft(screen, cursor);
|
|
20
7
|
}
|
|
21
|
-
export function sendClaudePanePrompt(commands, paneId, text, restoredPrompt, guard) {
|
|
8
|
+
export function sendClaudePanePrompt(commands, paneId, text, restoredPrompt, guard, reportUnreadable) {
|
|
22
9
|
return serializePaneInput(paneId, async () => {
|
|
23
10
|
if (guard && !await guard.validate())
|
|
24
11
|
return { nativeMutation: false };
|
|
25
12
|
await commands.exitCopyModeIfActive(paneId);
|
|
26
13
|
await commands.sendKey(paneId, 'End');
|
|
27
14
|
await delay(SUBMIT_GAP_MS);
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
15
|
+
const readEditor = async () => {
|
|
16
|
+
const screen = await commands.capturePlain(paneId);
|
|
17
|
+
const cursor = await commands.paneInfo(paneId);
|
|
18
|
+
return { screen, cursor, draft: claudeSingleLineDraft(screen, cursor) };
|
|
19
|
+
};
|
|
20
|
+
const draft = await readEditor();
|
|
21
|
+
if (draft.draft === null) {
|
|
22
|
+
// Unreadable: record the shape, never the text we could not read (see describeEditorArea).
|
|
23
|
+
reportUnreadable?.(describeEditorArea(draft.screen, draft.cursor));
|
|
24
|
+
return { nativeMutation: false, reason: 'terminal_draft_conflict' };
|
|
25
|
+
}
|
|
26
|
+
if (draft.draft !== '' && draft.draft !== restoredPrompt()) {
|
|
31
27
|
return { nativeMutation: false, reason: 'terminal_draft_conflict' };
|
|
32
28
|
}
|
|
33
29
|
if (guard && !await guard.validate())
|
|
34
30
|
return { nativeMutation: false };
|
|
35
|
-
if (draft) {
|
|
31
|
+
if (draft.draft) {
|
|
36
32
|
await commands.sendKey(paneId, 'C-u');
|
|
37
33
|
await delay(SUBMIT_GAP_MS);
|
|
38
|
-
|
|
34
|
+
const cleared = await readEditor();
|
|
35
|
+
if (cleared.draft !== '') {
|
|
36
|
+
if (cleared.draft === null)
|
|
37
|
+
reportUnreadable?.(describeEditorArea(cleared.screen, cleared.cursor));
|
|
39
38
|
return { nativeMutation: false, reason: 'terminal_draft_conflict' };
|
|
39
|
+
}
|
|
40
40
|
}
|
|
41
41
|
await commands.sendText(paneId, text);
|
|
42
42
|
if (text)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// The CodeBuddy Code agent adapter. Everything here is what Handmux knows that is SPECIFIC to CodeBuddy;
|
|
2
|
+
// the shared engine consumes this descriptor like any other provider.
|
|
3
|
+
//
|
|
4
|
+
// CodeBuddy is npm-first. `codebuddy`, `codebuddy-code` and `cbc` are symlinks onto one bin/codebuddy entry,
|
|
5
|
+
// so tmux only reports a stable name for the native build. In the documented npm layout the CLI runs INSIDE
|
|
6
|
+
// a Node process (`node /usr/local/bin/codebuddy` — verified live on 2.154.0), which makes three of the usual
|
|
7
|
+
// identity routes unusable:
|
|
8
|
+
// - `#{pane_current_command}` is a bare "node", so no exact command can ever own the pane;
|
|
9
|
+
// - the process's real executable (lsof/proc) is the Node binary, so executable-backed proof — the Claude
|
|
10
|
+
// and Codex route — cannot name CodeBuddy;
|
|
11
|
+
// - the Agent never spawns a native child binary, so the single-leaf foreground view, which deliberately
|
|
12
|
+
// prefers a non-launcher descendant for ambiguous launchers, can hand back a transient tool child
|
|
13
|
+
// (an npm/ruby update check) instead of the Agent itself.
|
|
14
|
+
// Identity therefore comes from the pane's whole foreground group, matched at a path boundary against the
|
|
15
|
+
// published launch entry points. We only ever answer "yes" from positive evidence, and we stay inconclusive
|
|
16
|
+
// rather than negative when the group was not actually read.
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
// The three entries the CodeBuddy CLI installs, plus the package that owns the npm layout.
|
|
20
|
+
const LAUNCH_NAMES = new Set(['codebuddy', 'codebuddy-code', 'cbc']);
|
|
21
|
+
const NPM_PACKAGE_MARKER = '/@tencent-ai/codebuddy-code/';
|
|
22
|
+
const NODE_PROGRAM_RE = /^(\S*\/)?node$/;
|
|
23
|
+
const basename = (value) => value.split('/').at(-1) ?? '';
|
|
24
|
+
// CodeBuddy's own on-disk layout, in one place because three readers need the same answer: the conversation
|
|
25
|
+
// adapter reads a session's JSONL, the native tail resolves a pane's session from its registry row, and the
|
|
26
|
+
// session binding reads the hook state file. A session log is
|
|
27
|
+
// `<projects>/<cwd with its slashes replaced by dashes>/<session-id>.jsonl`; a running session publishes
|
|
28
|
+
// `~/.codebuddy/sessions/<pid>.json` (pid, sessionId, cwd, heartbeat — never a status).
|
|
29
|
+
export const codebuddyProjectsDir = (home = os.homedir()) => path.join(home, '.codebuddy', 'projects');
|
|
30
|
+
export const codebuddySessionsDir = (home = os.homedir()) => path.join(home, '.codebuddy', 'sessions');
|
|
31
|
+
// Node's entry argument is the first non-flag token after the program. Nothing beyond it may be searched:
|
|
32
|
+
// the agent's own arguments (a prompt, a file path, a session id) are free to contain any word, and matching
|
|
33
|
+
// those would turn "some Node script that was passed the word codebuddy" into a false identity.
|
|
34
|
+
function nodeEntryArgument(rest) {
|
|
35
|
+
return rest.split(/\s+/).find((token) => token.length > 0 && !token.startsWith('-')) ?? '';
|
|
36
|
+
}
|
|
37
|
+
// Exported for the contract tests that freeze the accepted launch shapes.
|
|
38
|
+
export function isCodeBuddyCommandLine(value) {
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
if (!trimmed)
|
|
41
|
+
return false;
|
|
42
|
+
const segments = trimmed.split(/\s+/);
|
|
43
|
+
const program = (segments[0] ?? '').replace(/^-/, '');
|
|
44
|
+
// Native build: the launcher itself is the program token.
|
|
45
|
+
if (LAUNCH_NAMES.has(basename(program)))
|
|
46
|
+
return true;
|
|
47
|
+
// npm / Node launcher: `node <entry> …`. The entry must carry one of the published names AND sit in a
|
|
48
|
+
// recognised install layout — a bare `node codebuddy`, or an unrelated file that merely shares the name,
|
|
49
|
+
// is not proof.
|
|
50
|
+
if (!NODE_PROGRAM_RE.test(program))
|
|
51
|
+
return false;
|
|
52
|
+
const entry = nodeEntryArgument(segments.slice(1).join(' '));
|
|
53
|
+
if (!entry || !LAUNCH_NAMES.has(basename(entry)))
|
|
54
|
+
return false;
|
|
55
|
+
return entry.includes(NPM_PACKAGE_MARKER) || /(?:^|\/)(?:\.bin|bin)\//.test(entry);
|
|
56
|
+
}
|
|
57
|
+
// Returns the matched row rather than a bare `true`: that row is CodeBuddy's process, and the Runtime needs
|
|
58
|
+
// it as the lease anchor precisely because the pane's foreground leaf is NOT CodeBuddy. A row whose start time
|
|
59
|
+
// could not be resolved is still returned — the Runtime then declines to publish a lease until it can name a
|
|
60
|
+
// process generation, which is the same rule it applies to every other Agent.
|
|
61
|
+
export async function verifyCodeBuddyProcess(pane, context) {
|
|
62
|
+
const group = await context.inspectForegroundGroup?.(pane);
|
|
63
|
+
// A host that cannot supply the group cannot confirm a CodeBuddy pane (the single-leaf view may return a
|
|
64
|
+
// transient tool child), so CodeBuddy goes unidentified there. That is deliberately a plain `false` and not
|
|
65
|
+
// a thrown error: an inconclusive verdict is contagious — resolveAgentIdentity downgrades the WHOLE pane to
|
|
66
|
+
// `unknown` when any ambiguous candidate is unresolved, which would poison Codex/Pi on those hosts. Every
|
|
67
|
+
// production host builds its ProcessContext from createLocalAgentProcessContext, which does supply the group.
|
|
68
|
+
if (!group)
|
|
69
|
+
return false;
|
|
70
|
+
return group.find((entry) => isCodeBuddyCommandLine(entry.commandLine ?? '')) ?? false;
|
|
71
|
+
}
|
|
72
|
+
export const codebuddy = {
|
|
73
|
+
adapterApiVersion: 1,
|
|
74
|
+
id: 'codebuddy',
|
|
75
|
+
label: 'CodeBuddy',
|
|
76
|
+
process: {
|
|
77
|
+
commands: ['codebuddy', 'codebuddy-code', 'cbc'],
|
|
78
|
+
ambiguousCommands: ['node'],
|
|
79
|
+
// Process presence alone establishes the sessionless run. Beyond being the base every later capability
|
|
80
|
+
// builds on, this is what puts CodeBuddy on the pane roster the phone reads: the per-window Agent badge
|
|
81
|
+
// for windows you are NOT looking at comes from the Runtime's active runs (`/states`), not from pane
|
|
82
|
+
// identity — the pane you have selected is the only one resolved by `identifyPanes`.
|
|
83
|
+
runtimeAttach: true,
|
|
84
|
+
verify: verifyCodeBuddyProcess,
|
|
85
|
+
},
|
|
86
|
+
presentation: { iconId: 'codebuddy' },
|
|
87
|
+
// Only what is actually implemented. Inbox is delivered by CodeBuddy's Claude-compatible Hooks through
|
|
88
|
+
// its own Connector (connectors/codebuddy) + the Bridge durable lane; Conversation (with composer and
|
|
89
|
+
// interrupt, like Claude's) reads the session JSONL through ../../agents/codebuddyConversation.ts;
|
|
90
|
+
// Interaction reads the pending menu off the pane through ../../agents/codebuddyInteraction.ts — its
|
|
91
|
+
// question, review and permission screens were captured from a live 2.156.0 pane and all three parse with
|
|
92
|
+
// the shared parser, with the option digit as the key that drives them.
|
|
93
|
+
// The classification of its `src`→kind events lives in ../../codebuddyEvents.ts, next to the shared
|
|
94
|
+
// Hook vocabulary, because that is the module the Connector consumes.
|
|
95
|
+
capabilities: {
|
|
96
|
+
inbox: { apiVersion: 1 },
|
|
97
|
+
conversation: { apiVersion: 1, experimental: true },
|
|
98
|
+
interaction: { apiVersion: 1 },
|
|
99
|
+
},
|
|
100
|
+
};
|