@beevibe/daemon 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -301
- package/dist/main.js +1076 -70
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -2,8 +2,206 @@
|
|
|
2
2
|
|
|
3
3
|
// src/setup.ts
|
|
4
4
|
import { hostname, userInfo } from "node:os";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
5
|
|
|
6
|
+
// ../../node_modules/.pnpm/nanoid@5.1.9/node_modules/nanoid/index.js
|
|
7
|
+
import { webcrypto as crypto } from "node:crypto";
|
|
8
|
+
var POOL_SIZE_MULTIPLIER = 128;
|
|
9
|
+
var pool;
|
|
10
|
+
var poolOffset;
|
|
11
|
+
function fillPool(bytes) {
|
|
12
|
+
if (!pool || pool.length < bytes) {
|
|
13
|
+
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
|
14
|
+
crypto.getRandomValues(pool);
|
|
15
|
+
poolOffset = 0;
|
|
16
|
+
} else if (poolOffset + bytes > pool.length) {
|
|
17
|
+
crypto.getRandomValues(pool);
|
|
18
|
+
poolOffset = 0;
|
|
19
|
+
}
|
|
20
|
+
poolOffset += bytes;
|
|
21
|
+
}
|
|
22
|
+
function random(bytes) {
|
|
23
|
+
fillPool(bytes |= 0);
|
|
24
|
+
return pool.subarray(poolOffset - bytes, poolOffset);
|
|
25
|
+
}
|
|
26
|
+
function customRandom(alphabet, defaultSize, getRandom) {
|
|
27
|
+
let safeByteCutoff = 256 - 256 % alphabet.length;
|
|
28
|
+
if (safeByteCutoff === 256) {
|
|
29
|
+
let mask = alphabet.length - 1;
|
|
30
|
+
return (size = defaultSize) => {
|
|
31
|
+
if (!size)
|
|
32
|
+
return "";
|
|
33
|
+
let id = "";
|
|
34
|
+
while (true) {
|
|
35
|
+
let bytes = getRandom(size);
|
|
36
|
+
let i = size;
|
|
37
|
+
while (i--) {
|
|
38
|
+
id += alphabet[bytes[i] & mask];
|
|
39
|
+
if (id.length >= size)
|
|
40
|
+
return id;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
let step = Math.ceil(1.6 * 256 * defaultSize / safeByteCutoff);
|
|
46
|
+
return (size = defaultSize) => {
|
|
47
|
+
if (!size)
|
|
48
|
+
return "";
|
|
49
|
+
let id = "";
|
|
50
|
+
while (true) {
|
|
51
|
+
let bytes = getRandom(step);
|
|
52
|
+
let i = step;
|
|
53
|
+
while (i--) {
|
|
54
|
+
if (bytes[i] < safeByteCutoff) {
|
|
55
|
+
id += alphabet[bytes[i] % alphabet.length];
|
|
56
|
+
if (id.length >= size)
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function customAlphabet(alphabet, size = 21) {
|
|
64
|
+
return customRandom(alphabet, size, random);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ../core/dist/domain/ids.js
|
|
68
|
+
var alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
69
|
+
var nanoid12 = customAlphabet(alphabet, 12);
|
|
70
|
+
// ../core/dist/domain/core-memory.js
|
|
71
|
+
var DEFAULT_BLOCK_TEMPLATES = {
|
|
72
|
+
ic: [
|
|
73
|
+
{
|
|
74
|
+
block_name: "tag_line",
|
|
75
|
+
char_limit: 100,
|
|
76
|
+
is_system: true,
|
|
77
|
+
initial_content: "",
|
|
78
|
+
description: "One-line headline of my enduring specialization — shown on agent " + "cards in the UI. Describes what I'm an expert in, not what " + "project I'm currently on. Examples: 'Go backend specialist " + "(Chi/sqlc, websockets)', 'Next.js + React UI lead'. Max 100 " + "chars. Update only when my specialization itself shifts."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
block_name: "persona",
|
|
82
|
+
char_limit: 2000,
|
|
83
|
+
is_system: true,
|
|
84
|
+
initial_content: "",
|
|
85
|
+
description: "Who I am and how I work — my role and working style, persistent " + "across every project I touch. 1-3 sentences in first person. " + "Update when my self-conception genuinely shifts (acquired a " + "major capability, refined my approach). NOT my current project " + "(that's `active_context`), NOT my domain scope (that's `domain`)."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
block_name: "domain",
|
|
89
|
+
char_limit: 2000,
|
|
90
|
+
is_system: true,
|
|
91
|
+
initial_content: "",
|
|
92
|
+
description: "The areas I specialize in ACROSS all projects — my enduring " + "expertise. As I work on more projects in my domain, this can " + "deepen (becoming truly expert in narrower sub-areas). Bullet " + "format. Example: 'Go backend services: HTTP via Chi/echo, DB " + "via sqlc, websockets via gorilla, distribution via goreleaser.' " + "NOT project-specific paths (those go in `active_context`). NOT " + "the rules I follow (those go in `constraints`)."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
block_name: "active_context",
|
|
96
|
+
char_limit: 2000,
|
|
97
|
+
is_system: true,
|
|
98
|
+
initial_content: "",
|
|
99
|
+
description: "What I'm currently working on — the specific project and its " + "in-flight details. Bullet format. Example: 'Project: " + "github.com/multica-ai/multica. Local clone: /tmp/multica-repo. " + "Owned paths in this project: server/cmd/**, server/internal/**. " + "Current task: task_xfQpuEHWjbvk.' Transient — rewrite when the " + "project changes. This is where ALL project/codebase-specific " + "details live, NOT in `domain`."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
block_name: "constraints",
|
|
103
|
+
char_limit: 2000,
|
|
104
|
+
is_system: true,
|
|
105
|
+
initial_content: "",
|
|
106
|
+
description: "Hard rules I follow — non-negotiable conventions and " + "coordination boundaries. Mix of persistent rules ('queries " + "always via sqlc — never hand-write the DB layer') and " + "project-specific rules ('read /CLAUDE.md before changes in " + "this codebase'). Bullet format. Reference docs by path, not " + "content."
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
team: [
|
|
110
|
+
{
|
|
111
|
+
block_name: "tag_line",
|
|
112
|
+
char_limit: 100,
|
|
113
|
+
is_system: true,
|
|
114
|
+
initial_content: "",
|
|
115
|
+
description: "One-line headline of my enduring role — shown on agent cards. " + "Describes the team I lead, not the project we're on. Examples: " + "'Daniel's team — orchestrates 3 backend/frontend/platform " + "specialists', 'Solo lead — driving a small team for hire'. " + "Max 100 chars."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
block_name: "persona",
|
|
119
|
+
char_limit: 2000,
|
|
120
|
+
is_system: true,
|
|
121
|
+
initial_content: "",
|
|
122
|
+
description: "Who I am as a team lead — my orchestration style and how I " + "delegate. 1-3 sentences in first person. Persistent across " + "projects. NOT my team roster (that's `team_members`), NOT the " + "current work (that's `active_work`)."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
block_name: "team_members",
|
|
126
|
+
char_limit: 3000,
|
|
127
|
+
is_system: true,
|
|
128
|
+
initial_content: "",
|
|
129
|
+
description: "Roster of my direct reports — for each: name, agent_id, " + "specialization (NOT project assignment — same agents stay over " + "time as we work different projects). Bullet format. Update " + "when subordinates are spawned/archived/reassigned."
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
block_name: "active_work",
|
|
133
|
+
char_limit: 2000,
|
|
134
|
+
is_system: true,
|
|
135
|
+
initial_content: "",
|
|
136
|
+
description: "What my team is currently working on — the active project + " + "high-level work in flight across specialists. Bullet format. " + "Example: 'Project: github.com/multica-ai/multica. Backend " + "specialist running CLI audit (task_xfQpuEHWjbvk); Frontend on " + "standby.' Transient — rewrite on project shifts."
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
block_name: "patterns",
|
|
140
|
+
char_limit: 2000,
|
|
141
|
+
is_system: true,
|
|
142
|
+
initial_content: "",
|
|
143
|
+
description: "Cross-project patterns I've observed in how my team operates — " + "what works, what trips them up. Persistent. Example: 'When I " + "assign an audit task, the IC tends to produce thorough work " + "products but forgets to save_memory mid-pass.' NOT specific " + "findings about a codebase (those go in archival memory via " + "save_memory)."
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
org: [
|
|
147
|
+
{
|
|
148
|
+
block_name: "tag_line",
|
|
149
|
+
char_limit: 100,
|
|
150
|
+
is_system: true,
|
|
151
|
+
initial_content: "",
|
|
152
|
+
description: "One-line headline of my org-level role — shown on agent cards. " + "Describes the scope I oversee, not the current project. " + "Examples: 'Eng org lead — 3 teams (product/platform/infra)'. " + "Max 100 chars."
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
block_name: "persona",
|
|
156
|
+
char_limit: 2000,
|
|
157
|
+
is_system: true,
|
|
158
|
+
initial_content: "",
|
|
159
|
+
description: "Who I am as an org leader — my decision style and how I balance " + "across teams. 1-3 sentences. Persistent."
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
block_name: "teams",
|
|
163
|
+
char_limit: 3000,
|
|
164
|
+
is_system: true,
|
|
165
|
+
initial_content: "",
|
|
166
|
+
description: "Teams under my oversight — for each: name, team-lead agent_id, " + "scope. Persistent identity. Bullet format."
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
block_name: "strategy",
|
|
170
|
+
char_limit: 2000,
|
|
171
|
+
is_system: true,
|
|
172
|
+
initial_content: "",
|
|
173
|
+
description: "Cross-project / cross-team direction I'm driving. Higher-level " + "than active_work. Examples: 'Q2 focus: ship Multica self-host " + "v1', 'Hiring: prioritize backend over frontend this quarter'."
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
block_name: "decisions",
|
|
177
|
+
char_limit: 2000,
|
|
178
|
+
is_system: true,
|
|
179
|
+
initial_content: "",
|
|
180
|
+
description: "Cross-team decisions I've resolved — bullet log. Each entry: " + "what was decided, when, why. Persistent record so the same " + "question doesn't come back up."
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
// ../core/dist/domain/memory.js
|
|
185
|
+
var FACT_TYPE_DESCRIPTIONS = {
|
|
186
|
+
belief: "A position you hold based on multiple sessions of evidence. A lasting view, " + "not a fleeting reaction to one session.",
|
|
187
|
+
pattern: "A recurring observation about the codebase or the domain you work in — " + "knowledge another agent could reuse. NOT a pattern about your own behavior " + "(your search habits, your memory-keeping, how you should have responded next " + "time). Save the thing you learned about the world, not a note-to-self about " + "how to behave.",
|
|
188
|
+
gotcha: "A non-obvious thing-that-bites — a footgun that's easy to step on, where the " + "surprise itself is the value. Concrete and reusable across future tasks in " + "the same area.",
|
|
189
|
+
preference: `A user's stated durable rule. Trigger words: "always", "from now on", ` + '"every time", "as a default", "going forward". Do NOT save preferences ' + "for one-off requests scoped to a specific task, session, or work-product " + '("after this task", "for this audit", "now"). When in doubt, just do the ' + "thing once without saving — the user can restate it if they want it to stick.",
|
|
190
|
+
decision: 'A chosen path with rationale. The "why" that future-you (or another agent) ' + "needs to understand why the codebase / approach looks the way it does — not " + 'the mechanical "what" (read the code for that).'
|
|
191
|
+
};
|
|
192
|
+
// ../core/dist/domain/runtime.js
|
|
193
|
+
var KNOWN_CLIS = ["claude", "codex", "opencode"];
|
|
194
|
+
var RUNTIME_HEARTBEAT_INTERVAL_MS = 15000;
|
|
195
|
+
// ../core/dist/auth/api-key.js
|
|
196
|
+
var KEY_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
197
|
+
var nanoid24 = customAlphabet(KEY_ALPHABET, 24);
|
|
198
|
+
// ../core/dist/auth/password.js
|
|
199
|
+
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
200
|
+
import { promisify } from "node:util";
|
|
201
|
+
var scryptAsync = promisify(scrypt);
|
|
202
|
+
var SCRYPT_N = 16384;
|
|
203
|
+
var SCRYPT_R = 8;
|
|
204
|
+
var SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2;
|
|
7
205
|
// src/config.ts
|
|
8
206
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
207
|
import { homedir } from "node:os";
|
|
@@ -25,8 +223,30 @@ function saveConfig(cfg) {
|
|
|
25
223
|
`, { mode: 384 });
|
|
26
224
|
}
|
|
27
225
|
|
|
226
|
+
// src/detect-clis.ts
|
|
227
|
+
import { execFile } from "node:child_process";
|
|
228
|
+
import { promisify as promisify2 } from "node:util";
|
|
229
|
+
var execFileAsync = promisify2(execFile);
|
|
230
|
+
async function probeOne(cli) {
|
|
231
|
+
try {
|
|
232
|
+
await execFileAsync("which", [cli]);
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
let cli_version;
|
|
237
|
+
try {
|
|
238
|
+
const { stdout } = await execFileAsync(cli, ["--version"]);
|
|
239
|
+
cli_version = stdout.trim().split(`
|
|
240
|
+
`)[0];
|
|
241
|
+
} catch {}
|
|
242
|
+
return { cli, cli_version };
|
|
243
|
+
}
|
|
244
|
+
async function detectClis() {
|
|
245
|
+
const results = await Promise.all(KNOWN_CLIS.map((cli) => probeOne(cli)));
|
|
246
|
+
return results.filter((r) => r !== null);
|
|
247
|
+
}
|
|
248
|
+
|
|
28
249
|
// src/setup.ts
|
|
29
|
-
var KNOWN_CLIS = ["claude", "codex", "opencode"];
|
|
30
250
|
async function runSetup(options) {
|
|
31
251
|
if (!/^https?:\/\//.test(options.apiUrl)) {
|
|
32
252
|
throw new Error("--api must be an http(s) URL");
|
|
@@ -36,7 +256,7 @@ async function runSetup(options) {
|
|
|
36
256
|
}
|
|
37
257
|
const externalId = options.externalId ?? hostname();
|
|
38
258
|
const deviceName = options.deviceName ?? `${userInfo().username}@${hostname()}`;
|
|
39
|
-
const runtimes = options.detectedClis ?? detectClis();
|
|
259
|
+
const runtimes = options.detectedClis ?? await detectClis();
|
|
40
260
|
if (runtimes.length === 0) {
|
|
41
261
|
throw new Error(`No supported CLIs detected on PATH. beevibe currently looks for: ${KNOWN_CLIS.join(", ")}`);
|
|
42
262
|
}
|
|
@@ -66,26 +286,11 @@ async function runSetup(options) {
|
|
|
66
286
|
saveConfig(config);
|
|
67
287
|
return config;
|
|
68
288
|
}
|
|
69
|
-
function detectClis() {
|
|
70
|
-
const out = [];
|
|
71
|
-
for (const cli of KNOWN_CLIS) {
|
|
72
|
-
const which = spawnSync("which", [cli], { encoding: "utf8" });
|
|
73
|
-
if (which.status !== 0 || !which.stdout.trim())
|
|
74
|
-
continue;
|
|
75
|
-
const version = spawnSync(cli, ["--version"], { encoding: "utf8" });
|
|
76
|
-
out.push({
|
|
77
|
-
cli,
|
|
78
|
-
cli_version: version.status === 0 ? version.stdout.trim().split(`
|
|
79
|
-
`)[0] : undefined
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
289
|
|
|
85
290
|
// ../core/dist/adapters/local-workspace/manager.js
|
|
86
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
291
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
87
292
|
import { homedir as homedir2 } from "node:os";
|
|
88
|
-
import { join as join2 } from "node:path";
|
|
293
|
+
import { isAbsolute, join as join2 } from "node:path";
|
|
89
294
|
|
|
90
295
|
// ../core/dist/services/skills/sync.js
|
|
91
296
|
import { promises as fs } from "node:fs";
|
|
@@ -216,35 +421,48 @@ class LocalWorkspaceManager {
|
|
|
216
421
|
root;
|
|
217
422
|
constructor(config) {
|
|
218
423
|
this.config = config;
|
|
219
|
-
this.root = config.workspaceRoot
|
|
424
|
+
this.root = config.workspaceRoot || join2(homedir2(), ".beevibe", "workspaces");
|
|
425
|
+
if (!isAbsolute(this.root)) {
|
|
426
|
+
throw new Error(`LocalWorkspaceManager: workspaceRoot must be absolute, got "${this.root}"`);
|
|
427
|
+
}
|
|
220
428
|
}
|
|
221
|
-
async ensureWorkspace({ agent }) {
|
|
222
|
-
const path2 = join2(this.root,
|
|
429
|
+
async ensureWorkspace({ agent: agent2 }) {
|
|
430
|
+
const path2 = join2(this.root, agent2.id);
|
|
223
431
|
mkdirSync2(path2, { recursive: true, mode: 448 });
|
|
432
|
+
if (!agent2.api_key) {
|
|
433
|
+
throw new Error(`Cannot write mcp-config.json for agent ${agent2.id}: agent.api_key is missing`);
|
|
434
|
+
}
|
|
224
435
|
const configPath = join2(path2, "mcp-config.json");
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
436
|
+
const expected = buildMcpConfig(agent2.api_key, this.config.mcpServerUrl);
|
|
437
|
+
const needsWrite = !existsSync2(configPath) || readFileSync2(configPath, "utf-8") !== expected;
|
|
438
|
+
if (needsWrite) {
|
|
439
|
+
writeFileSync2(configPath, expected, { mode: 384 });
|
|
440
|
+
}
|
|
441
|
+
const workspace2 = { path: path2 };
|
|
442
|
+
const runtime3 = this.config.runtimeRegistry[agent2.runtime_config.type];
|
|
443
|
+
if (!runtime3) {
|
|
444
|
+
throw new Error(`No runtime registered for agent ${agent2.id} (runtime_config.type='${agent2.runtime_config.type}')`);
|
|
445
|
+
}
|
|
446
|
+
if (runtime3.prepareWorkspace) {
|
|
447
|
+
if (!agent2.api_key) {
|
|
448
|
+
throw new Error(`Cannot prepare workspace for agent ${agent2.id}: agent.api_key is missing`);
|
|
228
449
|
}
|
|
229
|
-
|
|
230
|
-
|
|
450
|
+
await runtime3.prepareWorkspace({
|
|
451
|
+
workspace: workspace2,
|
|
452
|
+
agentApiKey: agent2.api_key,
|
|
453
|
+
mcpServerUrl: this.config.mcpServerUrl
|
|
231
454
|
});
|
|
232
455
|
}
|
|
233
|
-
const workspace = { path: path2 };
|
|
234
|
-
const runtime = this.config.runtimeRegistry[agent.runtime_config.type];
|
|
235
|
-
if (!runtime) {
|
|
236
|
-
throw new Error(`No runtime registered for agent ${agent.id} (runtime_config.type='${agent.runtime_config.type}')`);
|
|
237
|
-
}
|
|
238
456
|
await syncSkills({
|
|
239
457
|
sourceDir: this.config.skillsSourceDir,
|
|
240
|
-
targetDir:
|
|
241
|
-
filter: tierFilterFor(
|
|
458
|
+
targetDir: runtime3.skillsDir(workspace2),
|
|
459
|
+
filter: tierFilterFor(agent2.hierarchy_level),
|
|
242
460
|
namespacePrefix: "beevibe"
|
|
243
461
|
});
|
|
244
|
-
return
|
|
462
|
+
return workspace2;
|
|
245
463
|
}
|
|
246
|
-
async removeWorkspace(
|
|
247
|
-
rmSync(
|
|
464
|
+
async removeWorkspace(workspace2) {
|
|
465
|
+
rmSync(workspace2.path, { recursive: true, force: true });
|
|
248
466
|
}
|
|
249
467
|
}
|
|
250
468
|
function buildMcpConfig(apiKey, mcpServerUrl) {
|
|
@@ -447,6 +665,15 @@ function extractStepEvents(msg) {
|
|
|
447
665
|
}
|
|
448
666
|
];
|
|
449
667
|
}
|
|
668
|
+
if (msg.type === STREAM_TYPE.ToolResult) {
|
|
669
|
+
return [
|
|
670
|
+
{
|
|
671
|
+
kind: "tool_result",
|
|
672
|
+
description: describeToolResult(msg.content, msg.is_error === true),
|
|
673
|
+
timestamp: now
|
|
674
|
+
}
|
|
675
|
+
];
|
|
676
|
+
}
|
|
450
677
|
if (msg.type === STREAM_TYPE.ContentBlockStart && msg.content_block?.type === BLOCK_TYPE.ToolUse) {
|
|
451
678
|
const block = msg.content_block;
|
|
452
679
|
return [
|
|
@@ -480,6 +707,11 @@ function extractStepEvents(msg) {
|
|
|
480
707
|
}
|
|
481
708
|
return [];
|
|
482
709
|
}
|
|
710
|
+
function describeToolResult(content, isError) {
|
|
711
|
+
const text = typeof content === "string" ? content : JSON.stringify(content ?? "");
|
|
712
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
713
|
+
return isError ? `[error] ${collapsed}` : collapsed;
|
|
714
|
+
}
|
|
483
715
|
var PREFERRED_INPUT_FIELDS = [
|
|
484
716
|
"file_path",
|
|
485
717
|
"command",
|
|
@@ -594,12 +826,15 @@ function parseClaudeMessages(messages, exitCode) {
|
|
|
594
826
|
} : undefined;
|
|
595
827
|
return {
|
|
596
828
|
status: succeeded ? "completed" : "failed",
|
|
597
|
-
output: output || (succeeded ? "Session completed." :
|
|
829
|
+
output: output || (succeeded ? "Session completed." : bareCliExitMessage(exitCode)),
|
|
598
830
|
transcript: transcript || undefined,
|
|
599
831
|
usage,
|
|
600
832
|
cli_session_id: sessionId
|
|
601
833
|
};
|
|
602
834
|
}
|
|
835
|
+
function bareCliExitMessage(exitCode) {
|
|
836
|
+
return `CLI exited with code ${exitCode}`;
|
|
837
|
+
}
|
|
603
838
|
|
|
604
839
|
// ../core/dist/adapters/claude-code/runtime.js
|
|
605
840
|
var NESTING_GUARD_VARS = [
|
|
@@ -641,16 +876,19 @@ class ClaudeCodeRuntime {
|
|
|
641
876
|
args.push("--max-turns", String(maxTurns));
|
|
642
877
|
if (context.resume_session_id)
|
|
643
878
|
args.push("--resume", context.resume_session_id);
|
|
879
|
+
if (context.disallowed_tools?.length) {
|
|
880
|
+
args.push("--disallowedTools", context.disallowed_tools.join(","));
|
|
881
|
+
}
|
|
644
882
|
if (context.system_prompt_append.length > 0) {
|
|
645
883
|
args.push("--append-system-prompt", context.system_prompt_append);
|
|
646
884
|
}
|
|
647
|
-
const
|
|
885
|
+
const env2 = { ...process.env };
|
|
648
886
|
for (const key of NESTING_GUARD_VARS)
|
|
649
|
-
delete
|
|
887
|
+
delete env2[key];
|
|
650
888
|
for (const key of ANTHROPIC_AUTH_VARS)
|
|
651
|
-
delete
|
|
889
|
+
delete env2[key];
|
|
652
890
|
if (context.env)
|
|
653
|
-
Object.assign(
|
|
891
|
+
Object.assign(env2, context.env);
|
|
654
892
|
const messages = [];
|
|
655
893
|
let pending = "";
|
|
656
894
|
const handleLine = (line) => {
|
|
@@ -668,7 +906,7 @@ class ClaudeCodeRuntime {
|
|
|
668
906
|
command: this.config.command ?? "claude",
|
|
669
907
|
args,
|
|
670
908
|
cwd,
|
|
671
|
-
env,
|
|
909
|
+
env: env2,
|
|
672
910
|
stdin: context.intent,
|
|
673
911
|
abortSignal: context.abort_signal,
|
|
674
912
|
onSpawn: ({ pid, process_group_id }) => {
|
|
@@ -700,10 +938,14 @@ class ClaudeCodeRuntime {
|
|
|
700
938
|
};
|
|
701
939
|
}
|
|
702
940
|
const parsed = parseClaudeMessages(messages, result.exitCode);
|
|
941
|
+
const STDERR_TAIL_BYTES = 4096;
|
|
942
|
+
const stderrTail = parsed.status === "failed" && result.stderr ? result.stderr.slice(-STDERR_TAIL_BYTES) : undefined;
|
|
703
943
|
return {
|
|
704
944
|
...parsed,
|
|
705
945
|
process_pid: result.pid ?? undefined,
|
|
706
|
-
process_group_id: result.process_group_id ?? undefined
|
|
946
|
+
process_group_id: result.process_group_id ?? undefined,
|
|
947
|
+
exit_code: result.exitCode,
|
|
948
|
+
...stderrTail ? { stderr: stderrTail } : {}
|
|
707
949
|
};
|
|
708
950
|
}
|
|
709
951
|
async healthCheck() {
|
|
@@ -724,15 +966,710 @@ class ClaudeCodeRuntime {
|
|
|
724
966
|
}
|
|
725
967
|
}
|
|
726
968
|
async shutdown() {}
|
|
727
|
-
skillsDir(
|
|
728
|
-
return join3(
|
|
969
|
+
skillsDir(workspace2) {
|
|
970
|
+
return join3(workspace2.path, ".claude", "skills");
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ../core/dist/adapters/codex/runtime.js
|
|
975
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, unlinkSync } from "node:fs";
|
|
976
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
977
|
+
import { join as join4 } from "node:path";
|
|
978
|
+
|
|
979
|
+
// ../core/dist/adapters/codex/stream-json.js
|
|
980
|
+
var CODEX_EVENT_TYPE = {
|
|
981
|
+
ThreadStarted: "thread.started",
|
|
982
|
+
TurnStarted: "turn.started",
|
|
983
|
+
TurnCompleted: "turn.completed",
|
|
984
|
+
TurnFailed: "turn.failed",
|
|
985
|
+
ItemStarted: "item.started",
|
|
986
|
+
ItemUpdated: "item.updated",
|
|
987
|
+
ItemCompleted: "item.completed",
|
|
988
|
+
Error: "error"
|
|
989
|
+
};
|
|
990
|
+
var CODEX_ITEM_TYPE = {
|
|
991
|
+
AgentMessage: "agent_message",
|
|
992
|
+
Reasoning: "reasoning",
|
|
993
|
+
CommandExecution: "command_execution",
|
|
994
|
+
FileChange: "file_change",
|
|
995
|
+
McpToolCall: "mcp_tool_call",
|
|
996
|
+
CollabToolCall: "collab_tool_call",
|
|
997
|
+
WebSearch: "web_search",
|
|
998
|
+
TodoList: "todo_list",
|
|
999
|
+
Error: "error"
|
|
1000
|
+
};
|
|
1001
|
+
function parseCodexEventLine(line) {
|
|
1002
|
+
const trimmed = line.trim();
|
|
1003
|
+
if (!trimmed || !trimmed.startsWith("{"))
|
|
1004
|
+
return null;
|
|
1005
|
+
try {
|
|
1006
|
+
return JSON.parse(trimmed);
|
|
1007
|
+
} catch {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function extractCodexStepEvents(evt) {
|
|
1012
|
+
const now = new Date().toISOString();
|
|
1013
|
+
if (evt.type !== CODEX_EVENT_TYPE.ItemStarted && evt.type !== CODEX_EVENT_TYPE.ItemCompleted) {
|
|
1014
|
+
return [];
|
|
1015
|
+
}
|
|
1016
|
+
const item = evt.item;
|
|
1017
|
+
if (!item || !item.type)
|
|
1018
|
+
return [];
|
|
1019
|
+
const isCompletion = evt.type === CODEX_EVENT_TYPE.ItemCompleted;
|
|
1020
|
+
if (item.type === CODEX_ITEM_TYPE.AgentMessage) {
|
|
1021
|
+
if (!isCompletion)
|
|
1022
|
+
return [];
|
|
1023
|
+
const text = item.text?.trim();
|
|
1024
|
+
if (!text)
|
|
1025
|
+
return [];
|
|
1026
|
+
return [{ kind: "agent", description: text, timestamp: now }];
|
|
1027
|
+
}
|
|
1028
|
+
if (item.type === CODEX_ITEM_TYPE.McpToolCall) {
|
|
1029
|
+
return [
|
|
1030
|
+
{
|
|
1031
|
+
kind: isCompletion ? "tool_result" : "tool_call",
|
|
1032
|
+
tool: item.tool ?? "unknown",
|
|
1033
|
+
description: describeCodexInput(item.arguments),
|
|
1034
|
+
timestamp: now
|
|
1035
|
+
}
|
|
1036
|
+
];
|
|
1037
|
+
}
|
|
1038
|
+
if (item.type === CODEX_ITEM_TYPE.CommandExecution) {
|
|
1039
|
+
return [
|
|
1040
|
+
{
|
|
1041
|
+
kind: isCompletion ? "tool_result" : "tool_call",
|
|
1042
|
+
tool: "shell",
|
|
1043
|
+
description: (item.command ?? "").slice(0, 200),
|
|
1044
|
+
timestamp: now
|
|
1045
|
+
}
|
|
1046
|
+
];
|
|
1047
|
+
}
|
|
1048
|
+
if (item.type === CODEX_ITEM_TYPE.FileChange) {
|
|
1049
|
+
const summary = (item.changes ?? []).map((c) => `${c.kind ?? "?"} ${c.path ?? ""}`).join(", ").slice(0, 200);
|
|
1050
|
+
return [
|
|
1051
|
+
{
|
|
1052
|
+
kind: isCompletion ? "tool_result" : "tool_call",
|
|
1053
|
+
tool: "file_change",
|
|
1054
|
+
description: summary,
|
|
1055
|
+
timestamp: now
|
|
1056
|
+
}
|
|
1057
|
+
];
|
|
1058
|
+
}
|
|
1059
|
+
if (item.type === CODEX_ITEM_TYPE.WebSearch) {
|
|
1060
|
+
return [
|
|
1061
|
+
{
|
|
1062
|
+
kind: isCompletion ? "tool_result" : "tool_call",
|
|
1063
|
+
tool: "web_search",
|
|
1064
|
+
description: (item.query ?? "").slice(0, 200),
|
|
1065
|
+
timestamp: now
|
|
1066
|
+
}
|
|
1067
|
+
];
|
|
1068
|
+
}
|
|
1069
|
+
return [];
|
|
1070
|
+
}
|
|
1071
|
+
var PREFERRED_CODEX_FIELDS = [
|
|
1072
|
+
"file_path",
|
|
1073
|
+
"path",
|
|
1074
|
+
"command",
|
|
1075
|
+
"cmd",
|
|
1076
|
+
"query",
|
|
1077
|
+
"pattern",
|
|
1078
|
+
"url",
|
|
1079
|
+
"intent"
|
|
1080
|
+
];
|
|
1081
|
+
function describeCodexInput(input) {
|
|
1082
|
+
if (typeof input === "string")
|
|
1083
|
+
return input.slice(0, 200);
|
|
1084
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
1085
|
+
return "";
|
|
1086
|
+
const obj = input;
|
|
1087
|
+
for (const key of PREFERRED_CODEX_FIELDS) {
|
|
1088
|
+
const v = obj[key];
|
|
1089
|
+
if (typeof v === "string" && v.length > 0)
|
|
1090
|
+
return v.slice(0, 200);
|
|
1091
|
+
}
|
|
1092
|
+
return JSON.stringify(input).slice(0, 200);
|
|
1093
|
+
}
|
|
1094
|
+
function parseCodexEvents(events, exitCode, lastMessage) {
|
|
1095
|
+
let threadId;
|
|
1096
|
+
let usage;
|
|
1097
|
+
let assistantText = "";
|
|
1098
|
+
let turnFailed;
|
|
1099
|
+
let topLevelError;
|
|
1100
|
+
const transcriptParts = [];
|
|
1101
|
+
for (const evt of events) {
|
|
1102
|
+
switch (evt.type) {
|
|
1103
|
+
case CODEX_EVENT_TYPE.ThreadStarted:
|
|
1104
|
+
if (evt.thread_id)
|
|
1105
|
+
threadId = evt.thread_id;
|
|
1106
|
+
break;
|
|
1107
|
+
case CODEX_EVENT_TYPE.TurnCompleted:
|
|
1108
|
+
if (evt.usage)
|
|
1109
|
+
usage = evt.usage;
|
|
1110
|
+
break;
|
|
1111
|
+
case CODEX_EVENT_TYPE.TurnFailed:
|
|
1112
|
+
turnFailed = evt.error?.message ?? turnFailed;
|
|
1113
|
+
break;
|
|
1114
|
+
case CODEX_EVENT_TYPE.Error:
|
|
1115
|
+
topLevelError = evt.message ?? topLevelError;
|
|
1116
|
+
if (evt.message)
|
|
1117
|
+
transcriptParts.push(`[error] ${evt.message}
|
|
1118
|
+
`);
|
|
1119
|
+
break;
|
|
1120
|
+
case CODEX_EVENT_TYPE.ItemCompleted: {
|
|
1121
|
+
const item = evt.item;
|
|
1122
|
+
if (!item || !item.type)
|
|
1123
|
+
break;
|
|
1124
|
+
if (item.type === CODEX_ITEM_TYPE.AgentMessage && item.text) {
|
|
1125
|
+
assistantText = item.text;
|
|
1126
|
+
transcriptParts.push(`[assistant] ${item.text}
|
|
1127
|
+
`);
|
|
1128
|
+
} else if (item.type === CODEX_ITEM_TYPE.McpToolCall) {
|
|
1129
|
+
const tool = item.tool ?? "unknown";
|
|
1130
|
+
transcriptParts.push(`[tool_call] ${tool}
|
|
1131
|
+
`);
|
|
1132
|
+
const resultSummary = summarizeMcpResult(item.result);
|
|
1133
|
+
if (resultSummary || item.error?.message) {
|
|
1134
|
+
transcriptParts.push(`[tool_result from ${tool}] ${item.error?.message ?? resultSummary}
|
|
1135
|
+
`);
|
|
1136
|
+
}
|
|
1137
|
+
} else if (item.type === CODEX_ITEM_TYPE.CommandExecution) {
|
|
1138
|
+
transcriptParts.push(`[tool_call] shell ${(item.command ?? "").slice(0, 200)}
|
|
1139
|
+
`);
|
|
1140
|
+
if (item.aggregated_output) {
|
|
1141
|
+
transcriptParts.push(`[tool_result from shell] ${item.aggregated_output.slice(0, 200).replace(/\n/g, " ")}
|
|
1142
|
+
`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
default:
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const failed = exitCode !== 0 || !!turnFailed || !!topLevelError;
|
|
1152
|
+
const trimmedLast = lastMessage.trim();
|
|
1153
|
+
const failureMessage = turnFailed ?? topLevelError;
|
|
1154
|
+
const output = failed ? failureMessage || assistantText || bareCliExitMessage(exitCode) : trimmedLast || assistantText || "Session completed.";
|
|
1155
|
+
return {
|
|
1156
|
+
status: failed ? "failed" : "completed",
|
|
1157
|
+
output,
|
|
1158
|
+
transcript: transcriptParts.join("") || undefined,
|
|
1159
|
+
cli_session_id: threadId,
|
|
1160
|
+
usage: usage ? {
|
|
1161
|
+
input_tokens: usage.input_tokens ?? 0,
|
|
1162
|
+
output_tokens: (usage.output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0),
|
|
1163
|
+
cache_creation_input_tokens: 0,
|
|
1164
|
+
cache_read_input_tokens: usage.cached_input_tokens ?? 0
|
|
1165
|
+
} : undefined
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
function summarizeMcpResult(result) {
|
|
1169
|
+
if (!result || !Array.isArray(result.content))
|
|
1170
|
+
return "";
|
|
1171
|
+
for (const block of result.content) {
|
|
1172
|
+
if (block && typeof block === "object" && block.type === "text") {
|
|
1173
|
+
const text = block.text;
|
|
1174
|
+
if (typeof text === "string")
|
|
1175
|
+
return text.slice(0, 200).replace(/\n/g, " ");
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return JSON.stringify(result.content).slice(0, 200);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// ../core/dist/adapters/codex/runtime.js
|
|
1182
|
+
var OPENAI_AUTH_VARS = ["OPENAI_API_KEY", "OPENAI_AUTH_TOKEN"];
|
|
1183
|
+
|
|
1184
|
+
class CodexRuntime {
|
|
1185
|
+
config;
|
|
1186
|
+
type = "codex";
|
|
1187
|
+
prepared = new Map;
|
|
1188
|
+
constructor(config = {}) {
|
|
1189
|
+
this.config = config;
|
|
1190
|
+
}
|
|
1191
|
+
async execute(context) {
|
|
1192
|
+
const prepared = this.prepared.get(context.workspace.path);
|
|
1193
|
+
const sid = context.env?.BEEVIBE_SESSION_ID;
|
|
1194
|
+
const lastMessagePath = join4(context.workspace.path, `.beevibe-codex-last-message-${Date.now()}.txt`);
|
|
1195
|
+
const globalArgs = buildGlobalArgs(context, this.config);
|
|
1196
|
+
const execArgs = [
|
|
1197
|
+
"--json",
|
|
1198
|
+
"--skip-git-repo-check",
|
|
1199
|
+
"--output-last-message",
|
|
1200
|
+
lastMessagePath
|
|
1201
|
+
];
|
|
1202
|
+
if (prepared && sid) {
|
|
1203
|
+
globalArgs.push("-c", `mcp_servers.beevibe.url=${tomlString(withBeevibeSession(prepared.mcpServerUrl, sid))}`, "-c", `mcp_servers.beevibe.bearer_token_env_var=${tomlString("BEEVIBE_AGENT_API_KEY")}`, "-c", `mcp_servers.beevibe.default_tools_approval_mode=${tomlString("approve")}`);
|
|
1204
|
+
}
|
|
1205
|
+
const args = context.resume_session_id ? [
|
|
1206
|
+
...globalArgs,
|
|
1207
|
+
"exec",
|
|
1208
|
+
"resume",
|
|
1209
|
+
...execArgs,
|
|
1210
|
+
context.resume_session_id,
|
|
1211
|
+
composePrompt(context)
|
|
1212
|
+
] : [...globalArgs, "exec", ...execArgs, composePrompt(context)];
|
|
1213
|
+
const env2 = { ...process.env };
|
|
1214
|
+
for (const key of OPENAI_AUTH_VARS)
|
|
1215
|
+
delete env2[key];
|
|
1216
|
+
if (context.env)
|
|
1217
|
+
Object.assign(env2, context.env);
|
|
1218
|
+
if (prepared)
|
|
1219
|
+
env2.BEEVIBE_AGENT_API_KEY = prepared.agentApiKey;
|
|
1220
|
+
const events = [];
|
|
1221
|
+
let pending = "";
|
|
1222
|
+
const handleLine = (line) => {
|
|
1223
|
+
const evt = parseCodexEventLine(line);
|
|
1224
|
+
if (!evt)
|
|
1225
|
+
return;
|
|
1226
|
+
events.push(evt);
|
|
1227
|
+
if (!context.onStep)
|
|
1228
|
+
return;
|
|
1229
|
+
for (const step of extractCodexStepEvents(evt)) {
|
|
1230
|
+
context.onStep(step);
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
const result = await runCliProcess({
|
|
1234
|
+
command: this.config.command ?? "codex",
|
|
1235
|
+
args,
|
|
1236
|
+
cwd: context.workspace.path,
|
|
1237
|
+
env: env2,
|
|
1238
|
+
abortSignal: context.abort_signal,
|
|
1239
|
+
onSpawn: ({ pid, process_group_id }) => {
|
|
1240
|
+
context.onSpawn?.({ process_pid: pid, process_group_id });
|
|
1241
|
+
},
|
|
1242
|
+
onLog: (stream, chunk) => {
|
|
1243
|
+
if (stream !== "stdout")
|
|
1244
|
+
return;
|
|
1245
|
+
pending += chunk;
|
|
1246
|
+
let nl;
|
|
1247
|
+
while ((nl = pending.indexOf(`
|
|
1248
|
+
`)) !== -1) {
|
|
1249
|
+
handleLine(pending.slice(0, nl));
|
|
1250
|
+
pending = pending.slice(nl + 1);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
if (pending)
|
|
1255
|
+
handleLine(pending);
|
|
1256
|
+
if (result.truncated) {
|
|
1257
|
+
console.warn("[CodexRuntime] stdout truncated at 4MB — result parsing may be incomplete");
|
|
1258
|
+
}
|
|
1259
|
+
if (result.aborted) {
|
|
1260
|
+
removeIfExists(lastMessagePath);
|
|
1261
|
+
return {
|
|
1262
|
+
status: "cancelled",
|
|
1263
|
+
output: "Session cancelled.",
|
|
1264
|
+
process_pid: result.pid ?? undefined,
|
|
1265
|
+
process_group_id: result.process_group_id ?? undefined
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
const lastMessage = readIfExists(lastMessagePath);
|
|
1269
|
+
removeIfExists(lastMessagePath);
|
|
1270
|
+
const parsed = parseCodexEvents(events, result.exitCode, lastMessage);
|
|
1271
|
+
const STDERR_TAIL_BYTES = 4096;
|
|
1272
|
+
const stderrTail = parsed.status === "failed" && result.stderr ? result.stderr.slice(-STDERR_TAIL_BYTES) : undefined;
|
|
1273
|
+
return {
|
|
1274
|
+
...parsed,
|
|
1275
|
+
process_pid: result.pid ?? undefined,
|
|
1276
|
+
process_group_id: result.process_group_id ?? undefined,
|
|
1277
|
+
exit_code: result.exitCode,
|
|
1278
|
+
...stderrTail ? { stderr: stderrTail } : {}
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
async healthCheck() {
|
|
1282
|
+
try {
|
|
1283
|
+
const result = await runCliProcess({
|
|
1284
|
+
command: this.config.command ?? "codex",
|
|
1285
|
+
args: ["--version"],
|
|
1286
|
+
cwd: tmpdir2(),
|
|
1287
|
+
timeoutMs: 5000,
|
|
1288
|
+
graceMs: 0
|
|
1289
|
+
});
|
|
1290
|
+
return {
|
|
1291
|
+
healthy: result.exitCode === 0,
|
|
1292
|
+
error: result.exitCode === 0 ? undefined : result.stderr.slice(-500)
|
|
1293
|
+
};
|
|
1294
|
+
} catch {
|
|
1295
|
+
return {
|
|
1296
|
+
healthy: false,
|
|
1297
|
+
error: `Command not found: ${this.config.command ?? "codex"}`
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async shutdown() {}
|
|
1302
|
+
skillsDir(workspace2) {
|
|
1303
|
+
return join4(workspace2.path, ".codex", "skills");
|
|
1304
|
+
}
|
|
1305
|
+
prepareWorkspace(context) {
|
|
1306
|
+
this.prepared.set(context.workspace.path, {
|
|
1307
|
+
agentApiKey: context.agentApiKey,
|
|
1308
|
+
mcpServerUrl: context.mcpServerUrl
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
function buildGlobalArgs(context, config) {
|
|
1313
|
+
const args = [
|
|
1314
|
+
"--sandbox",
|
|
1315
|
+
"workspace-write",
|
|
1316
|
+
"--ask-for-approval",
|
|
1317
|
+
"never",
|
|
1318
|
+
"--cd",
|
|
1319
|
+
context.workspace.path
|
|
1320
|
+
];
|
|
1321
|
+
const model = context.model ?? config.model;
|
|
1322
|
+
if (model)
|
|
1323
|
+
args.push("--model", model);
|
|
1324
|
+
return args;
|
|
1325
|
+
}
|
|
1326
|
+
function composePrompt(context) {
|
|
1327
|
+
if (context.system_prompt_append.length === 0)
|
|
1328
|
+
return context.intent;
|
|
1329
|
+
return [
|
|
1330
|
+
"<beevibe_system_context>",
|
|
1331
|
+
context.system_prompt_append,
|
|
1332
|
+
"</beevibe_system_context>",
|
|
1333
|
+
"",
|
|
1334
|
+
context.intent
|
|
1335
|
+
].join(`
|
|
1336
|
+
`);
|
|
1337
|
+
}
|
|
1338
|
+
function withBeevibeSession(mcpServerUrl, sid) {
|
|
1339
|
+
const url = new URL(mcpServerUrl);
|
|
1340
|
+
url.searchParams.set("beevibe_session", sid);
|
|
1341
|
+
return url.toString();
|
|
1342
|
+
}
|
|
1343
|
+
function tomlString(value) {
|
|
1344
|
+
return JSON.stringify(value);
|
|
1345
|
+
}
|
|
1346
|
+
function readIfExists(path2) {
|
|
1347
|
+
try {
|
|
1348
|
+
return existsSync3(path2) ? readFileSync3(path2, "utf8") : "";
|
|
1349
|
+
} catch {
|
|
1350
|
+
return "";
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function removeIfExists(path2) {
|
|
1354
|
+
try {
|
|
1355
|
+
if (existsSync3(path2))
|
|
1356
|
+
unlinkSync(path2);
|
|
1357
|
+
} catch {}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// ../core/dist/adapters/opencode/runtime.js
|
|
1361
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1362
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
1363
|
+
import { join as join5 } from "node:path";
|
|
1364
|
+
|
|
1365
|
+
// ../core/dist/adapters/opencode/stream-json.js
|
|
1366
|
+
var OPENCODE_EVENT_TYPE = {
|
|
1367
|
+
Text: "text",
|
|
1368
|
+
Reasoning: "reasoning",
|
|
1369
|
+
ToolUse: "tool_use",
|
|
1370
|
+
StepStart: "step_start",
|
|
1371
|
+
StepFinish: "step_finish",
|
|
1372
|
+
Error: "error"
|
|
1373
|
+
};
|
|
1374
|
+
var OPENCODE_TOOL_STATUS = {
|
|
1375
|
+
Pending: "pending",
|
|
1376
|
+
Running: "running",
|
|
1377
|
+
Completed: "completed",
|
|
1378
|
+
Error: "error"
|
|
1379
|
+
};
|
|
1380
|
+
function parseOpenCodeEventLine(line) {
|
|
1381
|
+
const trimmed = line.trim();
|
|
1382
|
+
if (!trimmed || !trimmed.startsWith("{"))
|
|
1383
|
+
return null;
|
|
1384
|
+
try {
|
|
1385
|
+
return JSON.parse(trimmed);
|
|
1386
|
+
} catch {
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
function extractOpenCodeStepEvents(evt) {
|
|
1391
|
+
const now = new Date().toISOString();
|
|
1392
|
+
if (evt.type === OPENCODE_EVENT_TYPE.Text) {
|
|
1393
|
+
const text = evt.part?.text?.trim();
|
|
1394
|
+
if (!text)
|
|
1395
|
+
return [];
|
|
1396
|
+
return [{ kind: "agent", description: text, timestamp: now }];
|
|
1397
|
+
}
|
|
1398
|
+
if (evt.type === OPENCODE_EVENT_TYPE.ToolUse) {
|
|
1399
|
+
const part = evt.part;
|
|
1400
|
+
if (!part)
|
|
1401
|
+
return [];
|
|
1402
|
+
const status = part.state?.status;
|
|
1403
|
+
const isTerminal = status === OPENCODE_TOOL_STATUS.Completed || status === OPENCODE_TOOL_STATUS.Error;
|
|
1404
|
+
return [
|
|
1405
|
+
{
|
|
1406
|
+
kind: isTerminal ? "tool_result" : "tool_call",
|
|
1407
|
+
tool: part.tool ?? "unknown",
|
|
1408
|
+
description: describeOpenCodeInput(part.state?.input),
|
|
1409
|
+
timestamp: now
|
|
1410
|
+
}
|
|
1411
|
+
];
|
|
1412
|
+
}
|
|
1413
|
+
return [];
|
|
1414
|
+
}
|
|
1415
|
+
var PREFERRED_OPENCODE_FIELDS = [
|
|
1416
|
+
"file_path",
|
|
1417
|
+
"path",
|
|
1418
|
+
"command",
|
|
1419
|
+
"cmd",
|
|
1420
|
+
"query",
|
|
1421
|
+
"pattern",
|
|
1422
|
+
"url",
|
|
1423
|
+
"intent"
|
|
1424
|
+
];
|
|
1425
|
+
function describeOpenCodeInput(input) {
|
|
1426
|
+
if (typeof input === "string")
|
|
1427
|
+
return input.slice(0, 200);
|
|
1428
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
1429
|
+
return "";
|
|
1430
|
+
const obj = input;
|
|
1431
|
+
for (const key of PREFERRED_OPENCODE_FIELDS) {
|
|
1432
|
+
const v = obj[key];
|
|
1433
|
+
if (typeof v === "string" && v.length > 0)
|
|
1434
|
+
return v.slice(0, 200);
|
|
1435
|
+
}
|
|
1436
|
+
return JSON.stringify(input).slice(0, 200);
|
|
1437
|
+
}
|
|
1438
|
+
function parseOpenCodeEvents(events, exitCode) {
|
|
1439
|
+
let sessionId;
|
|
1440
|
+
let sawUsage = false;
|
|
1441
|
+
let totalInput = 0;
|
|
1442
|
+
let totalOutput = 0;
|
|
1443
|
+
let totalReasoning = 0;
|
|
1444
|
+
let totalCacheRead = 0;
|
|
1445
|
+
let totalCacheWrite = 0;
|
|
1446
|
+
let totalCost = 0;
|
|
1447
|
+
const assistantTexts = [];
|
|
1448
|
+
const transcriptParts = [];
|
|
1449
|
+
let errorMessage;
|
|
1450
|
+
for (const evt of events) {
|
|
1451
|
+
if (evt.sessionID)
|
|
1452
|
+
sessionId = evt.sessionID;
|
|
1453
|
+
switch (evt.type) {
|
|
1454
|
+
case OPENCODE_EVENT_TYPE.StepFinish: {
|
|
1455
|
+
const part = evt.part;
|
|
1456
|
+
if (!part)
|
|
1457
|
+
break;
|
|
1458
|
+
sawUsage = true;
|
|
1459
|
+
totalCost += part.cost ?? 0;
|
|
1460
|
+
totalInput += part.tokens?.input ?? 0;
|
|
1461
|
+
totalOutput += part.tokens?.output ?? 0;
|
|
1462
|
+
totalReasoning += part.tokens?.reasoning ?? 0;
|
|
1463
|
+
totalCacheRead += part.tokens?.cache?.read ?? 0;
|
|
1464
|
+
totalCacheWrite += part.tokens?.cache?.write ?? 0;
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
case OPENCODE_EVENT_TYPE.Text: {
|
|
1468
|
+
const text = evt.part?.text;
|
|
1469
|
+
if (text) {
|
|
1470
|
+
assistantTexts.push(text);
|
|
1471
|
+
transcriptParts.push(`[assistant] ${text}
|
|
1472
|
+
`);
|
|
1473
|
+
}
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
case OPENCODE_EVENT_TYPE.ToolUse: {
|
|
1477
|
+
const part = evt.part;
|
|
1478
|
+
if (!part)
|
|
1479
|
+
break;
|
|
1480
|
+
const tool = part.tool ?? "unknown";
|
|
1481
|
+
const status = part.state?.status;
|
|
1482
|
+
if (status === OPENCODE_TOOL_STATUS.Completed || status === OPENCODE_TOOL_STATUS.Error) {
|
|
1483
|
+
const detail = (part.state?.error ?? part.state?.output ?? "").slice(0, 200).replace(/\n/g, " ");
|
|
1484
|
+
transcriptParts.push(detail ? `[tool_result from ${tool}] ${detail}
|
|
1485
|
+
` : `[tool_result from ${tool}]
|
|
1486
|
+
`);
|
|
1487
|
+
} else {
|
|
1488
|
+
transcriptParts.push(`[tool_call] ${tool}
|
|
1489
|
+
`);
|
|
1490
|
+
}
|
|
1491
|
+
break;
|
|
1492
|
+
}
|
|
1493
|
+
case OPENCODE_EVENT_TYPE.Error:
|
|
1494
|
+
errorMessage = evt.error?.message ?? evt.result?.error?.message ?? errorMessage;
|
|
1495
|
+
if (errorMessage)
|
|
1496
|
+
transcriptParts.push(`[error] ${errorMessage}
|
|
1497
|
+
`);
|
|
1498
|
+
break;
|
|
1499
|
+
default:
|
|
1500
|
+
break;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
const assistantText = assistantTexts.join(`
|
|
1504
|
+
`).trim();
|
|
1505
|
+
const failed = exitCode !== 0 || !!errorMessage;
|
|
1506
|
+
const output = failed ? errorMessage || assistantText || bareCliExitMessage(exitCode) : assistantText || "Session completed.";
|
|
1507
|
+
const usage = sawUsage ? {
|
|
1508
|
+
input_tokens: totalInput,
|
|
1509
|
+
output_tokens: totalOutput + totalReasoning,
|
|
1510
|
+
cache_creation_input_tokens: totalCacheWrite,
|
|
1511
|
+
cache_read_input_tokens: totalCacheRead,
|
|
1512
|
+
cost_usd: totalCost
|
|
1513
|
+
} : undefined;
|
|
1514
|
+
return {
|
|
1515
|
+
status: failed ? "failed" : "completed",
|
|
1516
|
+
output,
|
|
1517
|
+
transcript: transcriptParts.join("") || undefined,
|
|
1518
|
+
cli_session_id: sessionId,
|
|
1519
|
+
usage
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// ../core/dist/adapters/opencode/runtime.js
|
|
1524
|
+
class OpenCodeRuntime {
|
|
1525
|
+
config;
|
|
1526
|
+
type = "opencode";
|
|
1527
|
+
constructor(config = {}) {
|
|
1528
|
+
this.config = config;
|
|
1529
|
+
}
|
|
1530
|
+
async execute(context) {
|
|
1531
|
+
const args = [
|
|
1532
|
+
"run",
|
|
1533
|
+
"--format",
|
|
1534
|
+
"json",
|
|
1535
|
+
"--dangerously-skip-permissions",
|
|
1536
|
+
"--dir",
|
|
1537
|
+
context.workspace.path
|
|
1538
|
+
];
|
|
1539
|
+
const model = context.model ?? this.config.model;
|
|
1540
|
+
if (model)
|
|
1541
|
+
args.push("--model", model);
|
|
1542
|
+
if (context.resume_session_id)
|
|
1543
|
+
args.push("--session", context.resume_session_id);
|
|
1544
|
+
args.push(composePrompt2(context));
|
|
1545
|
+
const env2 = { ...process.env };
|
|
1546
|
+
if (context.env)
|
|
1547
|
+
Object.assign(env2, context.env);
|
|
1548
|
+
const events = [];
|
|
1549
|
+
let pending = "";
|
|
1550
|
+
const handleLine = (line) => {
|
|
1551
|
+
const evt = parseOpenCodeEventLine(line);
|
|
1552
|
+
if (!evt)
|
|
1553
|
+
return;
|
|
1554
|
+
events.push(evt);
|
|
1555
|
+
if (!context.onStep)
|
|
1556
|
+
return;
|
|
1557
|
+
for (const step of extractOpenCodeStepEvents(evt)) {
|
|
1558
|
+
context.onStep(step);
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
const result = await runCliProcess({
|
|
1562
|
+
command: this.config.command ?? "opencode",
|
|
1563
|
+
args,
|
|
1564
|
+
cwd: context.workspace.path,
|
|
1565
|
+
env: env2,
|
|
1566
|
+
abortSignal: context.abort_signal,
|
|
1567
|
+
onSpawn: ({ pid, process_group_id }) => {
|
|
1568
|
+
context.onSpawn?.({ process_pid: pid, process_group_id });
|
|
1569
|
+
},
|
|
1570
|
+
onLog: (stream, chunk) => {
|
|
1571
|
+
if (stream !== "stdout")
|
|
1572
|
+
return;
|
|
1573
|
+
pending += chunk;
|
|
1574
|
+
let nl;
|
|
1575
|
+
while ((nl = pending.indexOf(`
|
|
1576
|
+
`)) !== -1) {
|
|
1577
|
+
handleLine(pending.slice(0, nl));
|
|
1578
|
+
pending = pending.slice(nl + 1);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
if (pending)
|
|
1583
|
+
handleLine(pending);
|
|
1584
|
+
if (result.truncated) {
|
|
1585
|
+
console.warn("[OpenCodeRuntime] stdout truncated at 4MB — result parsing may be incomplete");
|
|
1586
|
+
}
|
|
1587
|
+
if (result.aborted) {
|
|
1588
|
+
return {
|
|
1589
|
+
status: "cancelled",
|
|
1590
|
+
output: "Session cancelled.",
|
|
1591
|
+
process_pid: result.pid ?? undefined,
|
|
1592
|
+
process_group_id: result.process_group_id ?? undefined
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
const parsed = parseOpenCodeEvents(events, result.exitCode);
|
|
1596
|
+
const STDERR_TAIL_BYTES = 4096;
|
|
1597
|
+
const stderrTail = parsed.status === "failed" && result.stderr ? result.stderr.slice(-STDERR_TAIL_BYTES) : undefined;
|
|
1598
|
+
return {
|
|
1599
|
+
...parsed,
|
|
1600
|
+
process_pid: result.pid ?? undefined,
|
|
1601
|
+
process_group_id: result.process_group_id ?? undefined,
|
|
1602
|
+
exit_code: result.exitCode,
|
|
1603
|
+
...stderrTail ? { stderr: stderrTail } : {}
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
async healthCheck() {
|
|
1607
|
+
try {
|
|
1608
|
+
const result = await runCliProcess({
|
|
1609
|
+
command: this.config.command ?? "opencode",
|
|
1610
|
+
args: ["--version"],
|
|
1611
|
+
cwd: tmpdir3(),
|
|
1612
|
+
timeoutMs: 5000,
|
|
1613
|
+
graceMs: 0
|
|
1614
|
+
});
|
|
1615
|
+
return { healthy: result.exitCode === 0 };
|
|
1616
|
+
} catch {
|
|
1617
|
+
return {
|
|
1618
|
+
healthy: false,
|
|
1619
|
+
error: `Command not found: ${this.config.command ?? "opencode"}`
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
async shutdown() {}
|
|
1624
|
+
skillsDir(workspace2) {
|
|
1625
|
+
return join5(workspace2.path, ".opencode", "skills");
|
|
1626
|
+
}
|
|
1627
|
+
prepareWorkspace(context) {
|
|
1628
|
+
const configPath = join5(context.workspace.path, "opencode.json");
|
|
1629
|
+
if (existsSync4(configPath))
|
|
1630
|
+
return;
|
|
1631
|
+
writeFileSync3(configPath, buildOpenCodeConfig(context.agentApiKey, context.mcpServerUrl), {
|
|
1632
|
+
mode: 384
|
|
1633
|
+
});
|
|
729
1634
|
}
|
|
730
1635
|
}
|
|
1636
|
+
function composePrompt2(context) {
|
|
1637
|
+
if (context.system_prompt_append.length === 0)
|
|
1638
|
+
return context.intent;
|
|
1639
|
+
return [
|
|
1640
|
+
"<beevibe_system_context>",
|
|
1641
|
+
context.system_prompt_append,
|
|
1642
|
+
"</beevibe_system_context>",
|
|
1643
|
+
"",
|
|
1644
|
+
context.intent
|
|
1645
|
+
].join(`
|
|
1646
|
+
`);
|
|
1647
|
+
}
|
|
1648
|
+
function buildOpenCodeConfig(apiKey, mcpServerUrl) {
|
|
1649
|
+
return JSON.stringify({
|
|
1650
|
+
$schema: "https://opencode.ai/config.json",
|
|
1651
|
+
mcp: {
|
|
1652
|
+
beevibe: {
|
|
1653
|
+
type: "remote",
|
|
1654
|
+
url: mcpServerUrl,
|
|
1655
|
+
enabled: true,
|
|
1656
|
+
oauth: false,
|
|
1657
|
+
headers: {
|
|
1658
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1659
|
+
"X-Beevibe-Session": "{env:BEEVIBE_SESSION_ID}"
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}, null, 2) + `
|
|
1664
|
+
`;
|
|
1665
|
+
}
|
|
731
1666
|
|
|
732
1667
|
// ../core/dist/adapters/runtime-registry.js
|
|
733
1668
|
function createDefaultRuntimeRegistry() {
|
|
734
1669
|
return {
|
|
735
|
-
claude: new ClaudeCodeRuntime({})
|
|
1670
|
+
claude: new ClaudeCodeRuntime({}),
|
|
1671
|
+
codex: new CodexRuntime({}),
|
|
1672
|
+
opencode: new OpenCodeRuntime({})
|
|
736
1673
|
};
|
|
737
1674
|
}
|
|
738
1675
|
|
|
@@ -794,16 +1731,22 @@ class ApiClient {
|
|
|
794
1731
|
return `${this.cfg.apiUrl}${path2}`;
|
|
795
1732
|
}
|
|
796
1733
|
}
|
|
1734
|
+
|
|
797
1735
|
// src/spawner.ts
|
|
798
1736
|
async function runDispatch(deps, payload, abortSignal) {
|
|
799
1737
|
const syntheticAgent = {
|
|
800
1738
|
id: payload.agent_id,
|
|
801
1739
|
api_key: payload.agent_api_key,
|
|
802
1740
|
hierarchy_level: payload.agent_hierarchy_level,
|
|
803
|
-
runtime_config: { type:
|
|
1741
|
+
runtime_config: { type: payload.runtime_type }
|
|
804
1742
|
};
|
|
805
1743
|
const ws = await deps.workspaceManager.ensureWorkspace({ agent: syntheticAgent });
|
|
806
|
-
|
|
1744
|
+
console.log(`[daemon/spawn] sess=${payload.session_id} agent=${payload.agent_id} runtime=${payload.runtime_type} type=${payload.type} cwd=${ws.path}`);
|
|
1745
|
+
const registry = deps.runtimeRegistry ?? createDefaultRuntimeRegistry();
|
|
1746
|
+
const runtime3 = registry[payload.runtime_type];
|
|
1747
|
+
if (!runtime3) {
|
|
1748
|
+
throw new Error(`No runtime registered for dispatch payload type '${payload.runtime_type}'`);
|
|
1749
|
+
}
|
|
807
1750
|
const buffer = [];
|
|
808
1751
|
let flushTimer;
|
|
809
1752
|
const flush = async () => {
|
|
@@ -839,12 +1782,13 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
839
1782
|
let result;
|
|
840
1783
|
let runError;
|
|
841
1784
|
try {
|
|
842
|
-
result = await
|
|
1785
|
+
result = await runtime3.execute({
|
|
843
1786
|
intent: payload.intent,
|
|
844
1787
|
workspace: ws,
|
|
845
1788
|
system_prompt_append: payload.system_prompt_append,
|
|
846
1789
|
model: payload.model,
|
|
847
1790
|
max_turns: payload.max_turns,
|
|
1791
|
+
disallowed_tools: payload.disallowed_tools,
|
|
848
1792
|
env: payload.env,
|
|
849
1793
|
resume_session_id: payload.resume_session_id,
|
|
850
1794
|
abort_signal: abortSignal,
|
|
@@ -859,15 +1803,25 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
859
1803
|
}
|
|
860
1804
|
await flush();
|
|
861
1805
|
const status = runError ? "failed" : result?.status === "completed" ? "succeeded" : result?.status === "cancelled" ? "cancelled" : "failed";
|
|
1806
|
+
const errorDetail = runError?.message ?? result?.stderr;
|
|
862
1807
|
const done = {
|
|
863
1808
|
session_id: payload.session_id,
|
|
864
1809
|
status,
|
|
865
1810
|
cli_session_id: result?.cli_session_id,
|
|
866
1811
|
result_summary: result?.output ?? "",
|
|
867
|
-
exit_code:
|
|
868
|
-
error:
|
|
1812
|
+
exit_code: result?.exit_code ?? null,
|
|
1813
|
+
error: errorDetail,
|
|
869
1814
|
usage: result?.usage
|
|
870
1815
|
};
|
|
1816
|
+
if (status === "succeeded") {
|
|
1817
|
+
console.log(`[daemon/spawn] sess=${payload.session_id} exit=0`);
|
|
1818
|
+
} else {
|
|
1819
|
+
console.error(`[daemon/spawn] sess=${payload.session_id} status=${status} exit=${done.exit_code}` + (errorDetail ? `
|
|
1820
|
+
error:
|
|
1821
|
+
${errorDetail.split(`
|
|
1822
|
+
`).join(`
|
|
1823
|
+
`)}` : ""));
|
|
1824
|
+
}
|
|
871
1825
|
try {
|
|
872
1826
|
await deps.api.post("/runtime/done", done);
|
|
873
1827
|
} catch (err) {
|
|
@@ -877,7 +1831,6 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
877
1831
|
|
|
878
1832
|
// src/claimer.ts
|
|
879
1833
|
var DEFAULT_POLL_MS = 30000;
|
|
880
|
-
var DEFAULT_HEARTBEAT_MS = 15000;
|
|
881
1834
|
var DEFAULT_WS_RECONNECT_MAX_MS = 30000;
|
|
882
1835
|
|
|
883
1836
|
class Claimer {
|
|
@@ -894,7 +1847,7 @@ class Claimer {
|
|
|
894
1847
|
constructor(cfg) {
|
|
895
1848
|
this.cfg = cfg;
|
|
896
1849
|
this.pollIntervalMs = cfg.pollIntervalMs ?? DEFAULT_POLL_MS;
|
|
897
|
-
this.heartbeatIntervalMs = cfg.heartbeatIntervalMs ??
|
|
1850
|
+
this.heartbeatIntervalMs = cfg.heartbeatIntervalMs ?? RUNTIME_HEARTBEAT_INTERVAL_MS;
|
|
898
1851
|
this.wsReconnectMaxDelayMs = cfg.wsReconnectMaxDelayMs ?? DEFAULT_WS_RECONNECT_MAX_MS;
|
|
899
1852
|
}
|
|
900
1853
|
start() {
|
|
@@ -988,11 +1941,22 @@ class Claimer {
|
|
|
988
1941
|
}
|
|
989
1942
|
async pollRuntime(runtimeId) {
|
|
990
1943
|
while (this.running && this.cfg.supervisor.hasCapacity()) {
|
|
991
|
-
|
|
1944
|
+
let payload;
|
|
1945
|
+
try {
|
|
1946
|
+
payload = await this.cfg.api.claim(runtimeId);
|
|
1947
|
+
} catch (err) {
|
|
1948
|
+
console.warn(`[daemon] claim failed for runtime=${runtimeId}:`, err instanceof Error ? err.message : String(err));
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
992
1951
|
if (!payload)
|
|
993
1952
|
return;
|
|
1953
|
+
console.log(`[daemon/claim] sess=${payload.session_id} agent=${payload.agent_id} runtime=${runtimeId}`);
|
|
994
1954
|
const ctrl = this.cfg.supervisor.start(payload.session_id);
|
|
995
|
-
runDispatch({
|
|
1955
|
+
runDispatch({
|
|
1956
|
+
api: this.cfg.api,
|
|
1957
|
+
workspaceManager: this.cfg.workspaceManager,
|
|
1958
|
+
runtimeRegistry: this.cfg.runtimeRegistry
|
|
1959
|
+
}, payload, ctrl.signal).catch((err) => console.error(`[daemon] dispatch ${payload.session_id} failed:`, err instanceof Error ? err.message : String(err))).finally(() => this.cfg.supervisor.finish(payload.session_id));
|
|
996
1960
|
}
|
|
997
1961
|
}
|
|
998
1962
|
}
|
|
@@ -1000,14 +1964,14 @@ class Claimer {
|
|
|
1000
1964
|
// src/skills-cache.ts
|
|
1001
1965
|
import { promises as fs2 } from "node:fs";
|
|
1002
1966
|
import { homedir as homedir3 } from "node:os";
|
|
1003
|
-
import { join as
|
|
1967
|
+
import { join as join6 } from "node:path";
|
|
1004
1968
|
function skillsCacheDir() {
|
|
1005
|
-
return
|
|
1969
|
+
return join6(homedir3(), ".beevibe", "skills");
|
|
1006
1970
|
}
|
|
1007
1971
|
var VERSION_FILE = ".version";
|
|
1008
1972
|
async function readCachedVersion() {
|
|
1009
1973
|
try {
|
|
1010
|
-
return (await fs2.readFile(
|
|
1974
|
+
return (await fs2.readFile(join6(skillsCacheDir(), VERSION_FILE), "utf8")).trim();
|
|
1011
1975
|
} catch {
|
|
1012
1976
|
return;
|
|
1013
1977
|
}
|
|
@@ -1028,19 +1992,19 @@ async function syncSkillsCache(api) {
|
|
|
1028
1992
|
if (!dirent.isDirectory())
|
|
1029
1993
|
continue;
|
|
1030
1994
|
if (dirent.name === "beevibe" || dirent.name.startsWith("beevibe-")) {
|
|
1031
|
-
await fs2.rm(
|
|
1995
|
+
await fs2.rm(join6(cache, dirent.name), { recursive: true, force: true });
|
|
1032
1996
|
}
|
|
1033
1997
|
}
|
|
1034
1998
|
for (const skill of res.skills) {
|
|
1035
|
-
const skillDir =
|
|
1999
|
+
const skillDir = join6(cache, skill.name);
|
|
1036
2000
|
await fs2.mkdir(skillDir, { recursive: true, mode: 448 });
|
|
1037
2001
|
for (const file of skill.files) {
|
|
1038
|
-
const filePath =
|
|
1039
|
-
await fs2.mkdir(
|
|
2002
|
+
const filePath = join6(skillDir, file.path);
|
|
2003
|
+
await fs2.mkdir(join6(filePath, ".."), { recursive: true });
|
|
1040
2004
|
await fs2.writeFile(filePath, file.content, { mode: 384 });
|
|
1041
2005
|
}
|
|
1042
2006
|
}
|
|
1043
|
-
await fs2.writeFile(
|
|
2007
|
+
await fs2.writeFile(join6(cache, VERSION_FILE), res.version, { mode: 384 });
|
|
1044
2008
|
return cache;
|
|
1045
2009
|
}
|
|
1046
2010
|
|
|
@@ -1119,6 +2083,7 @@ async function runStart() {
|
|
|
1119
2083
|
api,
|
|
1120
2084
|
supervisor,
|
|
1121
2085
|
workspaceManager,
|
|
2086
|
+
runtimeRegistry,
|
|
1122
2087
|
runtimeIds: cfg.runtimes.map((r) => r.id)
|
|
1123
2088
|
});
|
|
1124
2089
|
claimer.start();
|
|
@@ -1134,16 +2099,46 @@ async function runStart() {
|
|
|
1134
2099
|
};
|
|
1135
2100
|
process.on("SIGINT", () => void stop("SIGINT"));
|
|
1136
2101
|
process.on("SIGTERM", () => void stop("SIGTERM"));
|
|
2102
|
+
process.on("unhandledRejection", (reason) => {
|
|
2103
|
+
console.warn("[daemon] unhandledRejection (continuing):", reason instanceof Error ? reason.message : String(reason));
|
|
2104
|
+
});
|
|
1137
2105
|
await new Promise(() => {
|
|
1138
2106
|
return;
|
|
1139
2107
|
});
|
|
1140
2108
|
}
|
|
1141
2109
|
|
|
2110
|
+
// src/sync.ts
|
|
2111
|
+
async function runSync() {
|
|
2112
|
+
const config = loadConfig();
|
|
2113
|
+
if (!config) {
|
|
2114
|
+
throw new Error(`No daemon config at ${CONFIG_PATH}. Run 'beevibe-daemon setup' first.`);
|
|
2115
|
+
}
|
|
2116
|
+
const detected = await detectClis();
|
|
2117
|
+
if (detected.length === 0) {
|
|
2118
|
+
throw new Error(`No supported CLIs detected on PATH. beevibe currently looks for: ${KNOWN_CLIS.join(", ")}`);
|
|
2119
|
+
}
|
|
2120
|
+
const api = new ApiClient({
|
|
2121
|
+
apiUrl: config.api_url,
|
|
2122
|
+
daemonToken: config.daemon_token
|
|
2123
|
+
});
|
|
2124
|
+
const { status, body } = await api.post("/runtime/sync", {
|
|
2125
|
+
runtimes: detected
|
|
2126
|
+
});
|
|
2127
|
+
if (status !== 200 || !body) {
|
|
2128
|
+
throw new Error(`/runtime/sync failed: ${status}`);
|
|
2129
|
+
}
|
|
2130
|
+
const before = new Set(config.runtimes.map((r) => r.cli));
|
|
2131
|
+
const added = body.runtimes.filter((r) => !before.has(r.cli));
|
|
2132
|
+
const next = { ...config, runtimes: body.runtimes };
|
|
2133
|
+
saveConfig(next);
|
|
2134
|
+
return { added, runtimes: body.runtimes };
|
|
2135
|
+
}
|
|
2136
|
+
|
|
1142
2137
|
// src/update.ts
|
|
1143
2138
|
import { createHash } from "node:crypto";
|
|
1144
2139
|
import { createWriteStream, mkdtempSync, rmSync as rmSync2, chmodSync, renameSync } from "node:fs";
|
|
1145
|
-
import { tmpdir as
|
|
1146
|
-
import { join as
|
|
2140
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
2141
|
+
import { join as join7 } from "node:path";
|
|
1147
2142
|
import { Readable } from "node:stream";
|
|
1148
2143
|
import { pipeline } from "node:stream/promises";
|
|
1149
2144
|
import { createInterface } from "node:readline/promises";
|
|
@@ -1157,7 +2152,7 @@ var PLATFORM_ASSETS = {
|
|
|
1157
2152
|
"linux-arm64": "beevibe-daemon-linux-arm64"
|
|
1158
2153
|
};
|
|
1159
2154
|
function currentVersion() {
|
|
1160
|
-
return "0.1.
|
|
2155
|
+
return "0.1.3";
|
|
1161
2156
|
}
|
|
1162
2157
|
function isCompiledBinary() {
|
|
1163
2158
|
if (!process.versions.bun)
|
|
@@ -1270,8 +2265,8 @@ async function runUpdate(opts = {}) {
|
|
|
1270
2265
|
return;
|
|
1271
2266
|
}
|
|
1272
2267
|
}
|
|
1273
|
-
const stagingDir = mkdtempSync(
|
|
1274
|
-
const stagingPath =
|
|
2268
|
+
const stagingDir = mkdtempSync(join7(tmpdir4(), "beevibe-daemon-update-"));
|
|
2269
|
+
const stagingPath = join7(stagingDir, asset);
|
|
1275
2270
|
try {
|
|
1276
2271
|
console.log(`Downloading ${asset}…`);
|
|
1277
2272
|
const downloadUrl = `${DOWNLOAD_BASE}/${latest}/${asset}`;
|
|
@@ -1329,6 +2324,7 @@ function printHelp() {
|
|
|
1329
2324
|
"Commands:",
|
|
1330
2325
|
" setup Register this machine with a beevibe api server.",
|
|
1331
2326
|
" start Run the daemon: claim pending sessions and spawn the CLI.",
|
|
2327
|
+
" sync Re-detect CLIs on PATH and register newly-installed ones.",
|
|
1332
2328
|
" update Check for and install a newer daemon binary (brew/curl installs).",
|
|
1333
2329
|
"",
|
|
1334
2330
|
"setup flags:",
|
|
@@ -1370,6 +2366,16 @@ async function main() {
|
|
|
1370
2366
|
await runStart();
|
|
1371
2367
|
return;
|
|
1372
2368
|
}
|
|
2369
|
+
if (command === "sync") {
|
|
2370
|
+
const result = await runSync();
|
|
2371
|
+
if (result.added.length === 0) {
|
|
2372
|
+
console.log("No new CLIs detected.");
|
|
2373
|
+
} else {
|
|
2374
|
+
console.log(`Added ${result.added.length} runtime(s): ${result.added.map((r) => `${r.cli} (${r.id})`).join(", ")}.`);
|
|
2375
|
+
console.log("Restart the daemon to pick up the new runtime(s).");
|
|
2376
|
+
}
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
1373
2379
|
if (command === "update") {
|
|
1374
2380
|
const skipPrompt = rest.includes("--yes") || rest.includes("-y");
|
|
1375
2381
|
await runUpdate({ skipPrompt });
|