agent-companion 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.
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { printAgentCompanionBanner } from "./banner.mjs";
7
+
8
+ const argv = process.argv.slice(2);
9
+ const args = parseArgs(argv);
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const projectRoot = path.resolve(__dirname, "..");
13
+
14
+ const bridgePort = clampInt(args["bridge-port"], 8787, 1, 65535);
15
+ const relayPort = clampInt(args["relay-port"], 9797, 1, 65535);
16
+ const withLocalRelay = toBool(args["with-local-relay"]);
17
+ const defaultPublicRelayUrl = "https://agent-companion-relay.onrender.com";
18
+ const relayUrl = withLocalRelay
19
+ ? trim(args.relay) || trim(process.env.AGENT_RELAY_URL) || `http://localhost:${relayPort}`
20
+ : trim(args.relay) || trim(process.env.AGENT_RELAY_URL) || defaultPublicRelayUrl;
21
+ const companionName = trim(args.name);
22
+ const companionStateFile = trim(args["state-file"]);
23
+ const bridgeToken = trim(args["bridge-token"]) || trim(process.env.AGENT_BRIDGE_TOKEN);
24
+ const wakeMac = trim(args["wake-mac"]) || trim(args.wakeMac) || trim(process.env.AGENT_WAKE_MAC);
25
+ const verbose = toBool(args.verbose) || toBool(process.env.AGENT_VERBOSE);
26
+ const keepAwakeEnabled = !toBool(args["allow-sleep"]) && !toBool(process.env.AGENT_ALLOW_SLEEP);
27
+
28
+ const bridgeBaseUrl = `http://localhost:${bridgePort}`;
29
+ const childSpecs = [];
30
+
31
+ printAgentCompanionBanner();
32
+
33
+ childSpecs.push({
34
+ name: "bridge",
35
+ cmd: process.execPath,
36
+ args: ["bridge/server.mjs"],
37
+ env: {
38
+ ...process.env,
39
+ AGENT_BRIDGE_PORT: String(bridgePort),
40
+ ...(bridgeToken ? { AGENT_BRIDGE_TOKEN: bridgeToken } : {})
41
+ }
42
+ });
43
+
44
+ if (withLocalRelay) {
45
+ childSpecs.push({
46
+ name: "relay",
47
+ cmd: process.execPath,
48
+ args: ["relay/server.mjs"],
49
+ env: {
50
+ ...process.env,
51
+ RELAY_PORT: String(relayPort),
52
+ RELAY_PUBLIC_URL: trim(args["relay-public-url"]) || process.env.RELAY_PUBLIC_URL || `http://localhost:${relayPort}`
53
+ }
54
+ });
55
+ }
56
+
57
+ const children = [];
58
+ let shuttingDown = false;
59
+
60
+ if (keepAwakeEnabled) {
61
+ const keepAwake = startKeepAwakeProcess({ verbose });
62
+ if (keepAwake) {
63
+ children.push({ name: keepAwake.name, child: keepAwake.child });
64
+ }
65
+ }
66
+
67
+ process.on("SIGINT", () => shutdown("SIGINT"));
68
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
69
+
70
+ for (const spec of childSpecs) {
71
+ const child = spawn(spec.cmd, spec.args, {
72
+ cwd: projectRoot,
73
+ env: spec.env,
74
+ stdio: ["ignore", "pipe", "pipe"]
75
+ });
76
+ attachLogs(spec.name, child);
77
+ children.push({ name: spec.name, child });
78
+ }
79
+
80
+ await waitForHealth(`${bridgeBaseUrl}/health`, 15_000, "bridge");
81
+ if (withLocalRelay) {
82
+ await waitForHealth(`${relayUrl}/health`, 15_000, "relay");
83
+ }
84
+
85
+ const companionArgs = [
86
+ "scripts/laptop-companion.mjs",
87
+ "--bridge",
88
+ bridgeBaseUrl,
89
+ "--relay",
90
+ relayUrl
91
+ ];
92
+
93
+ if (companionName) {
94
+ companionArgs.push("--name", companionName);
95
+ }
96
+ if (companionStateFile) {
97
+ companionArgs.push("--state-file", companionStateFile);
98
+ }
99
+ if (bridgeToken) {
100
+ companionArgs.push("--bridgeToken", bridgeToken);
101
+ }
102
+ if (wakeMac) {
103
+ companionArgs.push("--wake-mac", wakeMac);
104
+ }
105
+
106
+ const companion = spawn(process.execPath, companionArgs, {
107
+ cwd: projectRoot,
108
+ env: {
109
+ ...process.env,
110
+ AGENT_COMPANION_QUIET: verbose ? "0" : "1"
111
+ },
112
+ stdio: ["ignore", "pipe", "pipe"]
113
+ });
114
+ companion.stdout.pipe(process.stdout);
115
+ companion.stderr.pipe(process.stderr);
116
+ children.push({ name: "companion", child: companion });
117
+
118
+ companion.on("close", (code) => {
119
+ if (shuttingDown) return;
120
+ if (Number.isInteger(code) && code === 0) return;
121
+ console.error(`[companion-service] companion exited with code ${code ?? "unknown"}`);
122
+ shutdown("companion-exit");
123
+ });
124
+
125
+ function attachLogs(name, child) {
126
+ if (!verbose) {
127
+ child.on("close", (code) => {
128
+ if (shuttingDown) return;
129
+ if (Number.isInteger(code) && code === 0) return;
130
+ console.error(`[companion-service] ${name} exited with code ${code ?? "unknown"}`);
131
+ });
132
+ return;
133
+ }
134
+
135
+ child.stdout.on("data", (chunk) => {
136
+ process.stdout.write(`[${name}] ${chunk}`);
137
+ });
138
+ child.stderr.on("data", (chunk) => {
139
+ process.stderr.write(`[${name}] ${chunk}`);
140
+ });
141
+ child.on("close", (code) => {
142
+ if (shuttingDown) return;
143
+ if (Number.isInteger(code) && code === 0) return;
144
+ console.error(`[companion-service] ${name} exited with code ${code ?? "unknown"}`);
145
+ });
146
+ }
147
+
148
+ function startKeepAwakeProcess({ verbose }) {
149
+ if (process.platform === "darwin") {
150
+ const child = spawn("caffeinate", ["-dimsu"], {
151
+ cwd: projectRoot,
152
+ env: process.env,
153
+ stdio: ["ignore", "pipe", "pipe"]
154
+ });
155
+ child.on("error", (error) => {
156
+ process.stderr.write(`[companion-service] keep-awake unavailable: ${String(error?.message || error)}\n`);
157
+ });
158
+ if (verbose) {
159
+ attachLogs("keep-awake", child);
160
+ } else {
161
+ child.stderr.on("data", () => {
162
+ // suppress noisy caffeinate stderr in quiet mode
163
+ });
164
+ }
165
+ return { name: "keep-awake", child };
166
+ }
167
+
168
+ if (process.platform === "linux") {
169
+ const child = spawn(
170
+ "systemd-inhibit",
171
+ ["--what=sleep", "--why=agent-companion", "--mode=block", "sleep", "infinity"],
172
+ {
173
+ cwd: projectRoot,
174
+ env: process.env,
175
+ stdio: ["ignore", "pipe", "pipe"]
176
+ }
177
+ );
178
+ child.on("error", (error) => {
179
+ process.stderr.write(`[companion-service] keep-awake unavailable: ${String(error?.message || error)}\n`);
180
+ });
181
+ if (verbose) {
182
+ attachLogs("keep-awake", child);
183
+ }
184
+ return { name: "keep-awake", child };
185
+ }
186
+
187
+ return null;
188
+ }
189
+
190
+ function shutdown(reason) {
191
+ if (shuttingDown) return;
192
+ shuttingDown = true;
193
+ process.stderr.write(`\n[companion-service] shutting down (${reason})...\n`);
194
+
195
+ for (const { child } of children) {
196
+ if (!child.killed) {
197
+ child.kill("SIGTERM");
198
+ }
199
+ }
200
+
201
+ setTimeout(() => {
202
+ for (const { child } of children) {
203
+ if (!child.killed) {
204
+ child.kill("SIGKILL");
205
+ }
206
+ }
207
+ process.exit(0);
208
+ }, 1500);
209
+ }
210
+
211
+ async function waitForHealth(url, timeoutMs, name) {
212
+ const start = Date.now();
213
+ while (Date.now() - start < timeoutMs) {
214
+ try {
215
+ const response = await fetch(url, { method: "GET" });
216
+ if (response.ok) return;
217
+ } catch {
218
+ // keep retrying
219
+ }
220
+ await sleep(250);
221
+ }
222
+ throw new Error(`${name} health check timed out at ${url}`);
223
+ }
224
+
225
+ function parseArgs(tokens) {
226
+ const out = {};
227
+ for (let index = 0; index < tokens.length; index += 1) {
228
+ const token = tokens[index];
229
+ if (!token.startsWith("--")) continue;
230
+ const key = token.slice(2);
231
+ const next = tokens[index + 1];
232
+ if (!next || next.startsWith("--")) {
233
+ out[key] = "true";
234
+ continue;
235
+ }
236
+ out[key] = next;
237
+ index += 1;
238
+ }
239
+ return out;
240
+ }
241
+
242
+ function toBool(value) {
243
+ const normalized = trim(value).toLowerCase();
244
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
245
+ }
246
+
247
+ function trim(value) {
248
+ return String(value || "").trim();
249
+ }
250
+
251
+ function clampInt(value, fallback, min, max) {
252
+ const parsed = Number.parseInt(String(value || ""), 10);
253
+ if (!Number.isFinite(parsed)) return fallback;
254
+ return Math.max(min, Math.min(max, parsed));
255
+ }
256
+
257
+ function sleep(ms) {
258
+ return new Promise((resolve) => setTimeout(resolve, ms));
259
+ }
260
+
261
+ function printUsageAndExit(code, error = "") {
262
+ if (error) {
263
+ console.error(`[companion-service] ${error}`);
264
+ }
265
+ console.log(`Usage:
266
+ node scripts/laptop-service.mjs [--relay <url>] [options]
267
+ node scripts/laptop-service.mjs --with-local-relay [options]
268
+
269
+ Options:
270
+ --relay <url> Public or private relay URL for companion
271
+ --with-local-relay Also run relay in this process group
272
+ --relay-port <port> Relay port when --with-local-relay (default: 9797)
273
+ --relay-public-url <url> Public URL relay announces (default local URL)
274
+ --bridge-port <port> Bridge port (default: 8787)
275
+ --bridge-token <token> Optional bridge token
276
+ --wake-mac <mac> Optional Wake-on-LAN MAC (AA:BB:CC:DD:EE:FF)
277
+ --allow-sleep Disable keep-awake mode
278
+ --name <label> Computer display name for pairing
279
+ --state-file <path> Companion state file path
280
+ `);
281
+ process.exit(code);
282
+ }
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ const argv = process.argv.slice(2);
7
+ const dryRun = argv.includes("--dry-run");
8
+ const daysValue = readOption(argv, "--days");
9
+ const daysBack = safeInt(daysValue, 30);
10
+ const maxAgeMs = daysBack > 0 ? daysBack * 24 * 60 * 60 * 1000 : 0;
11
+
12
+ const codexRoot = path.join(os.homedir(), ".codex");
13
+ const sessionsRoot = path.join(codexRoot, "sessions");
14
+ const globalStateFile = path.join(codexRoot, ".codex-global-state.json");
15
+
16
+ let scanned = 0;
17
+ let patched = 0;
18
+ let titlesFromRollouts = 0;
19
+
20
+ const discoveredTitles = new Map();
21
+
22
+ for (const rolloutPath of listRolloutFiles(sessionsRoot)) {
23
+ let stat;
24
+ try {
25
+ stat = fs.statSync(rolloutPath);
26
+ } catch {
27
+ continue;
28
+ }
29
+
30
+ if (maxAgeMs > 0 && Date.now() - stat.mtimeMs > maxAgeMs) {
31
+ continue;
32
+ }
33
+
34
+ scanned += 1;
35
+ const result = repairRollout(rolloutPath, { dryRun });
36
+ if (result.patched) patched += 1;
37
+
38
+ if (result.threadId) {
39
+ const existing = discoveredTitles.get(result.threadId) || "";
40
+ if (!existing && result.title) {
41
+ discoveredTitles.set(result.threadId, result.title);
42
+ titlesFromRollouts += 1;
43
+ } else if (!existing) {
44
+ discoveredTitles.set(result.threadId, `Phone task ${result.threadId.slice(0, 8)}`);
45
+ }
46
+ }
47
+ }
48
+
49
+ const indexed = upsertThreadTitles(globalStateFile, discoveredTitles, { dryRun });
50
+
51
+ console.log(
52
+ `[resume-repair] scanned=${scanned} patched=${patched} indexed=${indexed} titles_detected=${titlesFromRollouts} dry_run=${dryRun}`
53
+ );
54
+
55
+ function repairRollout(filePath, options) {
56
+ const out = { patched: false, threadId: "", title: "" };
57
+
58
+ let raw = "";
59
+ try {
60
+ raw = fs.readFileSync(filePath, "utf8");
61
+ } catch {
62
+ return out;
63
+ }
64
+ if (!raw.trim()) return out;
65
+
66
+ const newlineIndex = raw.indexOf("\n");
67
+ const firstLine = newlineIndex >= 0 ? raw.slice(0, newlineIndex) : raw;
68
+ const rest = newlineIndex >= 0 ? raw.slice(newlineIndex) : "";
69
+ if (!firstLine.trim()) return out;
70
+
71
+ let meta = null;
72
+ try {
73
+ meta = JSON.parse(firstLine);
74
+ } catch {
75
+ return out;
76
+ }
77
+ if (!meta || typeof meta !== "object") return out;
78
+
79
+ const topLevelThreadId = typeof meta.thread_id === "string" ? meta.thread_id : "";
80
+ const payloadThreadId =
81
+ meta.payload && typeof meta.payload === "object" && typeof meta.payload.id === "string"
82
+ ? meta.payload.id
83
+ : "";
84
+ const threadId = topLevelThreadId || payloadThreadId;
85
+ if (threadId) out.threadId = threadId;
86
+
87
+ const candidates = [meta];
88
+ if (meta.payload && typeof meta.payload === "object") {
89
+ candidates.push(meta.payload);
90
+ }
91
+
92
+ let changed = false;
93
+ for (const candidate of candidates) {
94
+ if (!candidate || typeof candidate !== "object") continue;
95
+ const source = typeof candidate.source === "string" ? candidate.source.trim().toLowerCase() : "";
96
+
97
+ if (
98
+ (source === "exec" || source === "cli") &&
99
+ (candidate.originator === "codex_exec" || candidate.originator === "Codex Desktop")
100
+ ) {
101
+ candidate.originator = "codex_cli_rs";
102
+ changed = true;
103
+ }
104
+
105
+ if (source === "exec") {
106
+ candidate.source = "cli";
107
+ changed = true;
108
+ }
109
+ }
110
+
111
+ out.title = extractPromptTitle(raw);
112
+
113
+ if (!changed) return out;
114
+ out.patched = true;
115
+
116
+ if (!options.dryRun) {
117
+ const nextRaw = `${JSON.stringify(meta)}${rest}`;
118
+ atomicWriteText(filePath, nextRaw);
119
+ }
120
+
121
+ return out;
122
+ }
123
+
124
+ function extractPromptTitle(raw) {
125
+ const lines = raw.split(/\r?\n/);
126
+ const maxLines = Math.min(lines.length, 180);
127
+
128
+ for (let index = 1; index < maxLines; index += 1) {
129
+ const line = lines[index].trim();
130
+ if (!line || line[0] !== "{") continue;
131
+
132
+ let parsed = null;
133
+ try {
134
+ parsed = JSON.parse(line);
135
+ } catch {
136
+ continue;
137
+ }
138
+
139
+ const eventMessage =
140
+ parsed?.type === "event_msg" && parsed?.payload?.type === "user_message"
141
+ ? parsed?.payload?.message
142
+ : "";
143
+ const responseMessage = extractTextFromResponseItem(parsed);
144
+ const candidate = cleanTitle(eventMessage || responseMessage);
145
+
146
+ if (!candidate) continue;
147
+ if (!isLikelyPrompt(candidate)) continue;
148
+ return candidate.slice(0, 120);
149
+ }
150
+
151
+ return "";
152
+ }
153
+
154
+ function extractTextFromResponseItem(parsed) {
155
+ if (parsed?.type !== "response_item") return "";
156
+ const payload = parsed?.payload;
157
+ if (!payload || payload.type !== "message" || payload.role !== "user") return "";
158
+
159
+ for (const entry of Array.isArray(payload.content) ? payload.content : []) {
160
+ if (!entry || typeof entry !== "object") continue;
161
+ if (typeof entry.input_text === "string" && entry.input_text.trim()) return entry.input_text.trim();
162
+ if (typeof entry.text === "string" && entry.text.trim()) return entry.text.trim();
163
+ }
164
+ return "";
165
+ }
166
+
167
+ function cleanTitle(value) {
168
+ if (typeof value !== "string") return "";
169
+ return value.replace(/\s+/g, " ").trim();
170
+ }
171
+
172
+ function isLikelyPrompt(text) {
173
+ const lower = text.toLowerCase();
174
+ if (!lower) return false;
175
+ if (lower.startsWith("# agents.md")) return false;
176
+ if (lower.includes("<permissions instructions>")) return false;
177
+ if (lower.includes("<environment_context>")) return false;
178
+ if (lower.includes("collaboration mode")) return false;
179
+ if (lower.includes("mcp tool discovery")) return false;
180
+ return true;
181
+ }
182
+
183
+ function upsertThreadTitles(filePath, titlesMap, options) {
184
+ if (titlesMap.size === 0) return 0;
185
+
186
+ let parsed = {};
187
+ if (fs.existsSync(filePath)) {
188
+ try {
189
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
190
+ } catch {
191
+ parsed = {};
192
+ }
193
+ }
194
+
195
+ if (!parsed || typeof parsed !== "object") {
196
+ parsed = {};
197
+ }
198
+
199
+ const current = parsed["thread-titles"];
200
+ const titles =
201
+ current && typeof current === "object" && current.titles && typeof current.titles === "object"
202
+ ? { ...current.titles }
203
+ : {};
204
+ const order =
205
+ current && typeof current === "object" && Array.isArray(current.order)
206
+ ? current.order.filter((item) => typeof item === "string")
207
+ : [];
208
+
209
+ let updated = 0;
210
+ const prioritized = [];
211
+ for (const [threadId, title] of titlesMap.entries()) {
212
+ const safeTitle = cleanTitle(title) || `Phone task ${threadId.slice(0, 8)}`;
213
+ const previous = typeof titles[threadId] === "string" ? titles[threadId] : "";
214
+
215
+ if (!previous || previous.startsWith("Phone task")) {
216
+ if (previous !== safeTitle) {
217
+ titles[threadId] = safeTitle;
218
+ updated += 1;
219
+ }
220
+ }
221
+ prioritized.push(threadId);
222
+ }
223
+
224
+ const nextOrder = [...prioritized, ...order.filter((id) => !prioritized.includes(id))].slice(0, 1200);
225
+ parsed["thread-titles"] = {
226
+ ...(current && typeof current === "object" ? current : {}),
227
+ titles,
228
+ order: nextOrder
229
+ };
230
+
231
+ if (!options.dryRun) {
232
+ atomicWriteText(filePath, JSON.stringify(parsed));
233
+ }
234
+
235
+ return updated;
236
+ }
237
+
238
+ function listRolloutFiles(root) {
239
+ const files = [];
240
+ if (!fs.existsSync(root)) return files;
241
+
242
+ const stack = [root];
243
+ while (stack.length > 0) {
244
+ const dir = stack.pop();
245
+ let entries = [];
246
+ try {
247
+ entries = fs.readdirSync(dir, { withFileTypes: true });
248
+ } catch {
249
+ continue;
250
+ }
251
+
252
+ for (const entry of entries) {
253
+ const fullPath = path.join(dir, entry.name);
254
+ if (entry.isDirectory()) {
255
+ stack.push(fullPath);
256
+ continue;
257
+ }
258
+ if (!entry.isFile()) continue;
259
+ if (!entry.name.startsWith("rollout-")) continue;
260
+ if (!entry.name.endsWith(".jsonl")) continue;
261
+ files.push(fullPath);
262
+ }
263
+ }
264
+
265
+ return files;
266
+ }
267
+
268
+ function atomicWriteText(filePath, content) {
269
+ const dir = path.dirname(filePath);
270
+ const base = path.basename(filePath);
271
+ const tempPath = path.join(dir, `.${base}.tmp-${process.pid}-${Date.now()}`);
272
+ let mode = undefined;
273
+
274
+ try {
275
+ mode = fs.statSync(filePath).mode;
276
+ } catch {
277
+ mode = undefined;
278
+ }
279
+
280
+ if (typeof mode === "number") {
281
+ fs.writeFileSync(tempPath, content, { mode });
282
+ } else {
283
+ fs.writeFileSync(tempPath, content);
284
+ }
285
+
286
+ fs.renameSync(tempPath, filePath);
287
+ }
288
+
289
+ function safeInt(value, fallback) {
290
+ const parsed = Number.parseInt(String(value), 10);
291
+ return Number.isFinite(parsed) ? parsed : fallback;
292
+ }
293
+
294
+ function readOption(args, name) {
295
+ const index = args.indexOf(name);
296
+ if (index < 0) return "";
297
+ const value = args[index + 1];
298
+ if (!value || value.startsWith("--")) return "";
299
+ return value;
300
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process";
3
+
4
+ const bridge = process.env.AGENT_BRIDGE_URL || "http://localhost:8787";
5
+
6
+ const response = await fetch(`${bridge}/api/reset`, {
7
+ method: "POST",
8
+ headers: {
9
+ "Content-Type": "application/json"
10
+ },
11
+ body: JSON.stringify({})
12
+ });
13
+
14
+ if (!response.ok) {
15
+ console.error(await response.text());
16
+ process.exit(1);
17
+ }
18
+
19
+ console.log("Bridge state reset to defaults.");
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process";
3
+
4
+ const argv = process.argv.slice(2);
5
+ const dividerIndex = argv.indexOf("--");
6
+ const optionArgs = dividerIndex >= 0 ? argv.slice(0, dividerIndex) : argv;
7
+ const commandArgs = (dividerIndex >= 0 ? argv.slice(dividerIndex + 1) : []).filter(Boolean);
8
+ while (commandArgs[0] === "--") {
9
+ commandArgs.shift();
10
+ }
11
+
12
+ const options = parseOptions(optionArgs);
13
+ const bridgeUrl = options.bridge || process.env.AGENT_BRIDGE_URL || "http://localhost:8787";
14
+ const workspacePath = options.workspace || process.cwd();
15
+ const agentType = options.agent === "CLAUDE" ? "CLAUDE" : "CODEX";
16
+ const prompt = options.prompt || "";
17
+ const title = options.title || "";
18
+ const token = options.token || process.env.AGENT_BRIDGE_TOKEN || "";
19
+ const fullWorkspaceAccess = parseBooleanOption(options["full-workspace-access"]);
20
+ const skipPermissions = parseBooleanOption(options["skip-permissions"]);
21
+ const planMode = parseBooleanOption(options["plan-mode"]);
22
+
23
+ if (commandArgs.length === 0 && !prompt.trim()) {
24
+ printUsageAndExit(1);
25
+ }
26
+
27
+ const payload = {
28
+ agentType,
29
+ workspacePath,
30
+ title,
31
+ prompt
32
+ };
33
+
34
+ if (commandArgs.length > 0) {
35
+ payload.command = commandArgs;
36
+ }
37
+ if (fullWorkspaceAccess) {
38
+ payload.fullWorkspaceAccess = true;
39
+ }
40
+ if (skipPermissions) {
41
+ payload.skipPermissions = true;
42
+ }
43
+ if (planMode) {
44
+ payload.planMode = true;
45
+ }
46
+
47
+ const headers = {
48
+ "Content-Type": "application/json"
49
+ };
50
+ if (token) {
51
+ headers["x-bridge-token"] = token;
52
+ }
53
+
54
+ const response = await fetch(`${bridgeUrl}/api/launcher/start`, {
55
+ method: "POST",
56
+ headers,
57
+ body: JSON.stringify(payload)
58
+ });
59
+
60
+ const text = await response.text();
61
+ let json;
62
+ try {
63
+ json = text ? JSON.parse(text) : {};
64
+ } catch {
65
+ json = { raw: text };
66
+ }
67
+
68
+ if (!response.ok) {
69
+ console.error(`[start-task] error ${response.status}:`, json);
70
+ process.exit(1);
71
+ }
72
+
73
+ console.log("[start-task] launched:");
74
+ console.log(JSON.stringify(json, null, 2));
75
+
76
+ function parseOptions(args) {
77
+ const out = {};
78
+
79
+ for (let index = 0; index < args.length; index += 1) {
80
+ const arg = args[index];
81
+ if (!arg.startsWith("--")) continue;
82
+
83
+ const key = arg.slice(2);
84
+ const next = args[index + 1];
85
+
86
+ if (!next || next.startsWith("--")) {
87
+ out[key] = "true";
88
+ continue;
89
+ }
90
+
91
+ out[key] = next;
92
+ index += 1;
93
+ }
94
+
95
+ return out;
96
+ }
97
+
98
+ function printUsageAndExit(code) {
99
+ console.log(`Usage:\n node scripts/start-task.mjs --agent CODEX|CLAUDE --workspace <path> --prompt "Task" [--title "Name"] [--bridge <url>] [--token <token>] [--full-workspace-access] [--skip-permissions] [--plan-mode]\n\n # custom command\n node scripts/start-task.mjs --agent CODEX --workspace <path> --plan-mode -- -- zsh -lc "echo hello"\n`);
100
+ process.exit(code);
101
+ }
102
+
103
+ function parseBooleanOption(value) {
104
+ if (value == null) return false;
105
+ const normalized = String(value).trim().toLowerCase();
106
+ if (!normalized) return false;
107
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
108
+ }