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,28 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { loadHostConfig, getHostConfigPath } from "./lib/host-config.js";
|
|
3
|
+
import { probeRunnerAvailability } from "./lib/local-runner.js";
|
|
4
|
+
void main().catch((error) => {
|
|
5
|
+
console.error(error);
|
|
6
|
+
process.exitCode = 1;
|
|
7
|
+
});
|
|
8
|
+
async function main() {
|
|
9
|
+
const config = await loadHostConfig();
|
|
10
|
+
const baseUrl = config?.base_url ?? "http://127.0.0.1:8787";
|
|
11
|
+
const codex = probeRunnerAvailability("codex", process.env.CODEX_CMD);
|
|
12
|
+
const claude = probeRunnerAvailability("claude", process.env.CLAUDE_CMD);
|
|
13
|
+
const health = await fetch(`${baseUrl}/healthz`)
|
|
14
|
+
.then(async (response) => (response.ok ? "ok" : `${response.status} ${await response.text()}`))
|
|
15
|
+
.catch(() => "unreachable");
|
|
16
|
+
console.log("office host doctor");
|
|
17
|
+
console.log(`Config: ${config ? "present" : "missing"}`);
|
|
18
|
+
console.log(`Config path: ${getHostConfigPath()}`);
|
|
19
|
+
console.log(`Worker health: ${health}`);
|
|
20
|
+
console.log(`Codex: ${codex.available ? `ok (${codex.command})` : "missing"}`);
|
|
21
|
+
console.log(`Claude: ${claude.available ? `ok (${claude.command})` : "missing"}`);
|
|
22
|
+
if (config) {
|
|
23
|
+
console.log(`Host id: ${config.host_id}`);
|
|
24
|
+
console.log(`Project: ${config.project_id}`);
|
|
25
|
+
console.log(`Workdir: ${config.workdir}`);
|
|
26
|
+
console.log(`Base URL: ${config.base_url}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { resolveRunnerCommand } from "./lib/local-runner.js";
|
|
4
|
+
void main().catch((error) => {
|
|
5
|
+
console.error(error);
|
|
6
|
+
process.exitCode = 1;
|
|
7
|
+
});
|
|
8
|
+
async function main() {
|
|
9
|
+
const provider = normalizeProvider(process.argv[2]);
|
|
10
|
+
const command = resolveRunnerCommand(provider);
|
|
11
|
+
const args = provider === "codex" ? ["login"] : ["auth"];
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
const child = spawn(command, args, {
|
|
14
|
+
stdio: "inherit",
|
|
15
|
+
windowsHide: false,
|
|
16
|
+
});
|
|
17
|
+
child.on("close", (code) => {
|
|
18
|
+
if ((code ?? 0) !== 0) {
|
|
19
|
+
reject(new Error(`${provider} login exited with code ${code}`));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
resolve();
|
|
23
|
+
});
|
|
24
|
+
child.on("error", reject);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function normalizeProvider(value) {
|
|
28
|
+
if (value === "codex" || value === "claude") {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
throw new Error("Usage: office-core login <codex|claude>");
|
|
32
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { loadHostConfig, upsertHostConfig, getHostConfigPath } from "./lib/host-config.js";
|
|
9
|
+
import { probeRunnerAvailability, resolveRunnerCommand } from "./lib/local-runner.js";
|
|
10
|
+
const rl = createInterface({ input, output });
|
|
11
|
+
void main().catch((error) => {
|
|
12
|
+
console.error(error);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
}).finally(async () => {
|
|
15
|
+
await rl.close();
|
|
16
|
+
});
|
|
17
|
+
async function main() {
|
|
18
|
+
console.clear();
|
|
19
|
+
printBanner();
|
|
20
|
+
for (;;) {
|
|
21
|
+
const config = await loadHostConfig();
|
|
22
|
+
printSummary(config);
|
|
23
|
+
const choice = (await safeQuestion([
|
|
24
|
+
"",
|
|
25
|
+
"1. First-time install / join this machine",
|
|
26
|
+
"2. Log into Codex",
|
|
27
|
+
"3. Log into Claude",
|
|
28
|
+
"4. Start configured host",
|
|
29
|
+
"5. Open office in browser",
|
|
30
|
+
"6. Doctor",
|
|
31
|
+
"7. Show current machine config",
|
|
32
|
+
"8. Exit",
|
|
33
|
+
"",
|
|
34
|
+
"Select an option: ",
|
|
35
|
+
].join("\n"))).trim();
|
|
36
|
+
if (choice === "1") {
|
|
37
|
+
await runInstallFlow(config);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (choice === "2") {
|
|
41
|
+
await runInteractiveCommand(resolveRunnerCommand("codex"), ["login"]);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (choice === "3") {
|
|
45
|
+
await runInteractiveCommand(resolveRunnerCommand("claude"), ["auth"]);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (choice === "4") {
|
|
49
|
+
await startConfiguredHost(config);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (choice === "5") {
|
|
53
|
+
await openBrowser(config);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (choice === "6") {
|
|
57
|
+
await runDoctor(config);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (choice === "7") {
|
|
61
|
+
printConfig(config);
|
|
62
|
+
await waitForEnter();
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (choice === "8" || choice.toLowerCase() === "q") {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function runInstallFlow(existing) {
|
|
71
|
+
const defaults = existing ?? {
|
|
72
|
+
base_url: "http://127.0.0.1:8787",
|
|
73
|
+
project_id: "prj_local",
|
|
74
|
+
workdir: process.cwd(),
|
|
75
|
+
display_name: `${os.hostname()} host`,
|
|
76
|
+
token: "",
|
|
77
|
+
room_cursor_seq: 0,
|
|
78
|
+
poll_ms: 5000,
|
|
79
|
+
auto_start: true,
|
|
80
|
+
};
|
|
81
|
+
const base_url = await ask("Cloudflare/Worker base URL", defaults.base_url);
|
|
82
|
+
const project_id = await ask("Project id", defaults.project_id);
|
|
83
|
+
const workdir = path.resolve(await ask("Workdir", defaults.workdir));
|
|
84
|
+
const display_name = await ask("Display name", defaults.display_name);
|
|
85
|
+
const enrollSecret = await ask("Enrollment secret", process.env.OFFICE_HOST_ENROLL_SECRET || process.env.DOUBLE_PENETRATION_ENROLL_SECRET || process.env.LOCAL_HOST_ENROLL_SECRET || "office-host-enroll-secret");
|
|
86
|
+
const poll_ms = Number(await ask("Poll interval (ms)", String(defaults.poll_ms)));
|
|
87
|
+
const auto_start = (await ask("Install startup launcher? (y/n)", defaults.auto_start ? "y" : "n")).toLowerCase().startsWith("y");
|
|
88
|
+
const registration = await fetch(`${base_url}/api/projects/${project_id}/local-host/register`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"content-type": "application/json",
|
|
92
|
+
"x-enroll-secret": enrollSecret,
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
display_name,
|
|
96
|
+
machine_name: os.hostname(),
|
|
97
|
+
}),
|
|
98
|
+
}).then(async (response) => {
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new Error(`Host registration failed: ${response.status} ${await response.text()}`);
|
|
101
|
+
}
|
|
102
|
+
return (await response.json());
|
|
103
|
+
});
|
|
104
|
+
const config = await upsertHostConfig({
|
|
105
|
+
host_id: registration.host_id,
|
|
106
|
+
base_url,
|
|
107
|
+
project_id,
|
|
108
|
+
workdir,
|
|
109
|
+
display_name,
|
|
110
|
+
token: registration.host_token,
|
|
111
|
+
room_cursor_seq: 0,
|
|
112
|
+
poll_ms,
|
|
113
|
+
auto_start,
|
|
114
|
+
});
|
|
115
|
+
console.log("");
|
|
116
|
+
console.log(`Saved machine config to ${getHostConfigPath()}`);
|
|
117
|
+
console.log(`Host id: ${config.host_id}`);
|
|
118
|
+
console.log(`Project: ${config.project_id}`);
|
|
119
|
+
console.log(`Workdir: ${config.workdir}`);
|
|
120
|
+
console.log(`Auto-start: ${config.auto_start ? "enabled" : "disabled"}`);
|
|
121
|
+
await waitForEnter();
|
|
122
|
+
}
|
|
123
|
+
async function startConfiguredHost(config) {
|
|
124
|
+
if (!config) {
|
|
125
|
+
console.log("");
|
|
126
|
+
console.log("No machine config found. Run option 1 first.");
|
|
127
|
+
await waitForEnter();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
131
|
+
spawn(process.execPath, [path.join(root, "bin", "double-penetration-host.mjs"), "start"], {
|
|
132
|
+
cwd: root,
|
|
133
|
+
detached: true,
|
|
134
|
+
stdio: "ignore",
|
|
135
|
+
windowsHide: false,
|
|
136
|
+
}).unref();
|
|
137
|
+
console.log("");
|
|
138
|
+
console.log(`Started configured host for ${config.project_id}.`);
|
|
139
|
+
await waitForEnter();
|
|
140
|
+
}
|
|
141
|
+
async function openBrowser(config) {
|
|
142
|
+
if (!config) {
|
|
143
|
+
console.log("");
|
|
144
|
+
console.log("No machine config found. Run option 1 first.");
|
|
145
|
+
await waitForEnter();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const url = `${config.base_url}/?projectId=${encodeURIComponent(config.project_id)}`;
|
|
149
|
+
spawn("cmd.exe", ["/c", "start", "", url], {
|
|
150
|
+
detached: true,
|
|
151
|
+
stdio: "ignore",
|
|
152
|
+
windowsHide: true,
|
|
153
|
+
}).unref();
|
|
154
|
+
console.log("");
|
|
155
|
+
console.log(`Opened ${url}`);
|
|
156
|
+
await waitForEnter();
|
|
157
|
+
}
|
|
158
|
+
async function runDoctor(config) {
|
|
159
|
+
const codex = probeRunnerAvailability("codex", process.env.CODEX_CMD);
|
|
160
|
+
const claude = probeRunnerAvailability("claude", process.env.CLAUDE_CMD);
|
|
161
|
+
const base_url = config?.base_url ?? "http://127.0.0.1:8787";
|
|
162
|
+
const health = await fetch(`${base_url}/healthz`).then(async (response) => (response.ok ? "ok" : `${response.status}`)).catch(() => "unreachable");
|
|
163
|
+
console.log("");
|
|
164
|
+
console.log("Doctor");
|
|
165
|
+
console.log(`Config: ${config ? "present" : "missing"}`);
|
|
166
|
+
console.log(`Config path: ${getHostConfigPath()}`);
|
|
167
|
+
console.log(`Worker health: ${health}`);
|
|
168
|
+
console.log(`Codex: ${codex.available ? `ok (${codex.command})` : "missing"}`);
|
|
169
|
+
console.log(`Claude: ${claude.available ? `ok (${claude.command})` : "missing"}`);
|
|
170
|
+
if (config) {
|
|
171
|
+
console.log(`Project: ${config.project_id}`);
|
|
172
|
+
console.log(`Workdir: ${config.workdir}`);
|
|
173
|
+
console.log(`Base URL: ${config.base_url}`);
|
|
174
|
+
}
|
|
175
|
+
await waitForEnter();
|
|
176
|
+
}
|
|
177
|
+
function printBanner() {
|
|
178
|
+
console.log("==============================================");
|
|
179
|
+
console.log("office core menu");
|
|
180
|
+
console.log("==============================================");
|
|
181
|
+
}
|
|
182
|
+
function printSummary(config) {
|
|
183
|
+
console.log("");
|
|
184
|
+
console.log(`Machine: ${os.hostname()}`);
|
|
185
|
+
console.log(`Config: ${config ? "installed" : "not installed"}`);
|
|
186
|
+
if (config) {
|
|
187
|
+
console.log(`Host id: ${config.host_id}`);
|
|
188
|
+
console.log(`Project: ${config.project_id}`);
|
|
189
|
+
console.log(`Base URL: ${config.base_url}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function printConfig(config) {
|
|
193
|
+
console.log("");
|
|
194
|
+
if (!config) {
|
|
195
|
+
console.log("No machine config installed.");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
console.log(JSON.stringify(config, null, 2));
|
|
199
|
+
}
|
|
200
|
+
async function runInteractiveCommand(command, args) {
|
|
201
|
+
await new Promise((resolve) => {
|
|
202
|
+
const child = spawn(command, args, {
|
|
203
|
+
stdio: "inherit",
|
|
204
|
+
windowsHide: false,
|
|
205
|
+
});
|
|
206
|
+
child.on("close", () => resolve());
|
|
207
|
+
child.on("error", () => resolve());
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function ask(label, defaultValue) {
|
|
211
|
+
const answer = (await safeQuestion(`${label} [${defaultValue}]: `)).trim();
|
|
212
|
+
return answer || defaultValue;
|
|
213
|
+
}
|
|
214
|
+
async function waitForEnter() {
|
|
215
|
+
await safeQuestion("\nPress Enter to continue...");
|
|
216
|
+
}
|
|
217
|
+
async function safeQuestion(prompt) {
|
|
218
|
+
try {
|
|
219
|
+
return await rl.question(prompt);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error instanceof Error && "code" in error && error.code === "ERR_USE_AFTER_CLOSE") {
|
|
223
|
+
return "8";
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { loadHostConfig } from "./lib/host-config.js";
|
|
4
|
+
void main().catch((error) => {
|
|
5
|
+
console.error(error);
|
|
6
|
+
process.exitCode = 1;
|
|
7
|
+
});
|
|
8
|
+
async function main() {
|
|
9
|
+
const config = await loadHostConfig();
|
|
10
|
+
if (!config) {
|
|
11
|
+
throw new Error("No host config found. Run office-core install first.");
|
|
12
|
+
}
|
|
13
|
+
const url = `${config.base_url}/?projectId=${encodeURIComponent(config.project_id)}`;
|
|
14
|
+
spawn("cmd.exe", ["/c", "start", "", url], {
|
|
15
|
+
detached: true,
|
|
16
|
+
stdio: "ignore",
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
}).unref();
|
|
19
|
+
console.log(`Opened ${url}`);
|
|
20
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { getHostConfigPath, getStartupLauncherPath, upsertHostConfig, } from "./lib/host-config.js";
|
|
7
|
+
const args = parseArgs(process.argv.slice(2));
|
|
8
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
void main().catch((error) => {
|
|
10
|
+
console.error(error);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
});
|
|
13
|
+
async function main() {
|
|
14
|
+
const baseUrl = (args.baseUrl ?? "http://127.0.0.1:8787").replace(/\/$/, "");
|
|
15
|
+
const projectId = args.project ?? "prj_local";
|
|
16
|
+
const requestedHostId = args.hostId;
|
|
17
|
+
const displayName = args.displayName ?? `${os.hostname()} host`;
|
|
18
|
+
const machineName = os.hostname();
|
|
19
|
+
const enrollSecret = args.enrollSecret ??
|
|
20
|
+
process.env.OFFICE_HOST_ENROLL_SECRET ??
|
|
21
|
+
process.env.DOUBLE_PENETRATION_ENROLL_SECRET ??
|
|
22
|
+
process.env.LOCAL_HOST_ENROLL_SECRET ??
|
|
23
|
+
"office-host-enroll-secret";
|
|
24
|
+
const registration = args.token
|
|
25
|
+
? {
|
|
26
|
+
ok: true,
|
|
27
|
+
host_id: requestedHostId ?? `host_${machineName.toLowerCase()}`,
|
|
28
|
+
host_token: args.token,
|
|
29
|
+
project_id: projectId,
|
|
30
|
+
}
|
|
31
|
+
: await registerHost(baseUrl, projectId, {
|
|
32
|
+
requested_host_id: requestedHostId,
|
|
33
|
+
display_name: displayName,
|
|
34
|
+
machine_name: machineName,
|
|
35
|
+
}, enrollSecret);
|
|
36
|
+
const config = await upsertHostConfig({
|
|
37
|
+
host_id: registration.host_id,
|
|
38
|
+
base_url: baseUrl,
|
|
39
|
+
project_id: registration.project_id,
|
|
40
|
+
workdir: args.workdir,
|
|
41
|
+
display_name: displayName,
|
|
42
|
+
token: registration.host_token,
|
|
43
|
+
room_cursor_seq: 0,
|
|
44
|
+
poll_ms: args.pollMs ? Number(args.pollMs) : undefined,
|
|
45
|
+
auto_start: args.autoStart ? args.autoStart !== "false" : undefined,
|
|
46
|
+
});
|
|
47
|
+
if (config.auto_start) {
|
|
48
|
+
await installStartupLauncher(root);
|
|
49
|
+
}
|
|
50
|
+
console.log(`Host config written: ${getHostConfigPath()}`);
|
|
51
|
+
console.log(`Host id: ${config.host_id}`);
|
|
52
|
+
console.log(`Project: ${config.project_id}`);
|
|
53
|
+
console.log(`Workdir: ${config.workdir}`);
|
|
54
|
+
console.log(`Base URL: ${config.base_url}`);
|
|
55
|
+
console.log(`Auto-start: ${config.auto_start ? "enabled" : "disabled"}`);
|
|
56
|
+
}
|
|
57
|
+
async function registerHost(baseUrl, projectId, body, enrollSecret) {
|
|
58
|
+
const response = await fetch(`${baseUrl}/api/projects/${projectId}/local-host/register`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
"content-type": "application/json",
|
|
62
|
+
"x-enroll-secret": enrollSecret,
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify(body),
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Host registration failed: ${response.status} ${await response.text()}`);
|
|
68
|
+
}
|
|
69
|
+
return response.json();
|
|
70
|
+
}
|
|
71
|
+
async function installStartupLauncher(rootDir) {
|
|
72
|
+
const startupPath = getStartupLauncherPath();
|
|
73
|
+
await mkdir(path.dirname(startupPath), { recursive: true });
|
|
74
|
+
const launcher = [
|
|
75
|
+
"@echo off",
|
|
76
|
+
"setlocal",
|
|
77
|
+
`cd /d "${escapeBatch(rootDir)}"`,
|
|
78
|
+
'if not exist "node_modules" (',
|
|
79
|
+
" call npm install",
|
|
80
|
+
" if errorlevel 1 exit /b 1",
|
|
81
|
+
")",
|
|
82
|
+
`start "office core" cmd /c "cd /d ""${escapeBatch(rootDir)}"" && npm run host-configured"`,
|
|
83
|
+
"endlocal",
|
|
84
|
+
"",
|
|
85
|
+
].join("\r\n");
|
|
86
|
+
await writeFile(startupPath, launcher, "utf8");
|
|
87
|
+
}
|
|
88
|
+
function parseArgs(argv) {
|
|
89
|
+
const result = {};
|
|
90
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
91
|
+
const item = argv[i];
|
|
92
|
+
if (!item.startsWith("--")) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const key = item.slice(2);
|
|
96
|
+
const value = argv[i + 1];
|
|
97
|
+
if (!value || value.startsWith("--")) {
|
|
98
|
+
result[key] = "true";
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
result[key] = value;
|
|
102
|
+
i += 1;
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
function escapeBatch(value) {
|
|
107
|
+
return value.replace(/"/g, '""');
|
|
108
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const HOST_CONFIG_VERSION = 1;
|
|
6
|
+
const HOST_CONFIG_DIRNAME = "office-host";
|
|
7
|
+
const LEGACY_HOST_CONFIG_DIRNAME = "double-penetration";
|
|
8
|
+
const HOST_CONFIG_FILENAME = "host-config.json";
|
|
9
|
+
const HOST_SESSIONS_FILENAME = "host-sessions.json";
|
|
10
|
+
const DEFAULT_LOCAL_HOST_TOKEN = "office-host-local-token";
|
|
11
|
+
export function getHostConfigDir() {
|
|
12
|
+
return path.join(baseAppDataDir(), HOST_CONFIG_DIRNAME);
|
|
13
|
+
}
|
|
14
|
+
export function getHostConfigPath() {
|
|
15
|
+
return path.join(getHostConfigDir(), HOST_CONFIG_FILENAME);
|
|
16
|
+
}
|
|
17
|
+
export function getHostSessionsPath() {
|
|
18
|
+
return path.join(getHostConfigDir(), HOST_SESSIONS_FILENAME);
|
|
19
|
+
}
|
|
20
|
+
export function getLegacyHostConfigDir() {
|
|
21
|
+
return path.join(baseAppDataDir(), LEGACY_HOST_CONFIG_DIRNAME);
|
|
22
|
+
}
|
|
23
|
+
export async function loadHostConfig() {
|
|
24
|
+
const filePath = firstExistingPath([
|
|
25
|
+
path.join(getHostConfigDir(), HOST_CONFIG_FILENAME),
|
|
26
|
+
path.join(getLegacyHostConfigDir(), HOST_CONFIG_FILENAME),
|
|
27
|
+
]);
|
|
28
|
+
if (!filePath) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const raw = await readFile(filePath, "utf8");
|
|
32
|
+
const parsed = parseLooseJson(raw);
|
|
33
|
+
if (!parsed || parsed.version !== HOST_CONFIG_VERSION) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return normalizeConfig(parsed);
|
|
37
|
+
}
|
|
38
|
+
export async function upsertHostConfig(overrides) {
|
|
39
|
+
const existing = await loadHostConfig();
|
|
40
|
+
const now = new Date().toISOString();
|
|
41
|
+
const machineName = os.hostname();
|
|
42
|
+
const next = normalizeConfig({
|
|
43
|
+
version: HOST_CONFIG_VERSION,
|
|
44
|
+
host_id: overrides.host_id ?? existing?.host_id ?? `host_${sanitize(machineName)}_${crypto.randomUUID().slice(0, 8)}`,
|
|
45
|
+
display_name: overrides.display_name ?? existing?.display_name ?? `${machineName} host`,
|
|
46
|
+
machine_name: machineName,
|
|
47
|
+
base_url: overrides.base_url ?? existing?.base_url ?? "http://127.0.0.1:8787",
|
|
48
|
+
project_id: overrides.project_id ?? existing?.project_id ?? "prj_local",
|
|
49
|
+
workdir: path.resolve(overrides.workdir ?? existing?.workdir ?? process.cwd()),
|
|
50
|
+
token: overrides.token ?? existing?.token ?? DEFAULT_LOCAL_HOST_TOKEN,
|
|
51
|
+
room_cursor_seq: overrides.room_cursor_seq ?? existing?.room_cursor_seq ?? 0,
|
|
52
|
+
poll_ms: overrides.poll_ms ?? existing?.poll_ms ?? 5000,
|
|
53
|
+
auto_start: overrides.auto_start ?? existing?.auto_start ?? true,
|
|
54
|
+
created_at: existing?.created_at ?? now,
|
|
55
|
+
updated_at: now,
|
|
56
|
+
});
|
|
57
|
+
await mkdir(getHostConfigDir(), { recursive: true });
|
|
58
|
+
await writeAtomicJson(getHostConfigPath(), next);
|
|
59
|
+
return next;
|
|
60
|
+
}
|
|
61
|
+
export async function loadPersistedHostSessions() {
|
|
62
|
+
const filePath = firstExistingPath([
|
|
63
|
+
path.join(getHostConfigDir(), HOST_SESSIONS_FILENAME),
|
|
64
|
+
path.join(getLegacyHostConfigDir(), HOST_SESSIONS_FILENAME),
|
|
65
|
+
]);
|
|
66
|
+
if (!filePath) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const raw = await readFile(filePath, "utf8");
|
|
71
|
+
const parsed = JSON.parse(raw);
|
|
72
|
+
if (!Array.isArray(parsed)) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return parsed
|
|
76
|
+
.filter((entry) => entry && typeof entry === "object" && entry.session_id)
|
|
77
|
+
.map((entry) => normalizePersistedSession(entry));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function savePersistedHostSessions(sessions) {
|
|
84
|
+
await mkdir(getHostConfigDir(), { recursive: true });
|
|
85
|
+
const next = sessions.map((session) => normalizePersistedSession(session));
|
|
86
|
+
await writeAtomicJson(getHostSessionsPath(), next);
|
|
87
|
+
}
|
|
88
|
+
export function getStartupLauncherPath() {
|
|
89
|
+
return path.join(baseAppDataDir(), "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "office-host.bat");
|
|
90
|
+
}
|
|
91
|
+
function normalizeConfig(input) {
|
|
92
|
+
return {
|
|
93
|
+
version: HOST_CONFIG_VERSION,
|
|
94
|
+
host_id: String(input.host_id ?? "").trim(),
|
|
95
|
+
display_name: String(input.display_name ?? "").trim(),
|
|
96
|
+
machine_name: String(input.machine_name ?? os.hostname()).trim(),
|
|
97
|
+
base_url: String(input.base_url ?? "http://127.0.0.1:8787").trim().replace(/\/$/, ""),
|
|
98
|
+
project_id: String(input.project_id ?? "prj_local").trim(),
|
|
99
|
+
workdir: path.resolve(String(input.workdir ?? process.cwd())),
|
|
100
|
+
token: String(input.token ?? DEFAULT_LOCAL_HOST_TOKEN).trim(),
|
|
101
|
+
room_cursor_seq: Number(input.room_cursor_seq ?? 0),
|
|
102
|
+
poll_ms: Number(input.poll_ms ?? 5000),
|
|
103
|
+
auto_start: Boolean(input.auto_start ?? true),
|
|
104
|
+
created_at: String(input.created_at ?? new Date().toISOString()),
|
|
105
|
+
updated_at: String(input.updated_at ?? new Date().toISOString()),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function normalizePersistedSession(input) {
|
|
109
|
+
return {
|
|
110
|
+
session_id: String(input.session_id ?? "").trim(),
|
|
111
|
+
project_id: input.project_id ? String(input.project_id) : null,
|
|
112
|
+
runner: input.runner === "claude" ? "claude" : "codex",
|
|
113
|
+
agent_id: String(input.agent_id ?? "").trim(),
|
|
114
|
+
mode: input.mode === "execute" || input.mode === "review" || input.mode === "attach" ? input.mode : "brainstorm",
|
|
115
|
+
workdir: path.resolve(String(input.workdir ?? process.cwd())),
|
|
116
|
+
status: input.status === "completed" || input.status === "failed" ? input.status : "running",
|
|
117
|
+
launched_at: String(input.launched_at ?? new Date().toISOString()),
|
|
118
|
+
task_id: input.task_id ? String(input.task_id) : null,
|
|
119
|
+
effort: input.effort === "low" || input.effort === "medium" || input.effort === "max" || input.effort === "high"
|
|
120
|
+
? input.effort
|
|
121
|
+
: "high",
|
|
122
|
+
shell_pid: Number.isFinite(Number(input.shell_pid)) ? Number(input.shell_pid) : null,
|
|
123
|
+
script_dir: input.script_dir ? String(input.script_dir) : null,
|
|
124
|
+
note: input.note ? String(input.note) : null,
|
|
125
|
+
bootstrap: input.bootstrap
|
|
126
|
+
? {
|
|
127
|
+
session_id: String(input.bootstrap.session_id ?? "").trim(),
|
|
128
|
+
session_epoch: Number(input.bootstrap.session_epoch ?? 0),
|
|
129
|
+
session_token: String(input.bootstrap.session_token ?? ""),
|
|
130
|
+
bundle_seq: Number(input.bootstrap.bundle_seq ?? 1),
|
|
131
|
+
task_version: Number(input.bootstrap.task_version ?? 1),
|
|
132
|
+
}
|
|
133
|
+
: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function sanitize(value) {
|
|
137
|
+
return value.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
function firstExistingPath(paths) {
|
|
140
|
+
for (const filePath of paths) {
|
|
141
|
+
if (existsSync(filePath)) {
|
|
142
|
+
return filePath;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
function parseLooseJson(raw) {
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(raw);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
for (let index = raw.lastIndexOf("}"); index >= 0; index = raw.lastIndexOf("}", index - 1)) {
|
|
153
|
+
const trimmed = raw.slice(0, index + 1).trim();
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(trimmed);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// keep walking backward until we hit the real JSON terminator
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
throw new SyntaxError("Unable to parse host config JSON");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function writeAtomicJson(filePath, value) {
|
|
165
|
+
const tempPath = `${filePath}.tmp`;
|
|
166
|
+
await writeFile(tempPath, JSON.stringify(value, null, 2), "utf8");
|
|
167
|
+
await rename(tempPath, filePath);
|
|
168
|
+
}
|
|
169
|
+
function baseAppDataDir() {
|
|
170
|
+
return process.env.APPDATA ?? path.join(process.env.USERPROFILE ?? os.homedir(), "AppData", "Roaming");
|
|
171
|
+
}
|