office-core 0.1.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/.runtime-dist/scripts/bundle-host-package.js +46 -0
- package/.runtime-dist/scripts/demo-multi-agent.js +130 -0
- package/.runtime-dist/scripts/home-agent-host.js +1403 -0
- package/.runtime-dist/scripts/host-doctor.js +28 -0
- package/.runtime-dist/scripts/host-login.js +32 -0
- package/.runtime-dist/scripts/host-menu.js +227 -0
- package/.runtime-dist/scripts/host-open.js +20 -0
- package/.runtime-dist/scripts/install-host.js +108 -0
- package/.runtime-dist/scripts/lib/host-config.js +171 -0
- package/.runtime-dist/scripts/lib/local-runner.js +287 -0
- package/.runtime-dist/scripts/office-cli.js +698 -0
- package/.runtime-dist/scripts/run-local-project.js +277 -0
- package/.runtime-dist/src/auth/session-token.js +62 -0
- package/.runtime-dist/src/discord/outbox-ledger.js +56 -0
- package/.runtime-dist/src/do/AgentDO.js +205 -0
- package/.runtime-dist/src/do/GatewayShardDO.js +9 -0
- package/.runtime-dist/src/do/ProjectDO.js +829 -0
- package/.runtime-dist/src/do/TaskDO.js +356 -0
- package/.runtime-dist/src/index.js +123 -0
- package/.runtime-dist/src/project/office-view.js +405 -0
- package/.runtime-dist/src/project/read-model.js +79 -0
- package/.runtime-dist/src/routes/agents-bootstrap.js +9 -0
- package/.runtime-dist/src/routes/agents-descriptor.js +12 -0
- package/.runtime-dist/src/routes/agents-events.js +17 -0
- package/.runtime-dist/src/routes/agents-heartbeat.js +21 -0
- package/.runtime-dist/src/routes/agents-task-context.js +17 -0
- package/.runtime-dist/src/routes/bundles.js +198 -0
- package/.runtime-dist/src/routes/local-host.js +49 -0
- package/.runtime-dist/src/routes/projects.js +119 -0
- package/.runtime-dist/src/routes/tasks.js +67 -0
- package/.runtime-dist/src/task/reducer.js +464 -0
- package/.runtime-dist/src/types/project.js +1 -0
- package/.runtime-dist/src/types/protocol.js +3 -0
- package/.runtime-dist/src/types/runtime.js +1 -0
- package/README.md +148 -0
- package/bin/double-penetration-host.mjs +83 -0
- package/package.json +48 -0
- package/public/index.html +1581 -0
- package/public/install-host.ps1 +64 -0
- package/scripts/run-runtime-script.mjs +43 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import { createInterface, emitKeypressEvents } from "node:readline";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { loadHostConfig, upsertHostConfig, getHostConfigPath } from "./lib/host-config.js";
|
|
7
|
+
import { probeRunnerAvailability, resolveRunnerCommand } from "./lib/local-runner.js";
|
|
8
|
+
import { sessions, hooks, buildRuntimeConfig, startDaemonLoop, stopDaemonLoop, stopSession, buildSpawnContext, spawnInteractiveSession, persistSessions, postRoomMessageUpsert, postJson, getJson, resolveProjectWorkdir, } from "./home-agent-host.js";
|
|
9
|
+
// ─── ANSI Codes ─────────────────────────────────────────────────────────────
|
|
10
|
+
const R = "\x1b[0m";
|
|
11
|
+
const BOLD = "\x1b[1m";
|
|
12
|
+
const DIM = "\x1b[2m";
|
|
13
|
+
const BMAG = "\x1b[95m";
|
|
14
|
+
const MAG = "\x1b[35m";
|
|
15
|
+
const BBLU = "\x1b[94m";
|
|
16
|
+
const BLU = "\x1b[34m";
|
|
17
|
+
const BCYN = "\x1b[96m";
|
|
18
|
+
const CYN = "\x1b[36m";
|
|
19
|
+
const GRN = "\x1b[32m";
|
|
20
|
+
const YEL = "\x1b[33m";
|
|
21
|
+
const RED = "\x1b[31m";
|
|
22
|
+
const WHT = "\x1b[97m";
|
|
23
|
+
const GRY = "\x1b[90m";
|
|
24
|
+
// ─── ASCII Banner ───────────────────────────────────────────────────────────
|
|
25
|
+
const BANNER = [
|
|
26
|
+
[" ██████ ███████ ███████ ██ ██████ ███████ ", BMAG],
|
|
27
|
+
[" ██ ██ ██ ██ ██ ██ ██ ", BMAG],
|
|
28
|
+
[" ██ ██ █████ █████ ██ ██ █████ ", MAG],
|
|
29
|
+
[" ██ ██ ██ ██ ██ ██ ██ ", MAG],
|
|
30
|
+
[" ██████ ██ ██ ██ ██████ ███████ ", `${MAG}${DIM}`],
|
|
31
|
+
[" ██████ ██████ ██████ ███████ ", BBLU],
|
|
32
|
+
[" ██ ██ ██ ██ ██ ██ ", BBLU],
|
|
33
|
+
[" ██ ██ ██ ██████ █████ ", BLU],
|
|
34
|
+
[" ██ ██ ██ ██ ██ ██ ", BLU],
|
|
35
|
+
[" ██████ ██████ ██ ██ ███████ ", `${BLU}${DIM}`],
|
|
36
|
+
];
|
|
37
|
+
function printBanner() {
|
|
38
|
+
console.log("");
|
|
39
|
+
for (const [text, color] of BANNER) {
|
|
40
|
+
console.log(` ${color}${text}${R}`);
|
|
41
|
+
}
|
|
42
|
+
console.log("");
|
|
43
|
+
}
|
|
44
|
+
function printHeader(label) {
|
|
45
|
+
console.log(` ${BOLD}${WHT}office core${R} ${DIM}-${R} ${BOLD}${label}${R}`);
|
|
46
|
+
}
|
|
47
|
+
// ─── Box Drawing ────────────────────────────────────────────────────────────
|
|
48
|
+
function wordWrap(text, width) {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const para of text.split("\n")) {
|
|
51
|
+
if (!para.trim()) {
|
|
52
|
+
out.push("");
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const words = para.split(/\s+/);
|
|
56
|
+
let line = "";
|
|
57
|
+
for (const w of words) {
|
|
58
|
+
if (line.length + w.length + 1 > width && line) {
|
|
59
|
+
out.push(line);
|
|
60
|
+
line = w;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
line = line ? `${line} ${w}` : w;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (line)
|
|
67
|
+
out.push(line);
|
|
68
|
+
}
|
|
69
|
+
return out.length ? out : [""];
|
|
70
|
+
}
|
|
71
|
+
function drawAgentBox(label, text, maxW = 74) {
|
|
72
|
+
const lines = wordWrap(text.trim(), maxW - 4);
|
|
73
|
+
const inner = Math.max(label.length + 2, ...lines.map((l) => l.length), 20);
|
|
74
|
+
const w = Math.min(inner, maxW - 4);
|
|
75
|
+
const top = `${CYN}┌─ ${R}${BOLD}${BCYN}${label}${R}${CYN} ${"─".repeat(Math.max(0, w - label.length - 1))}┐${R}`;
|
|
76
|
+
const bot = `${CYN}└${"─".repeat(w + 2)}┘${R}`;
|
|
77
|
+
const body = lines.map((l) => `${CYN}│${R} ${WHT}${l.padEnd(w)}${R} ${CYN}│${R}`);
|
|
78
|
+
return [top, ...body, bot].join("\n");
|
|
79
|
+
}
|
|
80
|
+
// ─── Timestamp ──────────────────────────────────────────────────────────────
|
|
81
|
+
function ts() {
|
|
82
|
+
return new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
83
|
+
}
|
|
84
|
+
// ─── Display Room Message ───────────────────────────────────────────────────
|
|
85
|
+
function displayMessage(msg) {
|
|
86
|
+
if (msg.author_type === "system") {
|
|
87
|
+
console.log(`${GRY}[${ts()}] system:${R} ${DIM}${msg.text}${R}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (msg.author_type === "agent") {
|
|
91
|
+
console.log("");
|
|
92
|
+
console.log(drawAgentBox(msg.author_label, msg.text));
|
|
93
|
+
if (msg.reply_to_seq != null)
|
|
94
|
+
console.log(` ${GRY}reply to #${msg.reply_to_seq}${R}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
console.log(`${GRN}[${ts()}]${R} ${BOLD}${msg.author_label ?? "You"}${R}: ${msg.text}`);
|
|
98
|
+
}
|
|
99
|
+
const CMDS = [
|
|
100
|
+
{ name: "status", alias: ["s"], desc: "Host info, runners, active sessions", fn: cmdStatus },
|
|
101
|
+
{ name: "spawn", args: "[codex|claude]", desc: "Spawn a new agent session", fn: cmdSpawn },
|
|
102
|
+
{ name: "stop", args: "[session]", desc: "Stop an agent session", fn: cmdStop },
|
|
103
|
+
{ name: "doctor", alias: ["doc"], desc: "Health checks", fn: cmdDoctor },
|
|
104
|
+
{ name: "config", args: "[set k v]", desc: "Show or update config", fn: cmdConfig },
|
|
105
|
+
{ name: "setup", desc: "Run first-time setup wizard", fn: cmdSetup },
|
|
106
|
+
{ name: "login", args: "<codex|claude>", desc: "Authenticate a runner", fn: cmdLogin },
|
|
107
|
+
{ name: "open", desc: "Open dashboard in browser", fn: cmdOpen },
|
|
108
|
+
{ name: "project", args: "[id]", desc: "Show or switch project", fn: cmdProject },
|
|
109
|
+
{ name: "clear", alias: ["cls"], desc: "Clear screen", fn: cmdClear },
|
|
110
|
+
{ name: "help", alias: ["h", "?"], desc: "Show available commands", fn: cmdHelp },
|
|
111
|
+
{ name: "quit", alias: ["q", "exit"], desc: "Exit", fn: cmdQuit },
|
|
112
|
+
];
|
|
113
|
+
function findCmd(name) {
|
|
114
|
+
const lower = name.toLowerCase();
|
|
115
|
+
return CMDS.find((c) => c.name === lower || c.alias?.includes(lower));
|
|
116
|
+
}
|
|
117
|
+
// ─── Slash Menu Renderer ────────────────────────────────────────────────────
|
|
118
|
+
let _menuLines = 0;
|
|
119
|
+
let _menuSelection = 0;
|
|
120
|
+
function getSlashMatches(partial) {
|
|
121
|
+
const q = partial.toLowerCase();
|
|
122
|
+
return CMDS.filter((c) => c.name.startsWith(q) || c.alias?.some((a) => a.startsWith(q)));
|
|
123
|
+
}
|
|
124
|
+
function renderSlashMenu(partial) {
|
|
125
|
+
clearSlashMenu();
|
|
126
|
+
const hits = getSlashMatches(partial);
|
|
127
|
+
if (hits.length === 0)
|
|
128
|
+
return;
|
|
129
|
+
_menuSelection = Math.max(0, Math.min(_menuSelection, hits.length - 1));
|
|
130
|
+
const lines = hits.map((c) => {
|
|
131
|
+
const args = c.args ? ` ${GRY}${c.args}${R}` : "";
|
|
132
|
+
return c === hits[_menuSelection]
|
|
133
|
+
? ` ${BOLD}${WHT}> /${c.name}${R}${args} ${DIM}${c.desc}${R}`
|
|
134
|
+
: ` ${GRN}/${c.name}${R}${args} ${DIM}${c.desc}${R}`;
|
|
135
|
+
});
|
|
136
|
+
// Save cursor, newline, print menu, restore cursor
|
|
137
|
+
process.stdout.write(`\x1b7\n${lines.join("\n")}\x1b8`);
|
|
138
|
+
_menuLines = lines.length;
|
|
139
|
+
}
|
|
140
|
+
function clearSlashMenu() {
|
|
141
|
+
if (_menuLines === 0)
|
|
142
|
+
return;
|
|
143
|
+
process.stdout.write(`\x1b7\n\x1b[J\x1b8`);
|
|
144
|
+
_menuLines = 0;
|
|
145
|
+
_menuSelection = 0;
|
|
146
|
+
}
|
|
147
|
+
// ─── Tab Completer ──────────────────────────────────────────────────────────
|
|
148
|
+
function completer(line) {
|
|
149
|
+
if (!line.startsWith("/"))
|
|
150
|
+
return [[], line];
|
|
151
|
+
const p = line.slice(1).toLowerCase();
|
|
152
|
+
const hits = [];
|
|
153
|
+
for (const c of CMDS) {
|
|
154
|
+
if (c.name.startsWith(p))
|
|
155
|
+
hits.push(`/${c.name} `);
|
|
156
|
+
for (const a of c.alias ?? []) {
|
|
157
|
+
if (a.startsWith(p))
|
|
158
|
+
hits.push(`/${a} `);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return [hits, line];
|
|
162
|
+
}
|
|
163
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
164
|
+
function ask(rl, prompt) {
|
|
165
|
+
return new Promise((resolve) => rl.question(prompt, (a) => resolve(a?.trim() ?? "")));
|
|
166
|
+
}
|
|
167
|
+
function safePrompt(ctx, preserveCursor = false) {
|
|
168
|
+
if (!ctx.running || !ctx.isInteractiveTerminal) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
ctx.rl.prompt(preserveCursor);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Ignore prompt attempts after readline closes during shutdown/tests.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function replaceInputLine(rl, value) {
|
|
179
|
+
const internal = rl;
|
|
180
|
+
internal.line = value;
|
|
181
|
+
internal.cursor = value.length;
|
|
182
|
+
internal._refreshLine?.();
|
|
183
|
+
}
|
|
184
|
+
function startDaemonInBackground(ctx) {
|
|
185
|
+
if (!ctx.runtime) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
void startDaemonLoop(ctx.runtime).catch((error) => {
|
|
189
|
+
if (!ctx.running) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
console.log(`${RED}Daemon failed: ${error instanceof Error ? error.message : String(error)}${R}`);
|
|
193
|
+
safePrompt(ctx);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function fmtAge(ms) {
|
|
197
|
+
if (ms < 60_000)
|
|
198
|
+
return "<1m";
|
|
199
|
+
if (ms < 3_600_000)
|
|
200
|
+
return `${Math.floor(ms / 60_000)}m`;
|
|
201
|
+
return `${Math.floor(ms / 3_600_000)}h${Math.floor((ms % 3_600_000) / 60_000)}m`;
|
|
202
|
+
}
|
|
203
|
+
function nextNum(runner) {
|
|
204
|
+
return Array.from(sessions.values()).filter((s) => s.runner === runner).length + 1;
|
|
205
|
+
}
|
|
206
|
+
function sessionSummary(session) {
|
|
207
|
+
if (session.status === "failed") {
|
|
208
|
+
return "Needs attention";
|
|
209
|
+
}
|
|
210
|
+
if (session.mode === "attach") {
|
|
211
|
+
return "Awaiting direction";
|
|
212
|
+
}
|
|
213
|
+
if (session.mode === "review") {
|
|
214
|
+
return "Reviewing";
|
|
215
|
+
}
|
|
216
|
+
if (session.mode === "brainstorm") {
|
|
217
|
+
return "Thinking";
|
|
218
|
+
}
|
|
219
|
+
return "Working";
|
|
220
|
+
}
|
|
221
|
+
// ─── Command Handlers ───────────────────────────────────────────────────────
|
|
222
|
+
async function cmdStatus(_a, ctx) {
|
|
223
|
+
if (!ctx.runtime) {
|
|
224
|
+
console.log(`${RED}Not connected. Run /setup${R}`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const cfg = await loadHostConfig();
|
|
228
|
+
const co = probeRunnerAvailability("codex", process.env.CODEX_CMD);
|
|
229
|
+
const cl = probeRunnerAvailability("claude", process.env.CLAUDE_CMD);
|
|
230
|
+
console.log("");
|
|
231
|
+
console.log(` ${BOLD}Host${R} ${cfg?.display_name ?? os.hostname()}`);
|
|
232
|
+
console.log(` ${BOLD}Project${R} ${ctx.runtime.projectId}`);
|
|
233
|
+
console.log(` ${BOLD}Worker${R} ${ctx.runtime.baseUrl}`);
|
|
234
|
+
console.log(` ${BOLD}Poll${R} ${ctx.runtime.pollMs}ms`);
|
|
235
|
+
console.log(` ${BOLD}Runners${R} codex: ${co.available ? `${GRN}ok${R}` : `${RED}missing${R}`} claude: ${cl.available ? `${GRN}ok${R}` : `${RED}missing${R}`}`);
|
|
236
|
+
const list = Array.from(sessions.values());
|
|
237
|
+
if (list.length === 0) {
|
|
238
|
+
console.log(`\n ${DIM}No active sessions${R}`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
console.log(`\n ${BOLD}Sessions${R}`);
|
|
242
|
+
for (const s of list) {
|
|
243
|
+
const age = fmtAge(Date.now() - new Date(s.launched_at).getTime());
|
|
244
|
+
const sc = s.status === "running" ? GRN : s.status === "failed" ? RED : GRY;
|
|
245
|
+
console.log(` ${WHT}${s.agent_id.padEnd(16)}${R} ${GRY}${s.runner.padEnd(8)}${R} ${sc}${s.status.padEnd(10)}${R} ${DIM}${age.padEnd(6)}${R} ${DIM}${sessionSummary(s)}${R}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
console.log("");
|
|
249
|
+
}
|
|
250
|
+
async function cmdSpawn(a, ctx) {
|
|
251
|
+
if (!ctx.runtime) {
|
|
252
|
+
console.log(`${RED}Not connected. Run /setup${R}`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const runner = (a.trim().toLowerCase() || "codex");
|
|
256
|
+
if (runner !== "codex" && runner !== "claude") {
|
|
257
|
+
console.log(`${RED}Usage: /spawn [codex|claude]${R}`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const avail = probeRunnerAvailability(runner, runner === "codex" ? process.env.CODEX_CMD : process.env.CLAUDE_CMD);
|
|
261
|
+
if (!avail.available) {
|
|
262
|
+
console.log(`${RED}${runner} not found on this machine.${R}`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const num = nextNum(runner);
|
|
266
|
+
const agentId = `${runner === "codex" ? "Codex" : "Claude"} #${num}`;
|
|
267
|
+
console.log(`${DIM}Spawning ${agentId}...${R}`);
|
|
268
|
+
const cmd = {
|
|
269
|
+
command_id: `local_${crypto.randomUUID()}`,
|
|
270
|
+
command_type: "spawn",
|
|
271
|
+
runner,
|
|
272
|
+
agent_id: agentId,
|
|
273
|
+
mode: "attach",
|
|
274
|
+
workdir: resolveProjectWorkdir(ctx.runtime.defaultWorkdir, ctx.runtime.projectId),
|
|
275
|
+
};
|
|
276
|
+
try {
|
|
277
|
+
const context = await buildSpawnContext(ctx.runtime, cmd);
|
|
278
|
+
const session = await spawnInteractiveSession(ctx.runtime, cmd, context);
|
|
279
|
+
sessions.set(session.session_id, session);
|
|
280
|
+
await persistSessions();
|
|
281
|
+
await postRoomMessageUpsert(ctx.runtime, {
|
|
282
|
+
message_id: `msg_${crypto.randomUUID()}`,
|
|
283
|
+
author_type: "system",
|
|
284
|
+
author_id: ctx.runtime.hostId,
|
|
285
|
+
author_label: ctx.runtime.displayName,
|
|
286
|
+
text: `${agentId} joined ${ctx.runtime.displayName}`,
|
|
287
|
+
session_id: session.session_id,
|
|
288
|
+
target_agent_ids: [agentId],
|
|
289
|
+
}).catch(() => undefined);
|
|
290
|
+
console.log(`${GRN}${agentId} started.${R}`);
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
console.log(`${RED}Spawn failed: ${e instanceof Error ? e.message : String(e)}${R}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function cmdStop(a, ctx) {
|
|
297
|
+
if (!ctx.runtime) {
|
|
298
|
+
console.log(`${RED}Not connected.${R}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const list = Array.from(sessions.values()).filter((s) => s.status === "running");
|
|
302
|
+
if (list.length === 0) {
|
|
303
|
+
console.log(`${DIM}No active sessions.${R}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
let target;
|
|
307
|
+
const q = a.trim().toLowerCase();
|
|
308
|
+
if (q) {
|
|
309
|
+
target = list.find((s) => s.agent_id.toLowerCase().includes(q) || s.session_id.includes(q));
|
|
310
|
+
if (!target) {
|
|
311
|
+
console.log(`${RED}No session matching "${q}".${R}`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
console.log(`\n ${BOLD}Active sessions:${R}`);
|
|
317
|
+
list.forEach((s, i) => console.log(` ${BOLD}${i + 1}.${R} ${s.agent_id} (${s.runner})`));
|
|
318
|
+
const choice = await ask(ctx.rl, `\n ${DIM}Stop which? (1-${list.length}): ${R}`);
|
|
319
|
+
const idx = parseInt(choice) - 1;
|
|
320
|
+
if (idx < 0 || idx >= list.length) {
|
|
321
|
+
console.log(`${DIM}Cancelled.${R}`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
target = list[idx];
|
|
325
|
+
}
|
|
326
|
+
console.log(`${DIM}Stopping ${target.agent_id}...${R}`);
|
|
327
|
+
await stopSession(ctx.runtime, target.session_id);
|
|
328
|
+
await postRoomMessageUpsert(ctx.runtime, {
|
|
329
|
+
message_id: `msg_${crypto.randomUUID()}`,
|
|
330
|
+
author_type: "system",
|
|
331
|
+
author_id: ctx.runtime.hostId,
|
|
332
|
+
author_label: ctx.runtime.displayName,
|
|
333
|
+
text: `${target.agent_id} left ${ctx.runtime.displayName}`,
|
|
334
|
+
session_id: target.session_id,
|
|
335
|
+
target_agent_ids: [target.agent_id],
|
|
336
|
+
}).catch(() => undefined);
|
|
337
|
+
console.log(`${GRN}${target.agent_id} stopped.${R}`);
|
|
338
|
+
}
|
|
339
|
+
async function cmdDoctor(_a, ctx) {
|
|
340
|
+
const cfg = await loadHostConfig();
|
|
341
|
+
const base = ctx.runtime?.baseUrl ?? cfg?.base_url ?? "http://127.0.0.1:8787";
|
|
342
|
+
const co = probeRunnerAvailability("codex", process.env.CODEX_CMD);
|
|
343
|
+
const cl = probeRunnerAvailability("claude", process.env.CLAUDE_CMD);
|
|
344
|
+
let health;
|
|
345
|
+
try {
|
|
346
|
+
const r = await fetch(`${base}/healthz`, { signal: AbortSignal.timeout(5000) });
|
|
347
|
+
health = r.ok ? `${GRN}ok${R}` : `${RED}${r.status}${R}`;
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
health = `${RED}unreachable${R}`;
|
|
351
|
+
}
|
|
352
|
+
console.log("");
|
|
353
|
+
console.log(` ${BOLD}Doctor${R}`);
|
|
354
|
+
console.log(` Config ${cfg ? `${GRN}present${R}` : `${RED}missing${R}`}`);
|
|
355
|
+
console.log(` Path ${DIM}${getHostConfigPath()}${R}`);
|
|
356
|
+
console.log(` Worker ${health}`);
|
|
357
|
+
console.log(` Codex ${co.available ? `${GRN}ok${R} ${DIM}(${co.command})${R}` : `${RED}missing${R}`}`);
|
|
358
|
+
console.log(` Claude ${cl.available ? `${GRN}ok${R} ${DIM}(${cl.command})${R}` : `${RED}missing${R}`}`);
|
|
359
|
+
if (cfg) {
|
|
360
|
+
console.log(` Host ${cfg.host_id}`);
|
|
361
|
+
console.log(` Project ${cfg.project_id}`);
|
|
362
|
+
console.log(` Workdir ${DIM}${cfg.workdir}${R}`);
|
|
363
|
+
}
|
|
364
|
+
console.log("");
|
|
365
|
+
}
|
|
366
|
+
async function cmdConfig(a, ctx) {
|
|
367
|
+
const cfg = await loadHostConfig();
|
|
368
|
+
if (a.trim().toLowerCase().startsWith("set ")) {
|
|
369
|
+
const parts = a.trim().slice(4).trim().split(/\s+/);
|
|
370
|
+
const [key, ...rest] = parts;
|
|
371
|
+
const val = rest.join(" ");
|
|
372
|
+
if (!key || !val) {
|
|
373
|
+
console.log(`${RED}Usage: /config set <key> <value>${R}`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const o = {};
|
|
377
|
+
if (key === "poll_ms" || key === "poll")
|
|
378
|
+
o.poll_ms = Number(val);
|
|
379
|
+
else if (key === "workdir")
|
|
380
|
+
o.workdir = val;
|
|
381
|
+
else if (key === "display_name" || key === "name")
|
|
382
|
+
o.display_name = val;
|
|
383
|
+
else if (key === "base_url" || key === "url")
|
|
384
|
+
o.base_url = val;
|
|
385
|
+
else {
|
|
386
|
+
console.log(`${RED}Unknown key: ${key}${R} ${DIM}(poll_ms, workdir, display_name, base_url)${R}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
await upsertHostConfig(o);
|
|
390
|
+
console.log(`${GRN}Updated ${key} = ${val}${R}`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (!cfg) {
|
|
394
|
+
console.log(`${DIM}No config. Run /setup${R}`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
console.log("");
|
|
398
|
+
console.log(` ${BOLD}Config${R}`);
|
|
399
|
+
console.log(` host_id ${cfg.host_id}`);
|
|
400
|
+
console.log(` display_name ${cfg.display_name}`);
|
|
401
|
+
console.log(` project_id ${cfg.project_id}`);
|
|
402
|
+
console.log(` base_url ${cfg.base_url}`);
|
|
403
|
+
console.log(` workdir ${cfg.workdir}`);
|
|
404
|
+
console.log(` poll_ms ${cfg.poll_ms}`);
|
|
405
|
+
console.log(` auto_start ${cfg.auto_start}`);
|
|
406
|
+
console.log(` path ${DIM}${getHostConfigPath()}${R}`);
|
|
407
|
+
console.log("");
|
|
408
|
+
}
|
|
409
|
+
async function cmdSetup(_a, ctx) {
|
|
410
|
+
const ex = await loadHostConfig();
|
|
411
|
+
const d = ex ?? { base_url: "http://127.0.0.1:8787", project_id: "prj_local", workdir: process.cwd(), display_name: `${os.hostname()} host`, poll_ms: 900 };
|
|
412
|
+
console.log(`\n ${BOLD}Setup Wizard${R}`);
|
|
413
|
+
console.log(` ${DIM}Press Enter to accept [defaults]${R}\n`);
|
|
414
|
+
const base_url = (await ask(ctx.rl, ` Worker URL [${d.base_url}]: `)) || String(d.base_url);
|
|
415
|
+
const project_id = (await ask(ctx.rl, ` Project ID [${d.project_id}]: `)) || String(d.project_id);
|
|
416
|
+
const workdir = path.resolve((await ask(ctx.rl, ` Workdir [${d.workdir}]: `)) || String(d.workdir));
|
|
417
|
+
const display_name = (await ask(ctx.rl, ` Display name [${d.display_name}]: `)) || String(d.display_name);
|
|
418
|
+
const enrollSecret = (await ask(ctx.rl, ` Enroll secret [office-host-enroll-secret]: `)) || "office-host-enroll-secret";
|
|
419
|
+
console.log(`\n ${DIM}Registering with worker...${R}`);
|
|
420
|
+
try {
|
|
421
|
+
const resp = await fetch(`${base_url}/api/projects/${project_id}/local-host/register`, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: { "content-type": "application/json", "x-enroll-secret": enrollSecret },
|
|
424
|
+
body: JSON.stringify({ display_name, machine_name: os.hostname() }),
|
|
425
|
+
});
|
|
426
|
+
if (!resp.ok)
|
|
427
|
+
throw new Error(`${resp.status} ${await resp.text()}`);
|
|
428
|
+
const reg = (await resp.json());
|
|
429
|
+
await upsertHostConfig({ host_id: reg.host_id, base_url, project_id, workdir, display_name, token: reg.host_token, room_cursor_seq: 0, poll_ms: 900 });
|
|
430
|
+
console.log(` ${GRN}Registered!${R} Host ID: ${reg.host_id}`);
|
|
431
|
+
console.log(` ${DIM}${getHostConfigPath()}${R}`);
|
|
432
|
+
// Connect immediately
|
|
433
|
+
stopDaemonLoop();
|
|
434
|
+
ctx.runtime = await buildRuntimeConfig();
|
|
435
|
+
startDaemonInBackground(ctx);
|
|
436
|
+
console.log(`\n ${GRN}Host online.${R}\n`);
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
console.log(` ${RED}Setup failed: ${e instanceof Error ? e.message : String(e)}${R}\n`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function cmdLogin(a, _ctx) {
|
|
443
|
+
const runner = a.trim().toLowerCase();
|
|
444
|
+
if (runner !== "codex" && runner !== "claude") {
|
|
445
|
+
console.log(`${RED}Usage: /login <codex|claude>${R}`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const cmd = resolveRunnerCommand(runner);
|
|
449
|
+
const args = runner === "codex" ? ["login"] : ["auth"];
|
|
450
|
+
console.log(`${DIM}Running ${runner} auth...${R}`);
|
|
451
|
+
await new Promise((resolve) => {
|
|
452
|
+
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
453
|
+
child.on("close", () => resolve());
|
|
454
|
+
child.on("error", () => { console.log(`${RED}Failed to run ${cmd}${R}`); resolve(); });
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
async function cmdOpen(_a, ctx) {
|
|
458
|
+
const cfg = await loadHostConfig();
|
|
459
|
+
if (!cfg) {
|
|
460
|
+
console.log(`${RED}No config. Run /setup${R}`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const url = `${cfg.base_url}/?projectId=${encodeURIComponent(cfg.project_id)}`;
|
|
464
|
+
spawn("cmd.exe", ["/c", "start", "", url], { detached: true, stdio: "ignore", windowsHide: true }).unref();
|
|
465
|
+
console.log(`${DIM}Opened ${url}${R}`);
|
|
466
|
+
}
|
|
467
|
+
async function cmdProject(a, ctx) {
|
|
468
|
+
if (!ctx.runtime) {
|
|
469
|
+
console.log(`${RED}Not connected.${R}`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (!a.trim()) {
|
|
473
|
+
try {
|
|
474
|
+
const p = await getJson(ctx.runtime, `/api/projects/${ctx.runtime.projectId}`);
|
|
475
|
+
console.log(`\n ${BOLD}Project${R} ${ctx.runtime.projectId}`);
|
|
476
|
+
console.log(` Name ${p?.name ?? ctx.runtime.projectId}`);
|
|
477
|
+
console.log(` Tasks ${p?.active_task_count ?? 0} active`);
|
|
478
|
+
console.log(` Status ${p?.status ?? "unknown"}\n`);
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
console.log(` ${BOLD}Project${R} ${ctx.runtime.projectId} ${DIM}(offline)${R}`);
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
// Dispatch switch_project through the API
|
|
486
|
+
try {
|
|
487
|
+
console.log(`${DIM}Switching to ${a.trim()}...${R}`);
|
|
488
|
+
await postJson(ctx.runtime, `/api/projects/${ctx.runtime.projectId}/local-host/switch`, {
|
|
489
|
+
host_id: ctx.runtime.hostId,
|
|
490
|
+
target_project_id: a.trim(),
|
|
491
|
+
target_project_name: a.trim(),
|
|
492
|
+
});
|
|
493
|
+
console.log(`${GRN}Switch command sent. Will take effect next tick.${R}`);
|
|
494
|
+
}
|
|
495
|
+
catch (e) {
|
|
496
|
+
console.log(`${RED}Switch failed: ${e instanceof Error ? e.message : String(e)}${R}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function cmdClear(_a, _ctx) {
|
|
500
|
+
console.clear();
|
|
501
|
+
printBanner();
|
|
502
|
+
}
|
|
503
|
+
async function cmdHelp(_a, _ctx) {
|
|
504
|
+
console.log("");
|
|
505
|
+
console.log(` ${BOLD}${WHT}Commands${R}`);
|
|
506
|
+
console.log(` ${"─".repeat(56)}`);
|
|
507
|
+
for (const c of CMDS) {
|
|
508
|
+
const a = c.args ? ` ${CYN}${c.args}${R}` : "";
|
|
509
|
+
const al = c.alias ? ` ${GRY}(${c.alias.map((x) => `/${x}`).join(", ")})${R}` : "";
|
|
510
|
+
console.log(` ${GRN}/${c.name.padEnd(10)}${R}${a}${al}`);
|
|
511
|
+
console.log(` ${"".padEnd(12)}${DIM}${c.desc}${R}`);
|
|
512
|
+
}
|
|
513
|
+
console.log(`\n ${DIM}Type without / to send a room message${R}`);
|
|
514
|
+
console.log(` ${DIM}Tab to autocomplete slash commands${R}\n`);
|
|
515
|
+
}
|
|
516
|
+
async function cmdQuit(_a, ctx) {
|
|
517
|
+
ctx.running = false;
|
|
518
|
+
stopDaemonLoop();
|
|
519
|
+
ctx.rl.close();
|
|
520
|
+
}
|
|
521
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
522
|
+
void main().catch((err) => {
|
|
523
|
+
console.error(err);
|
|
524
|
+
process.exitCode = 1;
|
|
525
|
+
});
|
|
526
|
+
async function main() {
|
|
527
|
+
console.clear();
|
|
528
|
+
printBanner();
|
|
529
|
+
const isInteractiveTerminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
530
|
+
const rl = createInterface({
|
|
531
|
+
input: process.stdin,
|
|
532
|
+
output: process.stdout,
|
|
533
|
+
completer,
|
|
534
|
+
terminal: isInteractiveTerminal,
|
|
535
|
+
});
|
|
536
|
+
const ctx = { runtime: null, rl, running: true, isInteractiveTerminal };
|
|
537
|
+
const sentIds = new Set();
|
|
538
|
+
let finishStartup;
|
|
539
|
+
const startupReady = new Promise((resolve) => {
|
|
540
|
+
finishStartup = resolve;
|
|
541
|
+
});
|
|
542
|
+
// ── REPL ──
|
|
543
|
+
if (ctx.isInteractiveTerminal) {
|
|
544
|
+
rl.setPrompt(`${BMAG}>${R} `);
|
|
545
|
+
safePrompt(ctx);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
try {
|
|
549
|
+
rl.resume();
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
// stdin may already be closed in redirected test harnesses
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Live slash menu on keypress
|
|
556
|
+
if (ctx.isInteractiveTerminal) {
|
|
557
|
+
emitKeypressEvents(process.stdin, rl);
|
|
558
|
+
process.stdin.setRawMode?.(true);
|
|
559
|
+
process.stdin.on("keypress", (_chunk, key) => {
|
|
560
|
+
if (!ctx.running)
|
|
561
|
+
return;
|
|
562
|
+
const line = rl.line ?? "";
|
|
563
|
+
if (line.startsWith("/") && line.length >= 1) {
|
|
564
|
+
const hits = getSlashMatches(line.slice(1));
|
|
565
|
+
if (hits.length > 0 && (key?.name === "up" || key?.name === "down")) {
|
|
566
|
+
_menuSelection =
|
|
567
|
+
key.name === "up"
|
|
568
|
+
? (_menuSelection + hits.length - 1) % hits.length
|
|
569
|
+
: (_menuSelection + 1) % hits.length;
|
|
570
|
+
renderSlashMenu(line.slice(1));
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (hits.length > 0 && key?.name === "tab") {
|
|
574
|
+
const selected = hits[_menuSelection] ?? hits[0];
|
|
575
|
+
replaceInputLine(rl, `/${selected.name} `);
|
|
576
|
+
renderSlashMenu(selected.name);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
renderSlashMenu(line.slice(1));
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
clearSlashMenu();
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
rl.on("line", async (raw) => {
|
|
587
|
+
clearSlashMenu();
|
|
588
|
+
const input = raw.trim();
|
|
589
|
+
if (!input) {
|
|
590
|
+
safePrompt(ctx);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
await startupReady;
|
|
594
|
+
if (input.startsWith("/")) {
|
|
595
|
+
const sp = input.indexOf(" ");
|
|
596
|
+
const name = (sp > 0 ? input.slice(1, sp) : input.slice(1)).toLowerCase();
|
|
597
|
+
const args = sp > 0 ? input.slice(sp + 1).trim() : "";
|
|
598
|
+
const cmd = findCmd(name);
|
|
599
|
+
if (cmd) {
|
|
600
|
+
try {
|
|
601
|
+
await cmd.fn(args, ctx);
|
|
602
|
+
}
|
|
603
|
+
catch (e) {
|
|
604
|
+
console.log(`${RED}Error: ${e instanceof Error ? e.message : String(e)}${R}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
const matches = CMDS.filter((c) => c.name.startsWith(name) || c.alias?.some((a) => a.startsWith(name)));
|
|
609
|
+
if (matches.length > 0) {
|
|
610
|
+
console.log(`${DIM}Did you mean:${R}`);
|
|
611
|
+
for (const m of matches)
|
|
612
|
+
console.log(` ${GRN}/${m.name}${R} ${DIM}${m.desc}${R}`);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
console.log(`${RED}Unknown command: /${name}${R} Type ${GRN}/help${R}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
// Send room message
|
|
621
|
+
if (!ctx.runtime) {
|
|
622
|
+
console.log(`${RED}Not connected. Run /setup${R}`);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
const messageId = `msg_${crypto.randomUUID()}`;
|
|
626
|
+
sentIds.add(messageId);
|
|
627
|
+
console.log(`${GRN}[${ts()}]${R} ${BOLD}You${R}: ${input}`);
|
|
628
|
+
try {
|
|
629
|
+
await postRoomMessageUpsert(ctx.runtime, {
|
|
630
|
+
message_id: messageId,
|
|
631
|
+
author_type: "user",
|
|
632
|
+
author_id: ctx.runtime.hostId,
|
|
633
|
+
author_label: "You",
|
|
634
|
+
text: input,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
catch (e) {
|
|
638
|
+
sentIds.delete(messageId);
|
|
639
|
+
console.log(`${RED}Send failed: ${e instanceof Error ? e.message : String(e)}${R}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
safePrompt(ctx);
|
|
644
|
+
});
|
|
645
|
+
rl.on("close", () => {
|
|
646
|
+
ctx.running = false;
|
|
647
|
+
stopDaemonLoop();
|
|
648
|
+
if (ctx.isInteractiveTerminal) {
|
|
649
|
+
process.stdin.setRawMode?.(false);
|
|
650
|
+
}
|
|
651
|
+
console.log(`\n${DIM}Goodbye.${R}`);
|
|
652
|
+
process.exit(0);
|
|
653
|
+
});
|
|
654
|
+
// ── Try to connect ──
|
|
655
|
+
const cfg = await loadHostConfig();
|
|
656
|
+
if (cfg) {
|
|
657
|
+
try {
|
|
658
|
+
ctx.runtime = await buildRuntimeConfig();
|
|
659
|
+
printHeader(ctx.runtime.displayName);
|
|
660
|
+
const co = probeRunnerAvailability("codex", process.env.CODEX_CMD);
|
|
661
|
+
const cl = probeRunnerAvailability("claude", process.env.CLAUDE_CMD);
|
|
662
|
+
const runners = [co.available ? "codex" : null, cl.available ? "claude" : null].filter(Boolean).join(" ");
|
|
663
|
+
console.log(` ${DIM}${ctx.runtime.projectId} | runners: ${runners || "none"} | poll: ${ctx.runtime.pollMs}ms${R}`);
|
|
664
|
+
hooks.onRoomMessage = (msg) => {
|
|
665
|
+
if (!ctx.running)
|
|
666
|
+
return;
|
|
667
|
+
if (sentIds.has(msg.message_id)) {
|
|
668
|
+
sentIds.delete(msg.message_id);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
process.stdout.write(`\r\x1b[K`);
|
|
672
|
+
displayMessage(msg);
|
|
673
|
+
safePrompt(ctx, true);
|
|
674
|
+
};
|
|
675
|
+
hooks.onSessionChange = () => {
|
|
676
|
+
// Session list changed - could update status bar
|
|
677
|
+
};
|
|
678
|
+
startDaemonInBackground(ctx);
|
|
679
|
+
console.log(` ${GRN}Host online.${R} Type ${GRN}/help${R} for commands.\n`);
|
|
680
|
+
safePrompt(ctx);
|
|
681
|
+
}
|
|
682
|
+
catch (e) {
|
|
683
|
+
console.log(` ${RED}Connection failed: ${e instanceof Error ? e.message : String(e)}${R}`);
|
|
684
|
+
console.log(` ${DIM}Run /setup to configure or /doctor to diagnose.${R}\n`);
|
|
685
|
+
safePrompt(ctx);
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
finishStartup();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
printHeader(os.hostname());
|
|
693
|
+
console.log(` ${YEL}No host config found.${R}`);
|
|
694
|
+
console.log(` ${DIM}Run /setup to get started or /doctor to check prerequisites.${R}\n`);
|
|
695
|
+
safePrompt(ctx);
|
|
696
|
+
finishStartup();
|
|
697
|
+
}
|
|
698
|
+
}
|