@aku11i/phantom 6.2.0 → 6.3.0-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,3475 @@
1
+ import { a as createRouter, c as createFileRoute, l as createRootRoute, n as Scripts, o as Outlet, r as HeadContent, s as lazyRouteComponent, u as require_jsx_runtime } from "../_libs/@tanstack/react-router+[...].mjs";
2
+ import { a as literal, c as string, i as boolean, n as _enum, o as object, r as array, s as preprocess, t as number } from "../_libs/zod.mjs";
3
+ import path, { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import fs, { copyFile, mkdir, readFile, readdir, realpath, rename, stat, writeFile } from "node:fs/promises";
5
+ import { execFile, spawn } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import readline from "node:readline";
8
+ import { homedir } from "node:os";
9
+ //#region node_modules/.nitro/vite/services/ssr/assets/router-C0zvMxvt.js
10
+ var import_jsx_runtime = require_jsx_runtime();
11
+ var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
12
+ var Route$14 = createRootRoute({
13
+ head: () => ({ meta: [
14
+ { charSet: "utf-8" },
15
+ {
16
+ name: "viewport",
17
+ content: "width=device-width, initial-scale=1"
18
+ },
19
+ { title: "Phantom Serve" }
20
+ ] }),
21
+ component: RootComponent
22
+ });
23
+ function RootComponent() {
24
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RootDocument, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Outlet, {}) });
25
+ }
26
+ function RootDocument({ children }) {
27
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("html", {
28
+ lang: "en",
29
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("head", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HeadContent, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("body", { children: [children, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Scripts, {})] })]
30
+ });
31
+ }
32
+ var $$splitComponentImporter = () => import("./routes-DpnTz1lw.mjs");
33
+ var Route$13 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter, "component") });
34
+ function json(data, init) {
35
+ return Response.json(data, init);
36
+ }
37
+ function jsonError(message, status = 400) {
38
+ return Response.json({ error: { message } }, { status });
39
+ }
40
+ async function readJsonObject(request) {
41
+ const body = await request.json().catch(() => void 0);
42
+ if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("Expected a JSON object request body");
43
+ return body;
44
+ }
45
+ function getString$1(body, key) {
46
+ const value = body[key];
47
+ return typeof value === "string" ? value : void 0;
48
+ }
49
+ function handleApiError(error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ return jsonError(message, message.includes("not found") || message.includes("Not found") ? 404 : 400);
52
+ }
53
+ var execFile$1 = promisify(execFile);
54
+ /**
55
+ * Execute a git command with consistent error handling
56
+ */
57
+ async function executeGitCommand(args, options = {}) {
58
+ const trimStdout = options.trimStdout ?? true;
59
+ const trimStderr = options.trimStderr ?? true;
60
+ try {
61
+ const result = await execFile$1("git", args, {
62
+ cwd: options.cwd,
63
+ env: options.env || process.env,
64
+ encoding: "utf8"
65
+ });
66
+ return {
67
+ stdout: trimStdout ? result.stdout.trim() : result.stdout,
68
+ stderr: trimStderr ? result.stderr.trim() : result.stderr
69
+ };
70
+ } catch (error) {
71
+ if (error && typeof error === "object" && "stdout" in error && "stderr" in error) {
72
+ const execError = error;
73
+ if (execError.stderr?.trim()) throw new Error(execError.stderr.trim());
74
+ return {
75
+ stdout: trimStdout ? execError.stdout?.trim() ?? "" : execError.stdout ?? "",
76
+ stderr: trimStderr ? execError.stderr?.trim() ?? "" : execError.stderr ?? ""
77
+ };
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+ async function addWorktree(options) {
83
+ const { path, branch, base = "HEAD", createBranch = true, cwd } = options;
84
+ const args = [
85
+ "worktree",
86
+ "add",
87
+ path
88
+ ];
89
+ if (createBranch) args.push("-b", branch, base);
90
+ else args.push(branch);
91
+ await executeGitCommand(args, { cwd });
92
+ }
93
+ /**
94
+ * Creates a successful Result containing the given value.
95
+ *
96
+ * @template T - The type of the success value
97
+ * @param value - The success value to wrap
98
+ * @returns A Result in the Ok state containing the value
99
+ *
100
+ * @example
101
+ * const result = ok(42);
102
+ * // result: Result<number, never> = { ok: true, value: 42 }
103
+ */
104
+ var ok = (value) => ({
105
+ ok: true,
106
+ value
107
+ });
108
+ /**
109
+ * Creates a failed Result containing the given error.
110
+ *
111
+ * @template E - The type of the error value
112
+ * @param error - The error value to wrap
113
+ * @returns A Result in the Err state containing the error
114
+ *
115
+ * @example
116
+ * const result = err(new Error("Something went wrong"));
117
+ * // result: Result<never, Error> = { ok: false, error: Error(...) }
118
+ */
119
+ var err = (error) => ({
120
+ ok: false,
121
+ error
122
+ });
123
+ /**
124
+ * Type guard that checks if a Result is in the Ok state.
125
+ *
126
+ * @template T - The type of the success value
127
+ * @template E - The type of the error value
128
+ * @param result - The Result to check
129
+ * @returns True if the Result is Ok, false otherwise
130
+ *
131
+ * @example
132
+ * if (isOk(result)) {
133
+ * console.log(result.value); // TypeScript knows result.value exists
134
+ * }
135
+ */
136
+ var isOk = (result) => result.ok;
137
+ /**
138
+ * Type guard that checks if a Result is in the Err state.
139
+ *
140
+ * @template T - The type of the success value
141
+ * @template E - The type of the error value
142
+ * @param result - The Result to check
143
+ * @returns True if the Result is Err, false otherwise
144
+ *
145
+ * @example
146
+ * if (isErr(result)) {
147
+ * console.error(result.error); // TypeScript knows result.error exists
148
+ * }
149
+ */
150
+ var isErr = (result) => !result.ok;
151
+ async function branchExists(gitRoot, branchName) {
152
+ try {
153
+ return ok((await executeGitCommand([
154
+ "branch",
155
+ "--list",
156
+ branchName
157
+ ], { cwd: gitRoot })).stdout.trim() !== "");
158
+ } catch (error) {
159
+ return err(/* @__PURE__ */ new Error(`Failed to check branch existence: ${error instanceof Error ? error.message : String(error)}`));
160
+ }
161
+ }
162
+ async function configGetRegexp(options) {
163
+ const { pattern, global = false, nullSeparated = false, cwd } = options;
164
+ const args = ["config"];
165
+ if (global) args.push("--global");
166
+ if (nullSeparated) args.push("--null");
167
+ args.push("--get-regexp", pattern);
168
+ const { stdout } = await executeGitCommand(args, { cwd });
169
+ return stdout;
170
+ }
171
+ async function deleteBranch$1(options) {
172
+ const { gitRoot, branch, force = true } = options;
173
+ await executeGitCommand([
174
+ "branch",
175
+ force ? "-D" : "-d",
176
+ branch
177
+ ], { cwd: gitRoot });
178
+ }
179
+ async function getGitRoot(options = {}) {
180
+ const { cwd = process.cwd() } = options;
181
+ const { stdout } = await executeGitCommand(["rev-parse", "--git-common-dir"], { cwd });
182
+ if (stdout.endsWith("/.git") || stdout === ".git") return resolve(cwd, dirname(stdout));
183
+ const { stdout: toplevel } = await executeGitCommand(["rev-parse", "--show-toplevel"], { cwd });
184
+ return toplevel;
185
+ }
186
+ function parseStatusLine(line) {
187
+ const indexStatus = line[0] ?? " ";
188
+ const workingTreeStatus = line[1] ?? " ";
189
+ const pathPart = line.slice(3);
190
+ const renamedPaths = indexStatus === "R" || indexStatus === "C" || workingTreeStatus === "R" || workingTreeStatus === "C" ? pathPart.match(/^(?<originalPath>.+) -> (?<path>.+)$/) : null;
191
+ return {
192
+ indexStatus,
193
+ workingTreeStatus,
194
+ path: renamedPaths?.groups?.path ?? pathPart,
195
+ originalPath: renamedPaths?.groups?.originalPath
196
+ };
197
+ }
198
+ async function getStatus(options) {
199
+ const { stdout } = await executeGitCommand(["status", "--porcelain"], {
200
+ cwd: options.cwd,
201
+ trimStdout: false
202
+ });
203
+ const entries = stdout.split("\n").filter((line) => line.length > 0).map(parseStatusLine);
204
+ return {
205
+ entries,
206
+ isClean: entries.length === 0
207
+ };
208
+ }
209
+ async function listWorktrees$1(gitRoot) {
210
+ const { stdout } = await executeGitCommand([
211
+ "worktree",
212
+ "list",
213
+ "--porcelain"
214
+ ], { cwd: gitRoot });
215
+ const worktrees = [];
216
+ let currentWorktree = {};
217
+ const lines = stdout.split("\n").filter((line) => line.length > 0);
218
+ for (const line of lines) if (line.startsWith("worktree ")) {
219
+ if (currentWorktree.path) worktrees.push(currentWorktree);
220
+ currentWorktree = {
221
+ path: line.substring(9),
222
+ isLocked: false,
223
+ isPrunable: false
224
+ };
225
+ } else if (line.startsWith("HEAD ")) currentWorktree.head = line.substring(5);
226
+ else if (line.startsWith("branch ")) {
227
+ const fullBranch = line.substring(7);
228
+ currentWorktree.branch = fullBranch.startsWith("refs/heads/") ? fullBranch.substring(11) : fullBranch;
229
+ } else if (line === "detached") currentWorktree.branch = "(detached HEAD)";
230
+ else if (line === "locked") currentWorktree.isLocked = true;
231
+ else if (line === "prunable") currentWorktree.isPrunable = true;
232
+ if (currentWorktree.path) worktrees.push(currentWorktree);
233
+ return worktrees;
234
+ }
235
+ async function removeWorktree$1(options) {
236
+ const { gitRoot, path, force = false } = options;
237
+ const args = ["worktree", "remove"];
238
+ if (force) args.push("--force");
239
+ args.push(path);
240
+ await executeGitCommand(args, { cwd: gitRoot });
241
+ }
242
+ var ConfigValidationError = class extends Error {
243
+ constructor(message) {
244
+ super(`Invalid phantom.config.json: ${message}`);
245
+ this.name = this.constructor.name;
246
+ }
247
+ };
248
+ var phantomConfigSchema = object({
249
+ postCreate: object({
250
+ copyFiles: array(string()).optional(),
251
+ commands: array(string()).optional()
252
+ }).passthrough().optional(),
253
+ preDelete: object({ commands: array(string()).optional() }).passthrough().optional(),
254
+ directoryNameSeparator: string().optional(),
255
+ worktreesDirectory: string().optional()
256
+ }).passthrough();
257
+ function validateConfig(config) {
258
+ const result = phantomConfigSchema.safeParse(config);
259
+ if (!result.success) {
260
+ const firstError = result.error.issues[0];
261
+ const path = firstError.path.join(".");
262
+ return err(new ConfigValidationError(path ? `${path}: ${firstError.message}` : firstError.message));
263
+ }
264
+ return ok(result.data);
265
+ }
266
+ var ConfigNotFoundError = class extends Error {
267
+ constructor() {
268
+ super("phantom.config.json not found");
269
+ this.name = this.constructor.name;
270
+ }
271
+ };
272
+ var ConfigParseError = class extends Error {
273
+ constructor(message) {
274
+ super(`Failed to parse phantom.config.json: ${message}`);
275
+ this.name = this.constructor.name;
276
+ }
277
+ };
278
+ async function loadConfig(gitRoot) {
279
+ const configPath = path.join(gitRoot, "phantom.config.json");
280
+ try {
281
+ const content = await fs.readFile(configPath, "utf-8");
282
+ try {
283
+ const validationResult = validateConfig(JSON.parse(content));
284
+ if (!validationResult.ok) return err(validationResult.error);
285
+ return ok(validationResult.value);
286
+ } catch (error) {
287
+ return err(new ConfigParseError(error instanceof Error ? error.message : String(error)));
288
+ }
289
+ } catch (error) {
290
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return err(new ConfigNotFoundError());
291
+ throw error;
292
+ }
293
+ }
294
+ var PreferencesValidationError = class extends Error {
295
+ constructor(message) {
296
+ super(`Invalid phantom preferences: ${message}`);
297
+ this.name = this.constructor.name;
298
+ }
299
+ };
300
+ var preferencesSchema = object({
301
+ editor: string().optional(),
302
+ ai: string().optional(),
303
+ worktreesDirectory: string().optional(),
304
+ directoryNameSeparator: string().optional(),
305
+ keepBranch: boolean().optional()
306
+ }).passthrough();
307
+ function parsePreferences(output) {
308
+ if (!output) return {};
309
+ const records = output.split("\0").filter((record) => record.length > 0);
310
+ const preferences = {};
311
+ for (const record of records) {
312
+ const newlineIndex = record.indexOf("\n");
313
+ const separatorIndex = newlineIndex >= 0 ? newlineIndex : record.indexOf(" ");
314
+ if (separatorIndex < 0) continue;
315
+ const key = record.slice(0, separatorIndex);
316
+ const value = record.slice(separatorIndex + 1);
317
+ if (!key.startsWith("phantom.")) continue;
318
+ const strippedKey = key.slice(8).toLowerCase();
319
+ if (strippedKey === "editor") preferences.editor = value;
320
+ else if (strippedKey === "ai") preferences.ai = value;
321
+ else if (strippedKey === "worktreesdirectory") preferences.worktreesDirectory = value;
322
+ else if (strippedKey === "directorynameseparator") preferences.directoryNameSeparator = value;
323
+ else if (strippedKey === "keepbranch") preferences.keepBranch = value === "true" ? true : value === "false" ? false : void 0;
324
+ }
325
+ const parsed = preferencesSchema.safeParse(preferences);
326
+ if (!parsed.success) throw new PreferencesValidationError(parsed.error.issues[0]?.message ?? parsed.error.message);
327
+ return parsed.data;
328
+ }
329
+ async function loadPreferences() {
330
+ return parsePreferences(await configGetRegexp({
331
+ pattern: "^phantom\\.",
332
+ global: true,
333
+ nullSeparated: true
334
+ }));
335
+ }
336
+ function getWorktreesDirectory(gitRoot, worktreesDirectory) {
337
+ if (worktreesDirectory) return isAbsolute(worktreesDirectory) ? worktreesDirectory : join(gitRoot, worktreesDirectory);
338
+ return join(gitRoot, ".git", "phantom", "worktrees");
339
+ }
340
+ function getWorktreePathFromDirectory(worktreeDirectory, name, directoryNameSeparator) {
341
+ return join(worktreeDirectory, directoryNameSeparator === "/" ? name : name.replaceAll("/", directoryNameSeparator));
342
+ }
343
+ async function createContext(gitRoot) {
344
+ const configResult = await loadConfig(gitRoot);
345
+ const config = isOk(configResult) ? configResult.value : null;
346
+ const preferences = await loadPreferences();
347
+ const worktreesDirectoryConfig = config?.worktreesDirectory;
348
+ const worktreesDirectory = preferences.worktreesDirectory ?? worktreesDirectoryConfig;
349
+ const directoryNameSeparator = preferences.directoryNameSeparator || config?.directoryNameSeparator || "/";
350
+ return {
351
+ gitRoot,
352
+ worktreesDirectory: getWorktreesDirectory(gitRoot, worktreesDirectory),
353
+ directoryNameSeparator,
354
+ config,
355
+ preferences
356
+ };
357
+ }
358
+ function getPhantomEnv(worktreeName, worktreePath) {
359
+ return {
360
+ PHANTOM: "1",
361
+ PHANTOM_NAME: worktreeName,
362
+ PHANTOM_PATH: worktreePath
363
+ };
364
+ }
365
+ var ProcessError = class extends Error {
366
+ exitCode;
367
+ constructor(message, exitCode) {
368
+ super(message);
369
+ this.name = this.constructor.name;
370
+ this.exitCode = exitCode;
371
+ }
372
+ };
373
+ var ProcessExecutionError = class extends ProcessError {
374
+ constructor(command, exitCode) {
375
+ super(`Command '${command}' failed with exit code ${exitCode}`, exitCode);
376
+ this.name = "ProcessExecutionError";
377
+ }
378
+ };
379
+ var ProcessSignalError = class extends ProcessError {
380
+ constructor(signal) {
381
+ const exitCode = 128 + (signal === "SIGTERM" ? 15 : 1);
382
+ super(`Command terminated by signal: ${signal}`, exitCode);
383
+ this.name = "ProcessSignalError";
384
+ }
385
+ };
386
+ var ProcessSpawnError = class extends ProcessError {
387
+ constructor(command, details) {
388
+ super(`Error executing command '${command}': ${details}`);
389
+ this.name = "ProcessSpawnError";
390
+ }
391
+ };
392
+ async function spawnProcess(config) {
393
+ return new Promise((resolve) => {
394
+ const { command, args = [], options = {} } = config;
395
+ const childProcess = spawn(command, args, {
396
+ stdio: "inherit",
397
+ ...options
398
+ });
399
+ childProcess.on("error", (error) => {
400
+ resolve(err(new ProcessSpawnError(command, error.message)));
401
+ });
402
+ childProcess.on("exit", (code, signal) => {
403
+ if (signal) resolve(err(new ProcessSignalError(signal)));
404
+ else {
405
+ const exitCode = code ?? 0;
406
+ if (exitCode === 0) resolve(ok({ exitCode }));
407
+ else resolve(err(new ProcessExecutionError(command, exitCode)));
408
+ }
409
+ });
410
+ });
411
+ }
412
+ var WorktreeError = class extends Error {
413
+ constructor(message) {
414
+ super(message);
415
+ this.name = this.constructor.name;
416
+ }
417
+ };
418
+ var WorktreeNotFoundError = class extends WorktreeError {
419
+ constructor(name) {
420
+ super(`Worktree '${name}' not found`);
421
+ this.name = "WorktreeNotFoundError";
422
+ }
423
+ };
424
+ var WorktreeAlreadyExistsError = class extends WorktreeError {
425
+ constructor(name) {
426
+ super(`Worktree '${name}' already exists`);
427
+ this.name = "WorktreeAlreadyExistsError";
428
+ }
429
+ };
430
+ var WorktreeActionConflictError = class extends WorktreeError {
431
+ constructor() {
432
+ super("Cannot use --shell, --exec, and --tmux options together");
433
+ this.name = "WorktreeActionConflictError";
434
+ }
435
+ };
436
+ var TmuxSessionRequiredError = class extends WorktreeError {
437
+ constructor() {
438
+ super("The --tmux option can only be used inside a tmux session");
439
+ this.name = "TmuxSessionRequiredError";
440
+ }
441
+ };
442
+ async function getWorktreeStatus(worktreePath) {
443
+ try {
444
+ return (await getStatus({ cwd: worktreePath })).isClean;
445
+ } catch {
446
+ return true;
447
+ }
448
+ }
449
+ async function listWorktrees(gitRoot, options = {}) {
450
+ try {
451
+ const gitWorktrees = await listWorktrees$1(gitRoot);
452
+ const excludeDefault = options.excludeDefault ?? false;
453
+ const filteredWorktrees = excludeDefault ? gitWorktrees.filter((worktree) => worktree.path !== gitRoot) : gitWorktrees;
454
+ if (filteredWorktrees.length === 0) return ok({
455
+ worktrees: [],
456
+ message: excludeDefault && gitWorktrees.length > 0 ? "No sub worktrees found" : "No worktrees found"
457
+ });
458
+ return ok({ worktrees: await Promise.all(filteredWorktrees.map(async (gitWorktree) => {
459
+ const shortHead = gitWorktree.head?.slice(0, 7) ?? "HEAD";
460
+ const branchName = gitWorktree.branch && gitWorktree.branch !== "(detached HEAD)" ? gitWorktree.branch : shortHead;
461
+ const isClean = await getWorktreeStatus(gitWorktree.path);
462
+ const pathToDisplay = relative(process.cwd(), gitWorktree.path) || ".";
463
+ return {
464
+ name: branchName,
465
+ path: gitWorktree.path,
466
+ pathToDisplay,
467
+ branch: branchName,
468
+ isClean
469
+ };
470
+ })) });
471
+ } catch (error) {
472
+ const errorMessage = error instanceof Error ? error.message : String(error);
473
+ throw new Error(`Failed to list worktrees: ${errorMessage}`);
474
+ }
475
+ }
476
+ async function validateWorktreeExists(gitRoot, _worktreeDirectory, name, options = {}) {
477
+ const worktreesResult = await listWorktrees(gitRoot, options);
478
+ if (isErr(worktreesResult)) return err(new WorktreeNotFoundError(name));
479
+ const worktree = worktreesResult.value.worktrees.find((wt) => wt.name === name);
480
+ if (!worktree) return err(new WorktreeNotFoundError(name));
481
+ return ok({ path: worktree.path });
482
+ }
483
+ async function validateWorktreeDoesNotExist(gitRoot, _worktreeDirectory, name) {
484
+ const worktreesResult = await listWorktrees(gitRoot);
485
+ if (isErr(worktreesResult)) return err(new WorktreeAlreadyExistsError(name));
486
+ if (worktreesResult.value.worktrees.find((wt) => wt.name === name)) return err(new WorktreeAlreadyExistsError(name));
487
+ return ok(void 0);
488
+ }
489
+ async function validateWorktreeDirectoryExists(worktreeDirectory) {
490
+ try {
491
+ await fs.access(worktreeDirectory);
492
+ return true;
493
+ } catch {
494
+ return false;
495
+ }
496
+ }
497
+ function validateWorktreeName(name) {
498
+ if (!name || name.trim() === "") return err(/* @__PURE__ */ new Error("Phantom name cannot be empty"));
499
+ if (!/^[a-zA-Z0-9\-_./]+$/.test(name)) return err(/* @__PURE__ */ new Error("Phantom name can only contain letters, numbers, hyphens, underscores, dots, and slashes"));
500
+ if (name.includes("..")) return err(/* @__PURE__ */ new Error("Phantom name cannot contain consecutive dots"));
501
+ return ok(void 0);
502
+ }
503
+ async function execInWorktree(gitRoot, worktreeDirectory, worktreeName, command, options) {
504
+ const validation = await validateWorktreeExists(gitRoot, worktreeDirectory, worktreeName);
505
+ if (isErr(validation)) return err(validation.error);
506
+ const worktreePath = validation.value.path;
507
+ const [cmd, ...args] = command;
508
+ return spawnProcess({
509
+ command: cmd,
510
+ args,
511
+ options: {
512
+ cwd: worktreePath,
513
+ stdio: options?.interactive ? "inherit" : [
514
+ "ignore",
515
+ "inherit",
516
+ "inherit"
517
+ ]
518
+ }
519
+ });
520
+ }
521
+ promisify(execFile);
522
+ number().int().positive();
523
+ number().int().positive();
524
+ promisify(execFile);
525
+ object({
526
+ owner: string(),
527
+ repo: string()
528
+ });
529
+ async function executePostCreateCommands(options) {
530
+ const { gitRoot, worktreesDirectory, worktreeName, commands, logger } = options;
531
+ const executedCommands = [];
532
+ for (const command of commands) {
533
+ logger?.log(`Executing: ${command}`);
534
+ const cmdResult = await execInWorktree(gitRoot, worktreesDirectory, worktreeName, [
535
+ process.env.SHELL || "/bin/sh",
536
+ "-c",
537
+ command
538
+ ]);
539
+ if (isErr(cmdResult)) {
540
+ const errorMessage = cmdResult.error instanceof Error ? cmdResult.error.message : String(cmdResult.error);
541
+ return err(/* @__PURE__ */ new Error(`Failed to execute post-create command "${command}": ${errorMessage}`));
542
+ }
543
+ if (cmdResult.value.exitCode !== 0) return err(/* @__PURE__ */ new Error(`Post-create command failed with exit code ${cmdResult.value.exitCode}: ${command}`));
544
+ executedCommands.push(command);
545
+ }
546
+ return ok({ executedCommands });
547
+ }
548
+ async function isInsideTmux() {
549
+ return process.env.TMUX !== void 0;
550
+ }
551
+ async function executeTmuxCommand(options) {
552
+ const { direction, command, args, cwd, env, windowName } = options;
553
+ const tmuxArgs = [];
554
+ switch (direction) {
555
+ case "new":
556
+ tmuxArgs.push("new-window");
557
+ if (windowName) tmuxArgs.push("-n", windowName);
558
+ break;
559
+ case "vertical":
560
+ tmuxArgs.push("split-window", "-v");
561
+ break;
562
+ case "horizontal":
563
+ tmuxArgs.push("split-window", "-h");
564
+ break;
565
+ }
566
+ if (cwd) tmuxArgs.push("-c", cwd);
567
+ if (env) for (const [key, value] of Object.entries(env)) tmuxArgs.push("-e", `${key}=${value}`);
568
+ tmuxArgs.push(command);
569
+ if (args && args.length > 0) tmuxArgs.push(...args);
570
+ return spawnProcess({
571
+ command: "tmux",
572
+ args: tmuxArgs
573
+ });
574
+ }
575
+ async function shellInWorktree(gitRoot, worktreeDirectory, worktreeName) {
576
+ const validation = await validateWorktreeExists(gitRoot, worktreeDirectory, worktreeName);
577
+ if (isErr(validation)) return err(validation.error);
578
+ const worktreePath = validation.value.path;
579
+ return spawnProcess({
580
+ command: process.env.SHELL || "/bin/sh",
581
+ args: [],
582
+ options: {
583
+ cwd: worktreePath,
584
+ env: {
585
+ ...process.env,
586
+ ...getPhantomEnv(worktreeName, worktreePath)
587
+ }
588
+ }
589
+ });
590
+ }
591
+ function mergeWorktreeCopyFiles(configuredFiles, requestedFiles) {
592
+ const files = [...new Set([...configuredFiles ?? [], ...requestedFiles ?? []])];
593
+ return files.length > 0 ? files : void 0;
594
+ }
595
+ function resolveWorktreeAction(options) {
596
+ const actions = [];
597
+ if (options?.shell) actions.push({ kind: "shell" });
598
+ if (options?.exec !== void 0) actions.push({
599
+ kind: "exec",
600
+ command: options.exec
601
+ });
602
+ if (options?.tmuxDirection) actions.push({
603
+ kind: "tmux",
604
+ direction: options.tmuxDirection
605
+ });
606
+ if (actions.length > 1) return err(new WorktreeActionConflictError());
607
+ return ok(actions[0]);
608
+ }
609
+ async function validateWorktreeAction(action) {
610
+ if (action?.kind === "tmux" && !await isInsideTmux()) return err(new TmuxSessionRequiredError());
611
+ return ok(void 0);
612
+ }
613
+ async function runWorktreeAction(options) {
614
+ const { gitRoot, worktreeDirectory, worktreeName, worktreePath, action, logger, exitWithProcessCode = false } = options;
615
+ if (!action) return ok({});
616
+ if (action.kind === "shell") {
617
+ logger?.log(`\nEntering worktree '${worktreeName}' at ${worktreePath}`);
618
+ logger?.log("Type 'exit' to return to your original directory\n");
619
+ const shellResult = await shellInWorktree(gitRoot, worktreeDirectory, worktreeName);
620
+ if (isErr(shellResult)) return err(shellResult.error);
621
+ return ok({ exitProcessCode: exitWithProcessCode ? shellResult.value.exitCode ?? 0 : void 0 });
622
+ }
623
+ if (action.kind === "exec") {
624
+ logger?.log(`\nExecuting command in worktree '${worktreeName}': ${action.command}`);
625
+ const execResult = await execInWorktree(gitRoot, worktreeDirectory, worktreeName, [
626
+ process.env.SHELL || "/bin/sh",
627
+ "-c",
628
+ action.command
629
+ ], { interactive: true });
630
+ if (isErr(execResult)) return err(execResult.error);
631
+ return ok({ exitProcessCode: exitWithProcessCode ? execResult.value.exitCode ?? 0 : void 0 });
632
+ }
633
+ logger?.log(`\nOpening worktree '${worktreeName}' in tmux ${action.direction === "new" ? "window" : "pane"}...`);
634
+ const shell = process.env.SHELL || "/bin/sh";
635
+ const tmuxResult = await executeTmuxCommand({
636
+ direction: action.direction,
637
+ command: shell,
638
+ cwd: worktreePath,
639
+ env: getPhantomEnv(worktreeName, worktreePath),
640
+ windowName: action.direction === "new" ? worktreeName : void 0
641
+ });
642
+ if (isErr(tmuxResult)) return err(tmuxResult.error);
643
+ return ok({});
644
+ }
645
+ var FileCopyError = class extends Error {
646
+ file;
647
+ constructor(file, message) {
648
+ super(`Failed to copy ${file}: ${message}`);
649
+ this.name = "FileCopyError";
650
+ this.file = file;
651
+ }
652
+ };
653
+ async function copyFiles(sourceDir, targetDir, files) {
654
+ const copiedFiles = [];
655
+ const skippedFiles = [];
656
+ for (const file of files) {
657
+ const sourcePath = path.join(sourceDir, file);
658
+ const targetPath = path.join(targetDir, file);
659
+ try {
660
+ if (!(await stat(sourcePath)).isFile()) {
661
+ skippedFiles.push(file);
662
+ continue;
663
+ }
664
+ await mkdir(path.dirname(targetPath), { recursive: true });
665
+ await copyFile(sourcePath, targetPath);
666
+ copiedFiles.push(file);
667
+ } catch (error) {
668
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") skippedFiles.push(file);
669
+ else return err(new FileCopyError(file, error instanceof Error ? error.message : String(error)));
670
+ }
671
+ }
672
+ return ok({
673
+ copiedFiles,
674
+ skippedFiles
675
+ });
676
+ }
677
+ var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => {
678
+ var __spreadArray = exports && exports.__spreadArray || function(to, from, pack) {
679
+ if (pack || arguments.length === 2) {
680
+ for (var i = 0, l = from.length, ar; i < l; i++) if (ar || !(i in from)) {
681
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
682
+ ar[i] = from[i];
683
+ }
684
+ }
685
+ return to.concat(ar || Array.prototype.slice.call(from));
686
+ };
687
+ Object.defineProperty(exports, "__esModule", { value: true });
688
+ exports.minLength = exports.maxLength = exports.poolSize = exports.humanId = exports.adverbs = exports.verbs = exports.nouns = exports.adjectives = void 0;
689
+ exports.adjectives = [
690
+ "afraid",
691
+ "all",
692
+ "beige",
693
+ "better",
694
+ "big",
695
+ "blue",
696
+ "bold",
697
+ "brave",
698
+ "breezy",
699
+ "bright",
700
+ "brown",
701
+ "bumpy",
702
+ "busy",
703
+ "calm",
704
+ "chatty",
705
+ "chilly",
706
+ "chubby",
707
+ "clean",
708
+ "clear",
709
+ "clever",
710
+ "cold",
711
+ "common",
712
+ "cool",
713
+ "cozy",
714
+ "crisp",
715
+ "cuddly",
716
+ "curly",
717
+ "curvy",
718
+ "cute",
719
+ "cyan",
720
+ "dark",
721
+ "deep",
722
+ "dirty",
723
+ "dry",
724
+ "dull",
725
+ "eager",
726
+ "early",
727
+ "easy",
728
+ "eight",
729
+ "eighty",
730
+ "eleven",
731
+ "empty",
732
+ "every",
733
+ "fair",
734
+ "famous",
735
+ "fancy",
736
+ "fast",
737
+ "few",
738
+ "fiery",
739
+ "fifty",
740
+ "fine",
741
+ "five",
742
+ "flat",
743
+ "floppy",
744
+ "fluffy",
745
+ "forty",
746
+ "four",
747
+ "frank",
748
+ "free",
749
+ "fresh",
750
+ "fruity",
751
+ "full",
752
+ "funky",
753
+ "funny",
754
+ "fuzzy",
755
+ "gentle",
756
+ "giant",
757
+ "gold",
758
+ "good",
759
+ "goofy",
760
+ "great",
761
+ "green",
762
+ "grumpy",
763
+ "happy",
764
+ "heavy",
765
+ "hip",
766
+ "honest",
767
+ "hot",
768
+ "huge",
769
+ "humble",
770
+ "hungry",
771
+ "icy",
772
+ "itchy",
773
+ "jolly",
774
+ "khaki",
775
+ "kind",
776
+ "large",
777
+ "late",
778
+ "lazy",
779
+ "legal",
780
+ "lemon",
781
+ "light",
782
+ "little",
783
+ "long",
784
+ "loose",
785
+ "loud",
786
+ "lovely",
787
+ "lucky",
788
+ "major",
789
+ "many",
790
+ "metal",
791
+ "mighty",
792
+ "modern",
793
+ "moody",
794
+ "neat",
795
+ "new",
796
+ "nice",
797
+ "nine",
798
+ "ninety",
799
+ "odd",
800
+ "old",
801
+ "olive",
802
+ "open",
803
+ "orange",
804
+ "perky",
805
+ "petite",
806
+ "pink",
807
+ "plain",
808
+ "plenty",
809
+ "polite",
810
+ "pretty",
811
+ "proud",
812
+ "public",
813
+ "puny",
814
+ "purple",
815
+ "quick",
816
+ "quiet",
817
+ "rare",
818
+ "ready",
819
+ "real",
820
+ "red",
821
+ "rich",
822
+ "ripe",
823
+ "salty",
824
+ "seven",
825
+ "shaggy",
826
+ "shaky",
827
+ "sharp",
828
+ "shiny",
829
+ "short",
830
+ "shy",
831
+ "silent",
832
+ "silly",
833
+ "silver",
834
+ "six",
835
+ "sixty",
836
+ "slick",
837
+ "slimy",
838
+ "slow",
839
+ "small",
840
+ "smart",
841
+ "smooth",
842
+ "social",
843
+ "soft",
844
+ "solid",
845
+ "some",
846
+ "sour",
847
+ "sparkly",
848
+ "spicy",
849
+ "spotty",
850
+ "stale",
851
+ "strict",
852
+ "strong",
853
+ "sunny",
854
+ "sweet",
855
+ "swift",
856
+ "tall",
857
+ "tame",
858
+ "tangy",
859
+ "tasty",
860
+ "ten",
861
+ "tender",
862
+ "thick",
863
+ "thin",
864
+ "thirty",
865
+ "three",
866
+ "tidy",
867
+ "tiny",
868
+ "tired",
869
+ "tough",
870
+ "tricky",
871
+ "true",
872
+ "twelve",
873
+ "twenty",
874
+ "two",
875
+ "upset",
876
+ "vast",
877
+ "violet",
878
+ "wacky",
879
+ "warm",
880
+ "wet",
881
+ "whole",
882
+ "wicked",
883
+ "wide",
884
+ "wild",
885
+ "wise",
886
+ "witty",
887
+ "yellow",
888
+ "young",
889
+ "yummy"
890
+ ];
891
+ exports.nouns = [
892
+ "actors",
893
+ "ads",
894
+ "adults",
895
+ "aliens",
896
+ "animals",
897
+ "ants",
898
+ "apes",
899
+ "apples",
900
+ "areas",
901
+ "baboons",
902
+ "badgers",
903
+ "bags",
904
+ "balloons",
905
+ "bananas",
906
+ "banks",
907
+ "bars",
908
+ "baths",
909
+ "bats",
910
+ "beans",
911
+ "bears",
912
+ "beds",
913
+ "beers",
914
+ "bees",
915
+ "berries",
916
+ "bikes",
917
+ "birds",
918
+ "boats",
919
+ "bobcats",
920
+ "books",
921
+ "bottles",
922
+ "boxes",
923
+ "breads",
924
+ "brooms",
925
+ "buckets",
926
+ "bugs",
927
+ "buses",
928
+ "bushes",
929
+ "buttons",
930
+ "camels",
931
+ "cameras",
932
+ "candies",
933
+ "candles",
934
+ "canyons",
935
+ "carpets",
936
+ "carrots",
937
+ "cars",
938
+ "cases",
939
+ "cats",
940
+ "chairs",
941
+ "chefs",
942
+ "chicken",
943
+ "cities",
944
+ "clocks",
945
+ "cloths",
946
+ "clouds",
947
+ "clowns",
948
+ "clubs",
949
+ "coats",
950
+ "cobras",
951
+ "coins",
952
+ "colts",
953
+ "comics",
954
+ "cooks",
955
+ "corners",
956
+ "cougars",
957
+ "cows",
958
+ "crabs",
959
+ "crews",
960
+ "cups",
961
+ "cycles",
962
+ "dancers",
963
+ "days",
964
+ "deer",
965
+ "deserts",
966
+ "dingos",
967
+ "dodos",
968
+ "dogs",
969
+ "dolls",
970
+ "donkeys",
971
+ "donuts",
972
+ "doodles",
973
+ "doors",
974
+ "dots",
975
+ "dragons",
976
+ "drinks",
977
+ "dryers",
978
+ "ducks",
979
+ "eagles",
980
+ "ears",
981
+ "eels",
982
+ "eggs",
983
+ "emus",
984
+ "ends",
985
+ "experts",
986
+ "eyes",
987
+ "facts",
988
+ "falcons",
989
+ "fans",
990
+ "feet",
991
+ "files",
992
+ "flies",
993
+ "flowers",
994
+ "forks",
995
+ "foxes",
996
+ "friends",
997
+ "frogs",
998
+ "games",
999
+ "garlics",
1000
+ "geckos",
1001
+ "geese",
1002
+ "ghosts",
1003
+ "gifts",
1004
+ "glasses",
1005
+ "goats",
1006
+ "grapes",
1007
+ "groups",
1008
+ "guests",
1009
+ "hairs",
1010
+ "hands",
1011
+ "hats",
1012
+ "heads",
1013
+ "hoops",
1014
+ "hornets",
1015
+ "horses",
1016
+ "hotels",
1017
+ "hounds",
1018
+ "houses",
1019
+ "humans",
1020
+ "icons",
1021
+ "ideas",
1022
+ "impalas",
1023
+ "insects",
1024
+ "islands",
1025
+ "items",
1026
+ "jars",
1027
+ "jeans",
1028
+ "jobs",
1029
+ "jokes",
1030
+ "keys",
1031
+ "kids",
1032
+ "kings",
1033
+ "kiwis",
1034
+ "knives",
1035
+ "lamps",
1036
+ "lands",
1037
+ "laws",
1038
+ "lemons",
1039
+ "lies",
1040
+ "lights",
1041
+ "lilies",
1042
+ "lines",
1043
+ "lions",
1044
+ "lizards",
1045
+ "llamas",
1046
+ "loops",
1047
+ "mails",
1048
+ "mammals",
1049
+ "mangos",
1050
+ "maps",
1051
+ "masks",
1052
+ "meals",
1053
+ "melons",
1054
+ "memes",
1055
+ "meteors",
1056
+ "mice",
1057
+ "mirrors",
1058
+ "moles",
1059
+ "moments",
1060
+ "monkeys",
1061
+ "months",
1062
+ "moons",
1063
+ "moose",
1064
+ "mugs",
1065
+ "nails",
1066
+ "needles",
1067
+ "news",
1068
+ "nights",
1069
+ "numbers",
1070
+ "olives",
1071
+ "onions",
1072
+ "oranges",
1073
+ "otters",
1074
+ "owls",
1075
+ "pandas",
1076
+ "pans",
1077
+ "pants",
1078
+ "papayas",
1079
+ "papers",
1080
+ "parents",
1081
+ "parks",
1082
+ "parrots",
1083
+ "parts",
1084
+ "paths",
1085
+ "paws",
1086
+ "peaches",
1087
+ "pears",
1088
+ "peas",
1089
+ "pens",
1090
+ "pets",
1091
+ "phones",
1092
+ "pianos",
1093
+ "pigs",
1094
+ "pillows",
1095
+ "places",
1096
+ "planes",
1097
+ "planets",
1098
+ "plants",
1099
+ "plums",
1100
+ "poems",
1101
+ "poets",
1102
+ "points",
1103
+ "pots",
1104
+ "pugs",
1105
+ "pumas",
1106
+ "queens",
1107
+ "rabbits",
1108
+ "radios",
1109
+ "rats",
1110
+ "ravens",
1111
+ "readers",
1112
+ "regions",
1113
+ "results",
1114
+ "rice",
1115
+ "rings",
1116
+ "rivers",
1117
+ "rockets",
1118
+ "rocks",
1119
+ "rooms",
1120
+ "roses",
1121
+ "rules",
1122
+ "sails",
1123
+ "schools",
1124
+ "seals",
1125
+ "seas",
1126
+ "sheep",
1127
+ "shirts",
1128
+ "shoes",
1129
+ "showers",
1130
+ "shrimps",
1131
+ "sides",
1132
+ "signs",
1133
+ "singers",
1134
+ "sites",
1135
+ "sloths",
1136
+ "snails",
1137
+ "snakes",
1138
+ "socks",
1139
+ "spiders",
1140
+ "spies",
1141
+ "spoons",
1142
+ "squids",
1143
+ "stamps",
1144
+ "stars",
1145
+ "states",
1146
+ "steaks",
1147
+ "streets",
1148
+ "suits",
1149
+ "suns",
1150
+ "swans",
1151
+ "symbols",
1152
+ "tables",
1153
+ "taxes",
1154
+ "taxis",
1155
+ "teams",
1156
+ "teeth",
1157
+ "terms",
1158
+ "things",
1159
+ "ties",
1160
+ "tigers",
1161
+ "times",
1162
+ "tips",
1163
+ "tires",
1164
+ "toes",
1165
+ "tools",
1166
+ "towns",
1167
+ "toys",
1168
+ "trains",
1169
+ "trams",
1170
+ "trees",
1171
+ "turkeys",
1172
+ "turtles",
1173
+ "vans",
1174
+ "views",
1175
+ "walls",
1176
+ "wasps",
1177
+ "waves",
1178
+ "ways",
1179
+ "webs",
1180
+ "weeks",
1181
+ "windows",
1182
+ "wings",
1183
+ "wolves",
1184
+ "wombats",
1185
+ "words",
1186
+ "worlds",
1187
+ "worms",
1188
+ "yaks",
1189
+ "years",
1190
+ "zebras",
1191
+ "zoos"
1192
+ ];
1193
+ exports.verbs = [
1194
+ "accept",
1195
+ "act",
1196
+ "add",
1197
+ "admire",
1198
+ "agree",
1199
+ "allow",
1200
+ "appear",
1201
+ "argue",
1202
+ "arrive",
1203
+ "ask",
1204
+ "attack",
1205
+ "attend",
1206
+ "bake",
1207
+ "bathe",
1208
+ "battle",
1209
+ "beam",
1210
+ "beg",
1211
+ "begin",
1212
+ "behave",
1213
+ "bet",
1214
+ "boil",
1215
+ "bow",
1216
+ "brake",
1217
+ "brush",
1218
+ "build",
1219
+ "burn",
1220
+ "buy",
1221
+ "call",
1222
+ "camp",
1223
+ "care",
1224
+ "carry",
1225
+ "change",
1226
+ "cheat",
1227
+ "check",
1228
+ "cheer",
1229
+ "chew",
1230
+ "clap",
1231
+ "clean",
1232
+ "cough",
1233
+ "count",
1234
+ "cover",
1235
+ "crash",
1236
+ "create",
1237
+ "cross",
1238
+ "cry",
1239
+ "cut",
1240
+ "dance",
1241
+ "decide",
1242
+ "deny",
1243
+ "design",
1244
+ "dig",
1245
+ "divide",
1246
+ "do",
1247
+ "double",
1248
+ "doubt",
1249
+ "draw",
1250
+ "dream",
1251
+ "dress",
1252
+ "drive",
1253
+ "drop",
1254
+ "drum",
1255
+ "eat",
1256
+ "end",
1257
+ "enjoy",
1258
+ "enter",
1259
+ "exist",
1260
+ "fail",
1261
+ "fall",
1262
+ "feel",
1263
+ "fetch",
1264
+ "film",
1265
+ "find",
1266
+ "fix",
1267
+ "flash",
1268
+ "float",
1269
+ "flow",
1270
+ "fly",
1271
+ "fold",
1272
+ "follow",
1273
+ "fry",
1274
+ "give",
1275
+ "glow",
1276
+ "go",
1277
+ "grab",
1278
+ "greet",
1279
+ "grin",
1280
+ "grow",
1281
+ "guess",
1282
+ "hammer",
1283
+ "hang",
1284
+ "happen",
1285
+ "heal",
1286
+ "hear",
1287
+ "help",
1288
+ "hide",
1289
+ "hope",
1290
+ "hug",
1291
+ "hunt",
1292
+ "invent",
1293
+ "invite",
1294
+ "itch",
1295
+ "jam",
1296
+ "jog",
1297
+ "join",
1298
+ "joke",
1299
+ "judge",
1300
+ "juggle",
1301
+ "jump",
1302
+ "kick",
1303
+ "kiss",
1304
+ "kneel",
1305
+ "knock",
1306
+ "know",
1307
+ "laugh",
1308
+ "lay",
1309
+ "lead",
1310
+ "learn",
1311
+ "leave",
1312
+ "lick",
1313
+ "lie",
1314
+ "like",
1315
+ "listen",
1316
+ "live",
1317
+ "look",
1318
+ "lose",
1319
+ "love",
1320
+ "make",
1321
+ "march",
1322
+ "marry",
1323
+ "mate",
1324
+ "matter",
1325
+ "melt",
1326
+ "mix",
1327
+ "move",
1328
+ "nail",
1329
+ "notice",
1330
+ "obey",
1331
+ "occur",
1332
+ "open",
1333
+ "own",
1334
+ "pay",
1335
+ "peel",
1336
+ "pick",
1337
+ "play",
1338
+ "poke",
1339
+ "post",
1340
+ "press",
1341
+ "prove",
1342
+ "pull",
1343
+ "pump",
1344
+ "punch",
1345
+ "push",
1346
+ "raise",
1347
+ "read",
1348
+ "refuse",
1349
+ "relate",
1350
+ "relax",
1351
+ "remain",
1352
+ "repair",
1353
+ "repeat",
1354
+ "reply",
1355
+ "report",
1356
+ "rescue",
1357
+ "rest",
1358
+ "retire",
1359
+ "return",
1360
+ "rhyme",
1361
+ "ring",
1362
+ "roll",
1363
+ "rule",
1364
+ "run",
1365
+ "rush",
1366
+ "say",
1367
+ "scream",
1368
+ "search",
1369
+ "see",
1370
+ "sell",
1371
+ "send",
1372
+ "serve",
1373
+ "shake",
1374
+ "share",
1375
+ "shave",
1376
+ "shine",
1377
+ "shop",
1378
+ "shout",
1379
+ "show",
1380
+ "sin",
1381
+ "sing",
1382
+ "sink",
1383
+ "sip",
1384
+ "sit",
1385
+ "sleep",
1386
+ "slide",
1387
+ "smash",
1388
+ "smell",
1389
+ "smile",
1390
+ "smoke",
1391
+ "sneeze",
1392
+ "sniff",
1393
+ "sort",
1394
+ "speak",
1395
+ "spend",
1396
+ "stand",
1397
+ "stare",
1398
+ "start",
1399
+ "stay",
1400
+ "stick",
1401
+ "stop",
1402
+ "strive",
1403
+ "study",
1404
+ "swim",
1405
+ "switch",
1406
+ "take",
1407
+ "talk",
1408
+ "tan",
1409
+ "tap",
1410
+ "taste",
1411
+ "teach",
1412
+ "tease",
1413
+ "tell",
1414
+ "thank",
1415
+ "think",
1416
+ "throw",
1417
+ "tickle",
1418
+ "tie",
1419
+ "trade",
1420
+ "train",
1421
+ "travel",
1422
+ "try",
1423
+ "turn",
1424
+ "type",
1425
+ "unite",
1426
+ "vanish",
1427
+ "visit",
1428
+ "wait",
1429
+ "walk",
1430
+ "warn",
1431
+ "wash",
1432
+ "watch",
1433
+ "wave",
1434
+ "wear",
1435
+ "win",
1436
+ "wink",
1437
+ "wish",
1438
+ "wonder",
1439
+ "work",
1440
+ "worry",
1441
+ "write",
1442
+ "yawn",
1443
+ "yell"
1444
+ ];
1445
+ exports.adverbs = [
1446
+ "bravely",
1447
+ "brightly",
1448
+ "busily",
1449
+ "daily",
1450
+ "freely",
1451
+ "hungrily",
1452
+ "joyously",
1453
+ "knowingly",
1454
+ "lazily",
1455
+ "noisily",
1456
+ "oddly",
1457
+ "politely",
1458
+ "quickly",
1459
+ "quietly",
1460
+ "rapidly",
1461
+ "safely",
1462
+ "sleepily",
1463
+ "slowly",
1464
+ "truly",
1465
+ "yearly"
1466
+ ];
1467
+ function random(arr) {
1468
+ return arr[Math.floor(Math.random() * arr.length)];
1469
+ }
1470
+ function longest(arr) {
1471
+ return arr.reduce(function(a, b) {
1472
+ return a.length > b.length ? a : b;
1473
+ });
1474
+ }
1475
+ function shortest(arr) {
1476
+ return arr.reduce(function(a, b) {
1477
+ return a.length < b.length ? a : b;
1478
+ });
1479
+ }
1480
+ function humanId(options) {
1481
+ if (options === void 0) options = {};
1482
+ if (typeof options === "string") options = { separator: options };
1483
+ if (typeof options === "boolean") options = { capitalize: options };
1484
+ var _a = options.separator, separator = _a === void 0 ? "" : _a, _b = options.capitalize, capitalize = _b === void 0 ? true : _b, _c = options.adjectiveCount, adjectiveCount = _c === void 0 ? 1 : _c, _d = options.addAdverb, addAdverb = _d === void 0 ? false : _d;
1485
+ var res = __spreadArray(__spreadArray(__spreadArray([], __spreadArray([], Array(adjectiveCount), true).map(function(_) {
1486
+ return random(exports.adjectives);
1487
+ }), true), [random(exports.nouns), random(exports.verbs)], false), addAdverb ? [random(exports.adverbs)] : [], true);
1488
+ if (capitalize) res = res.map(function(r) {
1489
+ return r.charAt(0).toUpperCase() + r.substr(1);
1490
+ });
1491
+ return res.join(separator);
1492
+ }
1493
+ exports.humanId = humanId;
1494
+ function poolSize(options) {
1495
+ if (options === void 0) options = {};
1496
+ var _a = options.adjectiveCount, adjectiveCount = _a === void 0 ? 1 : _a, _b = options.addAdverb, addAdverb = _b === void 0 ? false : _b;
1497
+ return exports.adjectives.length * adjectiveCount * exports.nouns.length * exports.verbs.length * (addAdverb ? exports.adverbs.length : 1);
1498
+ }
1499
+ exports.poolSize = poolSize;
1500
+ function maxLength(options) {
1501
+ if (options === void 0) options = {};
1502
+ var _a = options.adjectiveCount, adjectiveCount = _a === void 0 ? 1 : _a, _b = options.addAdverb, addAdverb = _b === void 0 ? false : _b, _c = options.separator, separator = _c === void 0 ? "" : _c;
1503
+ return longest(exports.adjectives).length * adjectiveCount + adjectiveCount * separator.length + longest(exports.nouns).length + separator.length + longest(exports.verbs).length + (addAdverb ? longest(exports.adverbs).length + separator.length : 0);
1504
+ }
1505
+ exports.maxLength = maxLength;
1506
+ function minLength(options) {
1507
+ if (options === void 0) options = {};
1508
+ var _a = options.adjectiveCount, adjectiveCount = _a === void 0 ? 1 : _a, _b = options.addAdverb, addAdverb = _b === void 0 ? false : _b, _c = options.separator, separator = _c === void 0 ? "" : _c;
1509
+ return shortest(exports.adjectives).length * adjectiveCount + adjectiveCount * separator.length + shortest(exports.nouns).length + separator.length + shortest(exports.verbs).length + (addAdverb ? shortest(exports.adverbs).length + separator.length : 0);
1510
+ }
1511
+ exports.minLength = minLength;
1512
+ exports.default = humanId;
1513
+ })))();
1514
+ var MAX_RETRIES = 10;
1515
+ function generate() {
1516
+ return (0, import_dist.humanId)({
1517
+ separator: "-",
1518
+ capitalize: false
1519
+ });
1520
+ }
1521
+ async function generateUniqueName(gitRoot, worktreesDirectory, directoryNameSeparator) {
1522
+ for (let i = 0; i < MAX_RETRIES; i++) {
1523
+ const name = generate();
1524
+ if (isErr(validateWorktreeName(name))) continue;
1525
+ if (await validateWorktreeDirectoryExists(getWorktreePathFromDirectory(worktreesDirectory, name, directoryNameSeparator))) continue;
1526
+ const result = await branchExists(gitRoot, name);
1527
+ if (isErr(result)) return err(result.error);
1528
+ if (result.value) continue;
1529
+ return ok(name);
1530
+ }
1531
+ return err(/* @__PURE__ */ new Error("Failed to generate a unique worktree name after maximum retries"));
1532
+ }
1533
+ async function createWorktree(gitRoot, worktreeDirectory, name, options, postCreateCopyFiles, postCreateCommands, directoryNameSeparator) {
1534
+ const nameValidation = validateWorktreeName(name);
1535
+ if (isErr(nameValidation)) return nameValidation;
1536
+ const { branch = name, base = "HEAD", copyFiles: requestedCopyFiles, logger } = options;
1537
+ const worktreePath = getWorktreePathFromDirectory(worktreeDirectory, name, directoryNameSeparator);
1538
+ try {
1539
+ await fs.access(worktreeDirectory);
1540
+ } catch {
1541
+ await fs.mkdir(worktreeDirectory, { recursive: true });
1542
+ }
1543
+ const validation = await validateWorktreeDoesNotExist(gitRoot, worktreeDirectory, name);
1544
+ if (isErr(validation)) return err(validation.error);
1545
+ try {
1546
+ await addWorktree({
1547
+ path: worktreePath,
1548
+ branch,
1549
+ base,
1550
+ cwd: gitRoot
1551
+ });
1552
+ let copiedFiles;
1553
+ let skippedFiles;
1554
+ let copyError;
1555
+ const filesToCopy = mergeWorktreeCopyFiles(requestedCopyFiles, postCreateCopyFiles);
1556
+ if (filesToCopy) {
1557
+ const copyResult = await copyFiles(gitRoot, worktreePath, filesToCopy);
1558
+ if (isOk(copyResult)) {
1559
+ copiedFiles = copyResult.value.copiedFiles;
1560
+ skippedFiles = copyResult.value.skippedFiles;
1561
+ } else copyError = copyResult.error.message;
1562
+ }
1563
+ if (postCreateCommands && postCreateCommands.length > 0) {
1564
+ logger?.log?.("\nRunning post-create commands...");
1565
+ const commandsResult = await executePostCreateCommands({
1566
+ gitRoot,
1567
+ worktreesDirectory: worktreeDirectory,
1568
+ worktreeName: name,
1569
+ commands: postCreateCommands,
1570
+ logger
1571
+ });
1572
+ if (isErr(commandsResult)) return err(new WorktreeError(commandsResult.error.message));
1573
+ }
1574
+ return ok({
1575
+ message: `Created worktree '${name}' at ${worktreePath}`,
1576
+ path: worktreePath,
1577
+ copiedFiles,
1578
+ skippedFiles,
1579
+ copyError
1580
+ });
1581
+ } catch (error) {
1582
+ return err(new WorktreeError(`worktree add failed: ${error instanceof Error ? error.message : String(error)}`));
1583
+ }
1584
+ }
1585
+ async function runCreateWorktree(options) {
1586
+ const actionResult = resolveWorktreeAction(options.action);
1587
+ if (isErr(actionResult)) return actionResult;
1588
+ const actionValidation = await validateWorktreeAction(actionResult.value);
1589
+ if (isErr(actionValidation)) return actionValidation;
1590
+ try {
1591
+ const context = await createContext(options.gitRoot ?? await getGitRoot());
1592
+ let worktreeName = options.name;
1593
+ if (!worktreeName) {
1594
+ const nameResult = await generateUniqueName(context.gitRoot, context.worktreesDirectory, context.directoryNameSeparator);
1595
+ if (isErr(nameResult)) return err(nameResult.error);
1596
+ worktreeName = nameResult.value;
1597
+ }
1598
+ const filesToCopy = mergeWorktreeCopyFiles(context.config?.postCreate?.copyFiles, options.copyFiles);
1599
+ const createResult = await createWorktree(context.gitRoot, context.worktreesDirectory, worktreeName, {
1600
+ base: options.base,
1601
+ copyFiles: filesToCopy,
1602
+ logger: options.logger
1603
+ }, void 0, context.config?.postCreate?.commands, context.directoryNameSeparator);
1604
+ if (isErr(createResult)) return err(createResult.error);
1605
+ options.logger?.log(createResult.value.message);
1606
+ if (createResult.value.copyError) options.logger?.warn?.(`\nWarning: Failed to copy some files: ${createResult.value.copyError}`);
1607
+ const worktreeActionResult = await runWorktreeAction({
1608
+ gitRoot: context.gitRoot,
1609
+ worktreeDirectory: context.worktreesDirectory,
1610
+ worktreeName,
1611
+ worktreePath: createResult.value.path,
1612
+ action: actionResult.value,
1613
+ logger: options.logger,
1614
+ exitWithProcessCode: true
1615
+ });
1616
+ if (isErr(worktreeActionResult)) return err(worktreeActionResult.error);
1617
+ return ok({
1618
+ name: worktreeName,
1619
+ path: createResult.value.path,
1620
+ message: createResult.value.message,
1621
+ copyError: createResult.value.copyError,
1622
+ exitProcessCode: worktreeActionResult.value.exitProcessCode
1623
+ });
1624
+ } catch (error) {
1625
+ return err(error instanceof Error ? error : new Error(String(error)));
1626
+ }
1627
+ }
1628
+ async function removeWorktree(gitRoot, worktreePath, force = false) {
1629
+ await removeWorktree$1({
1630
+ gitRoot,
1631
+ path: worktreePath,
1632
+ force
1633
+ });
1634
+ }
1635
+ async function deleteBranch(gitRoot, branchName) {
1636
+ try {
1637
+ await deleteBranch$1({
1638
+ gitRoot,
1639
+ branch: branchName
1640
+ });
1641
+ return ok(true);
1642
+ } catch (error) {
1643
+ return err(new WorktreeError(`branch delete failed: ${error instanceof Error ? error.message : String(error)}`));
1644
+ }
1645
+ }
1646
+ function getCodexBin() {
1647
+ return process.env.PHANTOM_SERVE_CODEX_BIN ?? "codex";
1648
+ }
1649
+ function createUserInput(text, options = {}) {
1650
+ const input = [{
1651
+ type: "text",
1652
+ text,
1653
+ text_elements: []
1654
+ }];
1655
+ for (const skill of options.skills ?? []) input.push({
1656
+ type: "skill",
1657
+ name: skill.name,
1658
+ path: skill.path
1659
+ });
1660
+ for (const file of options.files ?? []) input.push({
1661
+ type: "mention",
1662
+ name: file.name,
1663
+ path: file.path
1664
+ });
1665
+ return input;
1666
+ }
1667
+ var CodexBridge = class {
1668
+ proc = null;
1669
+ initialized = null;
1670
+ nextId = 1;
1671
+ stderr = "";
1672
+ pendingRequests = /* @__PURE__ */ new Map();
1673
+ serverRequests = /* @__PURE__ */ new Map();
1674
+ notificationHandlers = /* @__PURE__ */ new Set();
1675
+ serverRequestHandlers = /* @__PURE__ */ new Set();
1676
+ processExitHandlers = /* @__PURE__ */ new Set();
1677
+ constructor(codexBin = getCodexBin(), spawnCodexProcess = spawn) {
1678
+ this.codexBin = codexBin;
1679
+ this.spawnCodexProcess = spawnCodexProcess;
1680
+ }
1681
+ onNotification(handler) {
1682
+ this.notificationHandlers.add(handler);
1683
+ return () => this.notificationHandlers.delete(handler);
1684
+ }
1685
+ onServerRequest(handler) {
1686
+ this.serverRequestHandlers.add(handler);
1687
+ return () => this.serverRequestHandlers.delete(handler);
1688
+ }
1689
+ onProcessExit(handler) {
1690
+ this.processExitHandlers.add(handler);
1691
+ return () => this.processExitHandlers.delete(handler);
1692
+ }
1693
+ async ensureStarted() {
1694
+ if (this.initialized) return this.initialized;
1695
+ this.initialized = this.start();
1696
+ return this.initialized;
1697
+ }
1698
+ async request(method, params) {
1699
+ await this.ensureStarted();
1700
+ return this.sendRequest(method, params);
1701
+ }
1702
+ async notify(method, params) {
1703
+ await this.ensureStarted();
1704
+ this.write({
1705
+ method,
1706
+ params: params ?? {}
1707
+ });
1708
+ }
1709
+ respondToServerRequest(requestId, result) {
1710
+ const serverRequestId = this.resolveServerRequestId(requestId);
1711
+ if (serverRequestId === null) throw new Error(`Codex server request '${requestId}' was not found`);
1712
+ this.serverRequests.delete(serverRequestId);
1713
+ this.write({
1714
+ id: serverRequestId,
1715
+ result
1716
+ });
1717
+ }
1718
+ async readAccount() {
1719
+ return this.request("account/read", { refreshToken: false });
1720
+ }
1721
+ async listModels() {
1722
+ return this.request("model/list", {
1723
+ limit: 50,
1724
+ includeHidden: false
1725
+ });
1726
+ }
1727
+ async listSkills(cwds) {
1728
+ return this.request("skills/list", {
1729
+ cwds,
1730
+ forceReload: false
1731
+ });
1732
+ }
1733
+ async searchFiles(query, roots) {
1734
+ return this.request("fuzzyFileSearch", {
1735
+ query,
1736
+ roots,
1737
+ cancellationToken: null
1738
+ });
1739
+ }
1740
+ async startThread(cwd, options = {}) {
1741
+ return this.request("thread/start", {
1742
+ cwd,
1743
+ model: options.model,
1744
+ serviceName: "phantom_serve",
1745
+ experimentalRawEvents: false,
1746
+ persistExtendedHistory: true
1747
+ });
1748
+ }
1749
+ async resumeThread(threadId, cwd) {
1750
+ return this.request("thread/resume", {
1751
+ threadId,
1752
+ cwd,
1753
+ excludeTurns: true,
1754
+ persistExtendedHistory: true
1755
+ });
1756
+ }
1757
+ async startTurn(threadId, text, cwd, options = {}) {
1758
+ return this.request("turn/start", {
1759
+ threadId,
1760
+ cwd,
1761
+ input: createUserInput(text, options),
1762
+ model: options.model,
1763
+ effort: options.effort
1764
+ });
1765
+ }
1766
+ async steerTurn(threadId, turnId, text, options = {}) {
1767
+ return this.request("turn/steer", {
1768
+ threadId,
1769
+ expectedTurnId: turnId,
1770
+ input: createUserInput(text, options),
1771
+ model: options.model,
1772
+ effort: options.effort
1773
+ });
1774
+ }
1775
+ async interruptTurn(threadId, turnId) {
1776
+ return this.request("turn/interrupt", {
1777
+ threadId,
1778
+ turnId
1779
+ });
1780
+ }
1781
+ async start() {
1782
+ this.stderr = "";
1783
+ this.proc = this.spawnCodexProcess(this.codexBin, ["app-server"], { stdio: [
1784
+ "pipe",
1785
+ "pipe",
1786
+ "pipe"
1787
+ ] });
1788
+ const proc = this.proc;
1789
+ proc.stderr.on("data", (chunk) => {
1790
+ this.stderr += chunk.toString("utf8");
1791
+ if (this.stderr.length > 8e3) this.stderr = this.stderr.slice(-8e3);
1792
+ });
1793
+ proc.on("error", (error) => {
1794
+ this.handleProcessExit(proc, error);
1795
+ });
1796
+ proc.on("exit", (code, signal) => {
1797
+ const suffix = this.stderr.trim() ? `: ${this.stderr.trim()}` : "";
1798
+ this.handleProcessExit(proc, /* @__PURE__ */ new Error(`Codex App Server exited with ${signal ? `signal ${signal}` : `code ${code ?? 0}`}${suffix}`));
1799
+ });
1800
+ readline.createInterface({ input: proc.stdout }).on("line", (line) => this.handleLine(line));
1801
+ await this.sendRequest("initialize", {
1802
+ clientInfo: {
1803
+ name: "phantom_serve",
1804
+ title: "Phantom Serve",
1805
+ version: "0.1.0"
1806
+ },
1807
+ capabilities: { experimentalApi: true }
1808
+ });
1809
+ this.write({
1810
+ method: "initialized",
1811
+ params: {}
1812
+ });
1813
+ }
1814
+ sendRequest(method, params) {
1815
+ const id = this.nextId++;
1816
+ const promise = new Promise((resolve, reject) => {
1817
+ this.pendingRequests.set(id, {
1818
+ resolve,
1819
+ reject
1820
+ });
1821
+ });
1822
+ this.write({
1823
+ id,
1824
+ method,
1825
+ params
1826
+ });
1827
+ return promise;
1828
+ }
1829
+ write(message) {
1830
+ if (!this.proc?.stdin.writable) throw new Error("Codex App Server is not running");
1831
+ this.proc.stdin.write(`${JSON.stringify(message)}\n`);
1832
+ }
1833
+ resolveServerRequestId(requestId) {
1834
+ if (this.serverRequests.has(requestId)) return requestId;
1835
+ if (typeof requestId === "number") return null;
1836
+ const numericRequestId = Number(requestId);
1837
+ if (Number.isFinite(numericRequestId) && this.serverRequests.has(numericRequestId)) return numericRequestId;
1838
+ return null;
1839
+ }
1840
+ handleLine(line) {
1841
+ let message;
1842
+ try {
1843
+ message = JSON.parse(line);
1844
+ } catch {
1845
+ return;
1846
+ }
1847
+ if (message.id !== void 0 && !message.method) {
1848
+ const pending = this.pendingRequests.get(message.id);
1849
+ if (!pending) return;
1850
+ this.pendingRequests.delete(message.id);
1851
+ if (message.error) pending.reject(new Error(message.error.message));
1852
+ else pending.resolve(message.result);
1853
+ return;
1854
+ }
1855
+ if (message.id !== void 0 && message.method) {
1856
+ this.serverRequests.set(message.id, message);
1857
+ for (const handler of this.serverRequestHandlers) handler(message);
1858
+ return;
1859
+ }
1860
+ for (const handler of this.notificationHandlers) handler(message);
1861
+ }
1862
+ rejectPending(error) {
1863
+ for (const pending of this.pendingRequests.values()) pending.reject(error);
1864
+ this.pendingRequests.clear();
1865
+ }
1866
+ handleProcessExit(proc, error) {
1867
+ if (this.proc !== proc) return;
1868
+ this.rejectPending(error);
1869
+ this.serverRequests.clear();
1870
+ this.proc = null;
1871
+ this.initialized = null;
1872
+ for (const handler of this.processExitHandlers) handler(error);
1873
+ }
1874
+ };
1875
+ function getParamObject(params) {
1876
+ return params && typeof params === "object" && !Array.isArray(params) ? params : null;
1877
+ }
1878
+ function extractThreadId(result) {
1879
+ const threadId = getParamObject(getParamObject(result)?.thread)?.id;
1880
+ if (typeof threadId !== "string") throw new Error("Codex response did not include a thread id");
1881
+ return threadId;
1882
+ }
1883
+ function extractTurnId(result) {
1884
+ const turnId = getParamObject(getParamObject(result)?.turn)?.id;
1885
+ return typeof turnId === "string" ? turnId : null;
1886
+ }
1887
+ function extractThreadIdFromParams(params) {
1888
+ const object = getParamObject(params);
1889
+ if (!object) return null;
1890
+ if (typeof object.threadId === "string") return object.threadId;
1891
+ const thread = getParamObject(object.thread);
1892
+ return typeof thread?.id === "string" ? thread.id : null;
1893
+ }
1894
+ function extractTurnIdFromParams(params) {
1895
+ const object = getParamObject(params);
1896
+ if (!object) return null;
1897
+ if (typeof object.turnId === "string") return object.turnId;
1898
+ const turn = getParamObject(object.turn);
1899
+ return typeof turn?.id === "string" ? turn.id : null;
1900
+ }
1901
+ function getServerRequestId(requestId) {
1902
+ if (typeof requestId === "string" || typeof requestId === "number") return requestId;
1903
+ return null;
1904
+ }
1905
+ function extractServerRequestIdFromParams(params) {
1906
+ const object = getParamObject(params);
1907
+ if (!object) return null;
1908
+ return getServerRequestId(object.requestId) ?? getServerRequestId(object.serverRequestId) ?? getServerRequestId(object.id);
1909
+ }
1910
+ function mapCodexMethodToEvent(method) {
1911
+ if (method === "thread/started") return "agent.thread.started";
1912
+ if (method === "turn/started") return "agent.turn.started";
1913
+ if (method === "turn/completed") return "agent.turn.completed";
1914
+ if (method.startsWith("item/") && method.endsWith("/requestApproval")) return "agent.approval.requested";
1915
+ if (method.startsWith("item/")) return method.includes("delta") ? "agent.item.delta" : "agent.item.updated";
1916
+ if (method === "serverRequest/resolved") return "agent.approval.resolved";
1917
+ if (method === "account/updated") return "auth.updated";
1918
+ if (method === "error") return "agent.error";
1919
+ return "agent.event";
1920
+ }
1921
+ function summarizeCodexEvent(method, params) {
1922
+ const object = getParamObject(params);
1923
+ if (method === "item/started" || method === "item/completed") {
1924
+ const item = getParamObject(object?.item);
1925
+ return `${method}: ${typeof item?.type === "string" ? item.type : "item"}`;
1926
+ }
1927
+ if (method === "turn/completed") {
1928
+ const turn = getParamObject(object?.turn);
1929
+ return `turn completed: ${typeof turn?.status === "string" ? turn.status : "unknown"}`;
1930
+ }
1931
+ if (method === "error") {
1932
+ const error = getParamObject(object?.error);
1933
+ return typeof error?.message === "string" ? error.message : "Codex error";
1934
+ }
1935
+ return method;
1936
+ }
1937
+ async function listCodexSessionsForWorktree({ branchName, codexHome = getDefaultCodexHome(), projectId, worktreeName, worktreePath }) {
1938
+ return await listCodexSessionsForWorktrees({
1939
+ codexHome,
1940
+ projectId,
1941
+ worktrees: [{
1942
+ branchName,
1943
+ worktreeName,
1944
+ worktreePath
1945
+ }]
1946
+ });
1947
+ }
1948
+ async function listCodexSessionsForWorktrees({ codexHome = getDefaultCodexHome(), projectId, worktrees }) {
1949
+ const files = await listCodexSessionFiles(codexHome);
1950
+ const worktreeByPath = new Map(worktrees.map((worktree) => [worktree.worktreePath, worktree]));
1951
+ const parsedSessionsById = /* @__PURE__ */ new Map();
1952
+ for (const file of files) {
1953
+ const session = await parseCodexSession(file);
1954
+ if (!session || !worktreeByPath.has(session.worktreePath)) continue;
1955
+ const existingSession = parsedSessionsById.get(session.sessionId);
1956
+ if (!existingSession || existingSession.updatedAt.localeCompare(session.updatedAt) < 0) parsedSessionsById.set(session.sessionId, session);
1957
+ }
1958
+ return Array.from(parsedSessionsById.values()).map((session) => createImportedCodexSession({
1959
+ projectId,
1960
+ session,
1961
+ worktree: worktreeByPath.get(session.worktreePath)
1962
+ })).sort((left, right) => right.chat.updatedAt.localeCompare(left.chat.updatedAt));
1963
+ }
1964
+ function getDefaultCodexHome() {
1965
+ return process.env.CODEX_HOME ?? join(homedir(), ".codex");
1966
+ }
1967
+ async function listCodexSessionFiles(codexHome) {
1968
+ const roots = [join(codexHome, "sessions"), join(codexHome, "archived_sessions")];
1969
+ return (await Promise.all(roots.map((root) => listJsonlFiles(root)))).flat();
1970
+ }
1971
+ async function listJsonlFiles(root) {
1972
+ let entries;
1973
+ try {
1974
+ entries = await readdir(root, { withFileTypes: true });
1975
+ } catch (error) {
1976
+ if (isIgnorableHistoryRootError(error)) return [];
1977
+ throw error;
1978
+ }
1979
+ return (await Promise.all(entries.map(async (entry) => {
1980
+ const path = join(root, entry.name);
1981
+ if (entry.isDirectory()) return await listJsonlFiles(path);
1982
+ return entry.isFile() && entry.name.endsWith(".jsonl") ? [path] : [];
1983
+ }))).flat();
1984
+ }
1985
+ async function parseCodexSession(path) {
1986
+ let content;
1987
+ try {
1988
+ content = await readFile(path, "utf8");
1989
+ } catch (error) {
1990
+ if (isIgnorableHistoryFileError(error)) return null;
1991
+ throw error;
1992
+ }
1993
+ let sessionId = sessionIdFromPath(path);
1994
+ let worktreePath = null;
1995
+ let createdAt = null;
1996
+ let updatedAt = null;
1997
+ let title = null;
1998
+ let isSubagentSession = false;
1999
+ const messages = [];
2000
+ for (const line of content.split(/\n/)) {
2001
+ if (!line.trim()) continue;
2002
+ const event = parseJsonObject(line);
2003
+ const timestamp = getString(event?.timestamp);
2004
+ if (timestamp) {
2005
+ createdAt ??= timestamp;
2006
+ updatedAt = timestamp;
2007
+ }
2008
+ if (event?.type === "session_meta") {
2009
+ const payload = getObject(event.payload);
2010
+ sessionId = getString(payload?.id) ?? sessionId;
2011
+ worktreePath = getString(payload?.cwd) ?? worktreePath;
2012
+ createdAt = getString(payload?.timestamp) ?? createdAt;
2013
+ isSubagentSession = Boolean(getObject(payload?.source)?.subagent);
2014
+ continue;
2015
+ }
2016
+ if (event?.type === "event_msg") {
2017
+ const payload = getObject(event.payload);
2018
+ if (payload?.type === "thread_name_updated") title = getString(payload.thread_name) ?? title;
2019
+ continue;
2020
+ }
2021
+ if (event?.type !== "response_item") continue;
2022
+ const payload = getObject(event.payload);
2023
+ if (payload?.type !== "message") continue;
2024
+ const role = payload.role;
2025
+ if (role !== "assistant" && role !== "user") continue;
2026
+ const text = role === "user" ? stripCodexContextPreamble(extractMessageText(payload.content)) : extractMessageText(payload.content);
2027
+ if (!text) continue;
2028
+ messages.push({
2029
+ createdAt: timestamp ?? updatedAt ?? createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2030
+ role,
2031
+ text
2032
+ });
2033
+ title ??= role === "user" ? truncateTitle(text) : null;
2034
+ }
2035
+ if (isSubagentSession || !sessionId || !worktreePath) return null;
2036
+ return {
2037
+ createdAt: createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2038
+ messages,
2039
+ sessionId,
2040
+ title: title ?? sessionId.slice(0, 8),
2041
+ updatedAt: updatedAt ?? createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2042
+ worktreePath
2043
+ };
2044
+ }
2045
+ function createImportedCodexSession({ projectId, session, worktree }) {
2046
+ const chatId = createImportedChatId(session.sessionId);
2047
+ return {
2048
+ chat: {
2049
+ id: chatId,
2050
+ projectId,
2051
+ worktreeName: worktree.worktreeName,
2052
+ worktreePath: worktree.worktreePath,
2053
+ branchName: worktree.branchName,
2054
+ codexThreadId: session.sessionId,
2055
+ title: session.title,
2056
+ status: "idle",
2057
+ activeTurnId: null,
2058
+ createdAt: session.createdAt,
2059
+ updatedAt: session.updatedAt
2060
+ },
2061
+ messages: session.messages.map((message, index) => ({
2062
+ id: `${chatId}_msg_${index}`,
2063
+ chatId,
2064
+ role: message.role,
2065
+ text: message.text,
2066
+ createdAt: message.createdAt
2067
+ }))
2068
+ };
2069
+ }
2070
+ function createImportedChatId(sessionId) {
2071
+ return `chat_codex_${sessionId}`;
2072
+ }
2073
+ function sessionIdFromPath(path) {
2074
+ return basename(path).replace(/^rollout-/, "").replace(/\.jsonl$/, "");
2075
+ }
2076
+ function extractMessageText(content) {
2077
+ if (!Array.isArray(content)) return "";
2078
+ return content.map((item) => {
2079
+ return getString(getObject(item)?.text) ?? "";
2080
+ }).filter(Boolean).join("\n\n").trim();
2081
+ }
2082
+ function stripCodexContextPreamble(text) {
2083
+ let strippedText = text.trimStart();
2084
+ while (true) {
2085
+ if (strippedText.startsWith("# AGENTS.md instructions")) {
2086
+ const instructionsEnd = strippedText.indexOf("</INSTRUCTIONS>");
2087
+ if (instructionsEnd === -1) return "";
2088
+ strippedText = strippedText.slice(instructionsEnd + 15).trimStart();
2089
+ continue;
2090
+ }
2091
+ if (strippedText.startsWith("<environment_context>")) {
2092
+ const contextEnd = strippedText.indexOf("</environment_context>");
2093
+ if (contextEnd === -1) return "";
2094
+ strippedText = strippedText.slice(contextEnd + 22).trimStart();
2095
+ continue;
2096
+ }
2097
+ return strippedText.trim();
2098
+ }
2099
+ }
2100
+ function truncateTitle(text) {
2101
+ const firstLine = text.trim().split(/\n/)[0] ?? "";
2102
+ return firstLine.length > 32 ? `${firstLine.slice(0, 31)}...` : firstLine;
2103
+ }
2104
+ function parseJsonObject(line) {
2105
+ try {
2106
+ return getObject(JSON.parse(line));
2107
+ } catch {
2108
+ return null;
2109
+ }
2110
+ }
2111
+ function getObject(value) {
2112
+ return value && typeof value === "object" ? value : null;
2113
+ }
2114
+ function getString(value) {
2115
+ return typeof value === "string" ? value : null;
2116
+ }
2117
+ function isIgnorableHistoryFileError(error) {
2118
+ return Boolean(error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES" || error.code === "EPERM"));
2119
+ }
2120
+ function isIgnorableHistoryRootError(error) {
2121
+ return Boolean(error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES" || error.code === "EPERM"));
2122
+ }
2123
+ var chatStatusSchema = _enum([
2124
+ "idle",
2125
+ "running",
2126
+ "waitingForApproval",
2127
+ "failed",
2128
+ "archived"
2129
+ ]);
2130
+ var projectRecordBaseSchema = object({
2131
+ id: string(),
2132
+ name: string(),
2133
+ rootPath: string(),
2134
+ createdAt: string(),
2135
+ updatedAt: string(),
2136
+ lastOpenedAt: string()
2137
+ });
2138
+ var chatMessageRecordBaseSchema = object({
2139
+ id: string(),
2140
+ chatId: string(),
2141
+ role: _enum([
2142
+ "user",
2143
+ "assistant",
2144
+ "event",
2145
+ "error"
2146
+ ]),
2147
+ text: string(),
2148
+ eventType: string().optional(),
2149
+ itemId: string().optional(),
2150
+ createdAt: string()
2151
+ });
2152
+ var chatRecordBaseSchema = object({
2153
+ id: string(),
2154
+ projectId: string(),
2155
+ worktreeName: string(),
2156
+ worktreePath: string(),
2157
+ branchName: string(),
2158
+ codexThreadId: string().nullable(),
2159
+ title: string(),
2160
+ status: chatStatusSchema,
2161
+ activeTurnId: string().nullable().optional(),
2162
+ createdAt: string(),
2163
+ updatedAt: string()
2164
+ });
2165
+ var nullableStringFromUnknownSchema = preprocess((value) => typeof value === "string" ? value : null, string().nullable());
2166
+ object({
2167
+ version: literal(1),
2168
+ projects: array(projectRecordBaseSchema),
2169
+ chats: array(chatRecordBaseSchema),
2170
+ messages: array(chatMessageRecordBaseSchema),
2171
+ selectedProjectId: nullableStringFromUnknownSchema,
2172
+ selectedChatId: nullableStringFromUnknownSchema
2173
+ });
2174
+ var projectRecordSchema = projectRecordBaseSchema.passthrough();
2175
+ var chatMessageRecordSchema = chatMessageRecordBaseSchema.passthrough();
2176
+ var chatRecordSchema = chatRecordBaseSchema.passthrough();
2177
+ var serveStateSchema = object({
2178
+ version: literal(1),
2179
+ projects: array(projectRecordSchema),
2180
+ chats: array(chatRecordSchema),
2181
+ messages: array(chatMessageRecordSchema),
2182
+ selectedProjectId: nullableStringFromUnknownSchema,
2183
+ selectedChatId: nullableStringFromUnknownSchema
2184
+ }).passthrough();
2185
+ var STATE_FILE_NAME = "state.json";
2186
+ function now() {
2187
+ return (/* @__PURE__ */ new Date()).toISOString();
2188
+ }
2189
+ function createEmptyState() {
2190
+ return {
2191
+ version: 1,
2192
+ projects: [],
2193
+ chats: [],
2194
+ messages: [],
2195
+ selectedProjectId: null,
2196
+ selectedChatId: null
2197
+ };
2198
+ }
2199
+ function getDefaultServeDataDir(env = process.env) {
2200
+ const stateHome = env.XDG_STATE_HOME;
2201
+ return join(stateHome && isAbsolute(stateHome) ? stateHome : join(homedir(), ".local", "state"), "phantom");
2202
+ }
2203
+ function getServeDataDir() {
2204
+ return process.env.PHANTOM_SERVE_DATA_DIR ?? getDefaultServeDataDir();
2205
+ }
2206
+ function getStatePath(dataDir) {
2207
+ return join(dataDir, STATE_FILE_NAME);
2208
+ }
2209
+ function validateState(value) {
2210
+ if (!value || typeof value !== "object") throw new Error("Serve state is not a JSON object");
2211
+ if (value.version !== 1) throw new Error("Unsupported serve state version");
2212
+ const result = serveStateSchema.safeParse(value);
2213
+ if (!result.success) throw new Error("Serve state has an invalid shape");
2214
+ return result.data;
2215
+ }
2216
+ var ServeStateStore = class {
2217
+ state = null;
2218
+ updateChain = Promise.resolve();
2219
+ constructor(dataDir = getServeDataDir()) {
2220
+ this.dataDir = dataDir;
2221
+ }
2222
+ async loadOrCreateState() {
2223
+ if (this.state) return this.state;
2224
+ const statePath = getStatePath(this.dataDir);
2225
+ try {
2226
+ const content = await readFile(statePath, "utf8");
2227
+ const state = validateState(JSON.parse(content));
2228
+ this.state = state;
2229
+ return state;
2230
+ } catch (error) {
2231
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
2232
+ const state = createEmptyState();
2233
+ await this.save(state);
2234
+ return state;
2235
+ } else throw error;
2236
+ }
2237
+ }
2238
+ async load() {
2239
+ if (this.state) return this.state;
2240
+ const nextLoad = this.updateChain.then(() => this.loadOrCreateState());
2241
+ this.updateChain = nextLoad.catch(() => void 0);
2242
+ return nextLoad;
2243
+ }
2244
+ async save(state) {
2245
+ const statePath = getStatePath(this.dataDir);
2246
+ const temporaryPath = `${statePath}.tmp-${process.pid}-${Date.now()}`;
2247
+ await mkdir(dirname(statePath), { recursive: true });
2248
+ await writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`);
2249
+ await rename(temporaryPath, statePath);
2250
+ this.state = state;
2251
+ }
2252
+ async update(updater) {
2253
+ const nextUpdate = this.updateChain.then(async () => {
2254
+ const state = await this.loadOrCreateState();
2255
+ const nextState = await updater({
2256
+ ...state,
2257
+ projects: [...state.projects],
2258
+ chats: [...state.chats],
2259
+ messages: [...state.messages]
2260
+ });
2261
+ await this.save(nextState);
2262
+ return nextState;
2263
+ });
2264
+ this.updateChain = nextUpdate.catch(() => void 0);
2265
+ return nextUpdate;
2266
+ }
2267
+ };
2268
+ function touchProject(project) {
2269
+ const timestamp = now();
2270
+ return {
2271
+ ...project,
2272
+ updatedAt: timestamp,
2273
+ lastOpenedAt: timestamp
2274
+ };
2275
+ }
2276
+ function createRecordId(prefix) {
2277
+ return `${prefix}_${crypto.randomUUID()}`;
2278
+ }
2279
+ function createTimestamp() {
2280
+ return now();
2281
+ }
2282
+ var DEFAULT_REPLAY_LIMIT = 300;
2283
+ var HEARTBEAT_MS = 15e3;
2284
+ function encodeSseEvent(event) {
2285
+ return [
2286
+ `id: ${event.id}`,
2287
+ `event: ${event.type}`,
2288
+ `data: ${JSON.stringify(event)}`,
2289
+ "",
2290
+ ""
2291
+ ].join("\n");
2292
+ }
2293
+ var EventHub = class {
2294
+ nextId = 1;
2295
+ events = [];
2296
+ listeners = /* @__PURE__ */ new Set();
2297
+ constructor(replayLimit = DEFAULT_REPLAY_LIMIT) {
2298
+ this.replayLimit = replayLimit;
2299
+ }
2300
+ emit(type, data, options = {}) {
2301
+ const event = {
2302
+ id: this.nextId++,
2303
+ type,
2304
+ scope: options.scope ?? (options.chatId ? "chat" : "global"),
2305
+ chatId: options.chatId,
2306
+ data,
2307
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2308
+ };
2309
+ this.events.push(event);
2310
+ if (this.events.length > this.replayLimit) this.events.shift();
2311
+ for (const listener of this.listeners) if (listener.filter(event)) listener.send(event);
2312
+ return event;
2313
+ }
2314
+ subscribe(filter, lastEventId) {
2315
+ const encoder = new TextEncoder();
2316
+ let heartbeat;
2317
+ let listener;
2318
+ return new ReadableStream({
2319
+ start: (controller) => {
2320
+ const write = (chunk) => {
2321
+ controller.enqueue(encoder.encode(chunk));
2322
+ };
2323
+ if (lastEventId !== null) {
2324
+ for (const event of this.events) if (event.id > lastEventId && filter(event)) write(encodeSseEvent(event));
2325
+ }
2326
+ listener = {
2327
+ filter,
2328
+ send: (event) => write(encodeSseEvent(event))
2329
+ };
2330
+ this.listeners.add(listener);
2331
+ heartbeat = setInterval(() => {
2332
+ write(": heartbeat\n\n");
2333
+ }, HEARTBEAT_MS);
2334
+ },
2335
+ cancel: () => {
2336
+ if (heartbeat) clearInterval(heartbeat);
2337
+ if (listener) this.listeners.delete(listener);
2338
+ }
2339
+ });
2340
+ }
2341
+ listenerCount() {
2342
+ return this.listeners.size;
2343
+ }
2344
+ };
2345
+ function createSseResponse(stream) {
2346
+ return new Response(stream, { headers: {
2347
+ "Cache-Control": "no-cache, no-transform",
2348
+ Connection: "keep-alive",
2349
+ "Content-Type": "text/event-stream",
2350
+ "X-Accel-Buffering": "no"
2351
+ } });
2352
+ }
2353
+ function parseLastEventId(request) {
2354
+ const header = request.headers.get("Last-Event-ID");
2355
+ if (!header) return null;
2356
+ const value = Number.parseInt(header, 10);
2357
+ return Number.isFinite(value) ? value : null;
2358
+ }
2359
+ var ServeServices = class {
2360
+ eventHub;
2361
+ store;
2362
+ codex;
2363
+ codexHome;
2364
+ loadedThreadIds = /* @__PURE__ */ new Set();
2365
+ approvalRequests = /* @__PURE__ */ new Map();
2366
+ pendingTurnEvents = /* @__PURE__ */ new Map();
2367
+ pendingChatTurns = /* @__PURE__ */ new Set();
2368
+ constructor(options = {}) {
2369
+ this.eventHub = options.eventHub ?? new EventHub();
2370
+ this.store = options.store ?? new ServeStateStore();
2371
+ this.codex = options.codex ?? new CodexBridge();
2372
+ this.codexHome = options.codexHome;
2373
+ this.codex.onNotification((message) => {
2374
+ this.handleCodexNotification(message);
2375
+ });
2376
+ this.codex.onServerRequest((message) => {
2377
+ this.handleCodexServerRequest(message);
2378
+ });
2379
+ this.codex.onProcessExit((error) => {
2380
+ this.handleCodexProcessExit(error);
2381
+ });
2382
+ }
2383
+ async getHealth() {
2384
+ const state = await this.store.load();
2385
+ return {
2386
+ ok: true,
2387
+ projectCount: state.projects.length,
2388
+ chatCount: state.chats.length,
2389
+ codexBin: getCodexBin(),
2390
+ dataDir: process.env.PHANTOM_SERVE_DATA_DIR ?? null
2391
+ };
2392
+ }
2393
+ async listProjects() {
2394
+ return (await this.store.load()).projects;
2395
+ }
2396
+ async addProject(path) {
2397
+ if (!isAbsolute(path)) throw new Error("Project path must be absolute");
2398
+ const rootPath = await getGitRoot({ cwd: await realpath(path) });
2399
+ const timestamp = createTimestamp();
2400
+ let createdProject = null;
2401
+ await this.store.update((state) => {
2402
+ const existingProject = state.projects.find((project) => project.rootPath === rootPath);
2403
+ if (existingProject) {
2404
+ createdProject = touchProject(existingProject);
2405
+ return {
2406
+ ...state,
2407
+ projects: state.projects.map((project) => project.id === existingProject.id ? createdProject : project),
2408
+ selectedProjectId: existingProject.id
2409
+ };
2410
+ }
2411
+ createdProject = {
2412
+ id: createRecordId("proj"),
2413
+ name: basename(rootPath),
2414
+ rootPath,
2415
+ createdAt: timestamp,
2416
+ updatedAt: timestamp,
2417
+ lastOpenedAt: timestamp
2418
+ };
2419
+ return {
2420
+ ...state,
2421
+ projects: [...state.projects, createdProject],
2422
+ selectedProjectId: createdProject.id
2423
+ };
2424
+ });
2425
+ this.eventHub.emit("project.created", createdProject);
2426
+ return createdProject;
2427
+ }
2428
+ async removeProject(projectId) {
2429
+ await this.store.update((state) => {
2430
+ const removedChatIds = new Set(state.chats.filter((chat) => chat.projectId === projectId).map((chat) => chat.id));
2431
+ return {
2432
+ ...state,
2433
+ projects: state.projects.filter((project) => project.id !== projectId),
2434
+ chats: state.chats.filter((chat) => chat.projectId !== projectId),
2435
+ messages: state.messages.filter((message) => !removedChatIds.has(message.chatId)),
2436
+ selectedProjectId: state.selectedProjectId === projectId ? null : state.selectedProjectId,
2437
+ selectedChatId: removedChatIds.has(state.selectedChatId ?? "") ? null : state.selectedChatId
2438
+ };
2439
+ });
2440
+ this.eventHub.emit("project.removed", { projectId });
2441
+ }
2442
+ async listChats(projectId) {
2443
+ return (await this.store.load()).chats.filter((chat) => chat.projectId === projectId);
2444
+ }
2445
+ async listProjectWorktrees(projectId, options = {}) {
2446
+ const state = await this.store.load();
2447
+ if (options.sync === false) return projectWorktreesFromPersistedChats(state, projectId);
2448
+ const project = this.requireProject(state, projectId);
2449
+ let result;
2450
+ try {
2451
+ result = await listWorktrees(project.rootPath);
2452
+ } catch {
2453
+ return projectWorktreesFromPersistedChats(state, projectId);
2454
+ }
2455
+ if (!result.ok) return projectWorktreesFromPersistedChats(state, projectId);
2456
+ const { worktrees } = result.value;
2457
+ const timestamp = createTimestamp();
2458
+ const importedSessions = await listCodexSessionsForWorktrees({
2459
+ codexHome: this.codexHome,
2460
+ projectId,
2461
+ worktrees: worktrees.map((worktree) => ({
2462
+ branchName: worktree.branch,
2463
+ worktreeName: worktree.name,
2464
+ worktreePath: worktree.path
2465
+ }))
2466
+ });
2467
+ if (shouldSyncProjectWorktreeChats({
2468
+ importedSessions,
2469
+ projectId,
2470
+ state,
2471
+ worktrees
2472
+ })) await this.store.update((nextState) => {
2473
+ const existingChatsByPath = new Map(nextState.chats.filter((chat) => chat.projectId === projectId).map((chat) => [chat.worktreePath, chat]));
2474
+ const importedWorktreePaths = new Set(importedSessions.map((session) => session.chat.worktreePath));
2475
+ const chatsToAdd = worktrees.filter((worktree) => !existingChatsByPath.has(worktree.path) && !importedWorktreePaths.has(worktree.path)).map((worktree) => ({
2476
+ id: createRecordId("chat"),
2477
+ projectId,
2478
+ worktreeName: worktree.name,
2479
+ worktreePath: worktree.path,
2480
+ branchName: worktree.branch,
2481
+ codexThreadId: null,
2482
+ title: worktree.name,
2483
+ status: "idle",
2484
+ activeTurnId: null,
2485
+ createdAt: timestamp,
2486
+ updatedAt: timestamp
2487
+ }));
2488
+ const importableSessions = importedSessions.filter((session) => !nextState.chats.some((chat) => shouldPreferExistingChatOverImport(chat, session.chat)));
2489
+ const importableMessages = importableSessions.flatMap((session) => session.messages);
2490
+ if (chatsToAdd.length === 0 && importableSessions.length === 0) return nextState;
2491
+ const chatsToRemove = nextState.chats.filter((chat) => !shouldPreserveChatDuringImport(chat, projectId, importableSessions));
2492
+ const chatIdsToRemove = new Set(chatsToRemove.map((chat) => chat.id));
2493
+ const selectedReplacement = findImportedReplacement(nextState.selectedChatId, chatsToRemove, importableSessions);
2494
+ return {
2495
+ ...nextState,
2496
+ chats: [
2497
+ ...nextState.chats.filter((chat) => !chatIdsToRemove.has(chat.id)),
2498
+ ...importableSessions.map((session) => session.chat),
2499
+ ...chatsToAdd
2500
+ ],
2501
+ messages: [...nextState.messages.filter((message) => !chatIdsToRemove.has(message.chatId)), ...importableMessages],
2502
+ selectedChatId: nextState.selectedChatId && chatIdsToRemove.has(nextState.selectedChatId) ? selectedReplacement?.chat.id ?? nextState.selectedChatId : nextState.selectedChatId
2503
+ };
2504
+ });
2505
+ const syncedState = await this.store.load();
2506
+ const syncedChatsByPath = /* @__PURE__ */ new Map();
2507
+ for (const chat of syncedState.chats.filter((candidate) => candidate.projectId === projectId)) syncedChatsByPath.set(chat.worktreePath, [...syncedChatsByPath.get(chat.worktreePath) ?? [], chat]);
2508
+ return worktrees.map((worktree) => {
2509
+ const chat = latestChatForWorktree(syncedChatsByPath.get(worktree.path) ?? []);
2510
+ return {
2511
+ name: worktree.name,
2512
+ path: worktree.path,
2513
+ pathToDisplay: worktree.pathToDisplay,
2514
+ branch: worktree.branch,
2515
+ isClean: worktree.isClean,
2516
+ chatId: chat?.id ?? null,
2517
+ chatStatus: chat?.status ?? null,
2518
+ chatTitle: chat?.title ?? worktree.name
2519
+ };
2520
+ });
2521
+ }
2522
+ async createChat(projectId, input) {
2523
+ const state = await this.store.load();
2524
+ const project = this.requireProject(state, projectId);
2525
+ const createResult = await runCreateWorktree({
2526
+ gitRoot: project.rootPath,
2527
+ name: input.name,
2528
+ base: input.base
2529
+ });
2530
+ if (!createResult.ok) throw createResult.error;
2531
+ let importedSessions;
2532
+ try {
2533
+ importedSessions = await listCodexSessionsForWorktree({
2534
+ branchName: createResult.value.name,
2535
+ codexHome: this.codexHome,
2536
+ projectId,
2537
+ worktreeName: createResult.value.name,
2538
+ worktreePath: createResult.value.path
2539
+ });
2540
+ } catch (error) {
2541
+ try {
2542
+ await rollbackCreatedWorktree(project.rootPath, createResult.value.path, createResult.value.name);
2543
+ } catch (rollbackError) {
2544
+ throw new Error(`Failed to import Codex history: ${toErrorMessage(error)}. Rollback failed: ${toErrorMessage(rollbackError)}`);
2545
+ }
2546
+ throw error instanceof Error ? error : new Error(String(error));
2547
+ }
2548
+ const latestImportedSession = importedSessions[0];
2549
+ if (latestImportedSession) {
2550
+ let selectedImportedChat = latestImportedSession.chat;
2551
+ await this.store.update((nextState) => {
2552
+ selectedImportedChat = findExistingChatForImportedSession(nextState.chats, projectId, latestImportedSession.chat) ?? latestImportedSession.chat;
2553
+ return {
2554
+ ...mergeImportedSessionsForProject(nextState, projectId, importedSessions),
2555
+ selectedProjectId: projectId,
2556
+ selectedChatId: selectedImportedChat.id
2557
+ };
2558
+ });
2559
+ this.eventHub.emit("chat.created", selectedImportedChat, { chatId: selectedImportedChat.id });
2560
+ return selectedImportedChat;
2561
+ }
2562
+ let codexThreadId;
2563
+ try {
2564
+ codexThreadId = extractThreadId(await this.codex.startThread(createResult.value.path));
2565
+ } catch (error) {
2566
+ try {
2567
+ await rollbackCreatedWorktree(project.rootPath, createResult.value.path, createResult.value.name);
2568
+ } catch (rollbackError) {
2569
+ throw new Error(`Failed to start Codex thread: ${toErrorMessage(error)}. Rollback failed: ${toErrorMessage(rollbackError)}`);
2570
+ }
2571
+ throw error instanceof Error ? error : new Error(String(error));
2572
+ }
2573
+ this.loadedThreadIds.add(codexThreadId);
2574
+ const timestamp = createTimestamp();
2575
+ const chat = {
2576
+ id: createRecordId("chat"),
2577
+ projectId,
2578
+ worktreeName: createResult.value.name,
2579
+ worktreePath: createResult.value.path,
2580
+ branchName: createResult.value.name,
2581
+ codexThreadId,
2582
+ title: createResult.value.name,
2583
+ status: "idle",
2584
+ activeTurnId: null,
2585
+ createdAt: timestamp,
2586
+ updatedAt: timestamp
2587
+ };
2588
+ await this.store.update((nextState) => ({
2589
+ ...nextState,
2590
+ chats: [...nextState.chats, chat],
2591
+ selectedProjectId: projectId,
2592
+ selectedChatId: chat.id
2593
+ }));
2594
+ this.eventHub.emit("chat.created", chat, { chatId: chat.id });
2595
+ return chat;
2596
+ }
2597
+ async getChat(chatId) {
2598
+ const state = await this.store.load();
2599
+ return this.requireChat(state, chatId);
2600
+ }
2601
+ async getMessages(chatId) {
2602
+ const state = await this.store.load();
2603
+ this.requireChat(state, chatId);
2604
+ return state.messages.filter((message) => message.chatId === chatId);
2605
+ }
2606
+ async sendMessage(chatId, input) {
2607
+ return this.submitMessage(chatId, input, { requireActiveTurn: false });
2608
+ }
2609
+ async steerMessage(chatId, input) {
2610
+ return this.submitMessage(chatId, input, { requireActiveTurn: true });
2611
+ }
2612
+ async submitMessage(chatId, input, options) {
2613
+ const text = input.text.trim();
2614
+ if (!text) throw new Error("Message text cannot be empty");
2615
+ const state = await this.store.load();
2616
+ const chat = this.requireChat(state, chatId);
2617
+ if (chat.status === "waitingForApproval") throw new Error("Chat is waiting for approval");
2618
+ const previousMessageIds = new Set(state.messages.filter((message) => message.chatId === chatId).map((message) => message.id));
2619
+ const isSteeringActiveTurn = chat.status === "running" && Boolean(chat.activeTurnId);
2620
+ if (options.requireActiveTurn && !isSteeringActiveTurn) throw new Error("Chat does not have an active Codex turn");
2621
+ const turnOptions = await this.createCodexTurnOptions(input, chat);
2622
+ if (!isSteeringActiveTurn) {
2623
+ if (this.pendingChatTurns.has(chatId)) throw new Error("Chat already has an active Codex turn");
2624
+ this.pendingChatTurns.add(chatId);
2625
+ }
2626
+ const userMessage = createMessage(chat.id, "user", text);
2627
+ let nextStatus = null;
2628
+ let nextActiveTurnId;
2629
+ let pendingTurnThreadId = null;
2630
+ let userMessageStored = false;
2631
+ try {
2632
+ await this.store.update((nextState) => ({
2633
+ ...nextState,
2634
+ messages: [...nextState.messages, userMessage]
2635
+ }));
2636
+ userMessageStored = true;
2637
+ try {
2638
+ const threadId = await this.ensureThread(chat);
2639
+ const activeTurnId = chat.activeTurnId;
2640
+ if (chat.status === "running" && activeTurnId) if (turnOptions) await this.codex.steerTurn(threadId, activeTurnId, text, turnOptions);
2641
+ else await this.codex.steerTurn(threadId, activeTurnId, text);
2642
+ else {
2643
+ const existingPendingTurn = this.pendingTurnEvents.get(threadId);
2644
+ if (existingPendingTurn) {
2645
+ if (existingPendingTurn.discard) throw new Error("Chat is waiting for failed Codex turn cleanup");
2646
+ throw new Error("Chat already has an active Codex turn");
2647
+ }
2648
+ pendingTurnThreadId = threadId;
2649
+ this.pendingTurnEvents.set(threadId, {
2650
+ chatId,
2651
+ discard: false,
2652
+ events: []
2653
+ });
2654
+ const turnId = extractTurnId(turnOptions ? await this.codex.startTurn(threadId, text, chat.worktreePath, turnOptions) : await this.codex.startTurn(threadId, text, chat.worktreePath));
2655
+ if (turnId) {
2656
+ nextStatus = "running";
2657
+ nextActiveTurnId = turnId;
2658
+ }
2659
+ }
2660
+ } catch (error) {
2661
+ if (pendingTurnThreadId) this.discardPendingTurnEvents(pendingTurnThreadId);
2662
+ await this.store.update((nextState) => ({
2663
+ ...nextState,
2664
+ messages: userMessageStored ? nextState.messages.filter((message) => message.id !== userMessage.id && (isSteeringActiveTurn || message.chatId !== chatId || previousMessageIds.has(message.id))) : nextState.messages,
2665
+ chats: isSteeringActiveTurn ? nextState.chats : nextState.chats.map((candidate) => candidate.id === chatId ? {
2666
+ ...candidate,
2667
+ status: "failed",
2668
+ activeTurnId: chat.activeTurnId ?? null,
2669
+ updatedAt: createTimestamp()
2670
+ } : candidate)
2671
+ }));
2672
+ this.eventHub.emit("agent.error", { message: toErrorMessage(error) }, { chatId });
2673
+ throw error instanceof Error ? error : new Error(String(error));
2674
+ }
2675
+ await this.store.update((nextState) => ({
2676
+ ...nextState,
2677
+ chats: nextState.chats.map((candidate) => candidate.id === chatId && nextStatus ? {
2678
+ ...candidate,
2679
+ status: nextStatus,
2680
+ activeTurnId: nextActiveTurnId,
2681
+ updatedAt: createTimestamp()
2682
+ } : candidate)
2683
+ }));
2684
+ this.eventHub.emit("chat.message.created", userMessage, { chatId });
2685
+ if (pendingTurnThreadId) await this.flushPendingTurnEvents(pendingTurnThreadId);
2686
+ return await this.getChat(chatId);
2687
+ } finally {
2688
+ if (!isSteeringActiveTurn) this.pendingChatTurns.delete(chatId);
2689
+ }
2690
+ }
2691
+ async interruptChat(chatId) {
2692
+ const chat = await this.getChat(chatId);
2693
+ if (!chat.codexThreadId || !chat.activeTurnId) throw new Error("Chat does not have an active Codex turn");
2694
+ await this.codex.interruptTurn(chat.codexThreadId, chat.activeTurnId);
2695
+ }
2696
+ async answerApproval(chatId, requestId, input) {
2697
+ const chat = await this.getChat(chatId);
2698
+ const pendingApproval = this.approvalRequests.get(requestId);
2699
+ if (!pendingApproval) throw new Error(`Approval request '${requestId}' was not found`);
2700
+ if (pendingApproval.chatId !== chat.id) throw new Error(`Approval request '${requestId}' does not belong to chat '${chatId}'`);
2701
+ if (pendingApproval.responded) throw new Error(`Approval request '${requestId}' was already answered`);
2702
+ this.codex.respondToServerRequest(pendingApproval.serverRequestId, { decision: input.decision });
2703
+ this.approvalRequests.set(requestId, {
2704
+ ...pendingApproval,
2705
+ responded: true
2706
+ });
2707
+ this.eventHub.emit("agent.approval.answered", {
2708
+ requestId,
2709
+ decision: input.decision
2710
+ }, { chatId: chat.id });
2711
+ }
2712
+ declineApprovalRequest(requestId) {
2713
+ try {
2714
+ this.codex.respondToServerRequest(requestId, { decision: "decline" });
2715
+ } catch (error) {
2716
+ this.eventHub.emit("agent.error", {
2717
+ message: "Failed to decline unmapped Codex approval request",
2718
+ requestId,
2719
+ error: toErrorMessage(error)
2720
+ });
2721
+ }
2722
+ }
2723
+ async readAuth() {
2724
+ return this.codex.readAccount();
2725
+ }
2726
+ async listModels() {
2727
+ return normalizeModelRecords(await this.codex.listModels());
2728
+ }
2729
+ async listSkills(chatId) {
2730
+ const chat = await this.getChat(chatId);
2731
+ return normalizeSkillRecords(await this.codex.listSkills([chat.worktreePath]));
2732
+ }
2733
+ async searchFiles(chatId, query) {
2734
+ const trimmedQuery = query.trim();
2735
+ if (!trimmedQuery) return [];
2736
+ const chat = await this.getChat(chatId);
2737
+ return normalizeFileRecords(await this.codex.searchFiles(trimmedQuery, [chat.worktreePath]));
2738
+ }
2739
+ async createCodexTurnOptions(input, chat) {
2740
+ const files = await normalizeFileContextItems(input.files, chat.worktreePath);
2741
+ const skills = await this.normalizeSkillContextItems(input.skills, chat.worktreePath);
2742
+ if (!input.effort && !input.model && files.length === 0 && skills.length === 0) return;
2743
+ return {
2744
+ effort: input.effort,
2745
+ files: files.length > 0 ? files : void 0,
2746
+ model: input.model,
2747
+ skills: skills.length > 0 ? skills : void 0
2748
+ };
2749
+ }
2750
+ async normalizeSkillContextItems(items, worktreePath) {
2751
+ const normalized = normalizeTurnContextItems(items);
2752
+ if (normalized.length === 0) return [];
2753
+ const availableSkills = normalizeSkillRecords(await this.codex.listSkills([worktreePath]));
2754
+ const skillsByPath = new Map(availableSkills.filter((skill) => skill.enabled).map((skill) => [skill.path, skill]));
2755
+ return normalized.map((item) => {
2756
+ const skill = skillsByPath.get(item.path);
2757
+ if (!skill) throw new Error(`Skill context path is not available: ${item.path}`);
2758
+ return {
2759
+ name: skill.name,
2760
+ path: skill.path
2761
+ };
2762
+ });
2763
+ }
2764
+ async ensureThread(chat) {
2765
+ if (chat.codexThreadId) {
2766
+ if (!this.loadedThreadIds.has(chat.codexThreadId)) {
2767
+ await this.codex.resumeThread(chat.codexThreadId, chat.worktreePath);
2768
+ this.loadedThreadIds.add(chat.codexThreadId);
2769
+ }
2770
+ return chat.codexThreadId;
2771
+ }
2772
+ const threadId = extractThreadId(await this.codex.startThread(chat.worktreePath));
2773
+ this.loadedThreadIds.add(threadId);
2774
+ await this.store.update((state) => ({
2775
+ ...state,
2776
+ chats: state.chats.map((candidate) => candidate.id === chat.id ? {
2777
+ ...candidate,
2778
+ codexThreadId: threadId,
2779
+ updatedAt: createTimestamp()
2780
+ } : candidate)
2781
+ }));
2782
+ return threadId;
2783
+ }
2784
+ async handleCodexProcessExit(error) {
2785
+ this.loadedThreadIds.clear();
2786
+ this.approvalRequests.clear();
2787
+ this.pendingTurnEvents.clear();
2788
+ this.pendingChatTurns.clear();
2789
+ const affectedChatIds = [];
2790
+ await this.store.update((state) => ({
2791
+ ...state,
2792
+ chats: state.chats.map((chat) => {
2793
+ if (!(chat.status === "running" || chat.status === "waitingForApproval" || Boolean(chat.activeTurnId))) return chat;
2794
+ affectedChatIds.push(chat.id);
2795
+ return {
2796
+ ...chat,
2797
+ status: "failed",
2798
+ activeTurnId: null,
2799
+ updatedAt: createTimestamp()
2800
+ };
2801
+ })
2802
+ }));
2803
+ for (const chatId of affectedChatIds) this.eventHub.emit("agent.error", {
2804
+ message: "Codex App Server exited; chat turn state was reset",
2805
+ error: error.message
2806
+ }, { chatId });
2807
+ }
2808
+ async handleCodexNotification(message) {
2809
+ const threadId = extractThreadIdFromParams(message.params);
2810
+ if (threadId && this.bufferPendingTurnEvent(threadId, {
2811
+ kind: "notification",
2812
+ message
2813
+ })) return;
2814
+ await this.processCodexNotification(message);
2815
+ }
2816
+ async processCodexNotification(message) {
2817
+ const method = message.method ?? "unknown";
2818
+ const threadId = extractThreadIdFromParams(message.params);
2819
+ const chat = threadId ? await this.findChatByThreadId(threadId) : null;
2820
+ const eventType = mapCodexMethodToEvent(method);
2821
+ if (chat) {
2822
+ if (!await this.applyCodexStateChange(chat.id, method, message.params)) return;
2823
+ await this.addMessageFromCodexEvent(chat.id, method, message.params);
2824
+ this.eventHub.emit(eventType, message, { chatId: chat.id });
2825
+ } else {
2826
+ if (method === "serverRequest/resolved") return;
2827
+ this.eventHub.emit(eventType, message);
2828
+ }
2829
+ }
2830
+ async handleCodexServerRequest(message) {
2831
+ const threadId = extractThreadIdFromParams(message.params);
2832
+ if (threadId && this.bufferPendingTurnEvent(threadId, {
2833
+ kind: "serverRequest",
2834
+ message
2835
+ })) return;
2836
+ await this.processCodexServerRequest(message);
2837
+ }
2838
+ async processCodexServerRequest(message) {
2839
+ const threadId = extractThreadIdFromParams(message.params);
2840
+ const chat = threadId ? await this.findChatByThreadId(threadId) : null;
2841
+ const serverRequestId = getServerRequestId(message.id);
2842
+ if (!chat) {
2843
+ if (serverRequestId !== null) this.declineApprovalRequest(serverRequestId);
2844
+ this.eventHub.emit("agent.error", {
2845
+ message: "Codex approval request could not be mapped to a chat",
2846
+ method: message.method,
2847
+ requestId: serverRequestId
2848
+ });
2849
+ return;
2850
+ }
2851
+ if (serverRequestId === null) {
2852
+ this.eventHub.emit("agent.error", {
2853
+ message: "Codex approval request did not include a request id",
2854
+ method: message.method
2855
+ }, { chatId: chat.id });
2856
+ return;
2857
+ }
2858
+ if (!chat.activeTurnId) {
2859
+ this.declineApprovalRequest(serverRequestId);
2860
+ this.eventHub.emit("agent.error", {
2861
+ message: "Codex approval request did not belong to an active turn",
2862
+ method: message.method,
2863
+ requestId: serverRequestId
2864
+ }, { chatId: chat.id });
2865
+ return;
2866
+ }
2867
+ const requestTurnId = extractTurnIdFromParams(message.params);
2868
+ if (requestTurnId !== null && requestTurnId !== chat.activeTurnId) {
2869
+ this.declineApprovalRequest(serverRequestId);
2870
+ this.eventHub.emit("agent.error", {
2871
+ message: "Codex approval request belonged to a stale turn",
2872
+ method: message.method,
2873
+ requestId: serverRequestId,
2874
+ turnId: requestTurnId
2875
+ }, { chatId: chat.id });
2876
+ return;
2877
+ }
2878
+ const approvalRequestId = createRecordId("approval");
2879
+ this.approvalRequests.set(approvalRequestId, {
2880
+ chatId: chat.id,
2881
+ serverRequestId,
2882
+ responded: false
2883
+ });
2884
+ await this.updateChatStatus(chat.id, "waitingForApproval", chat.activeTurnId);
2885
+ this.eventHub.emit("agent.approval.requested", {
2886
+ requestId: approvalRequestId,
2887
+ method: message.method,
2888
+ params: message.params
2889
+ }, { chatId: chat.id });
2890
+ }
2891
+ async flushPendingTurnEvents(threadId) {
2892
+ const pendingTurnEvents = this.pendingTurnEvents.get(threadId);
2893
+ if (!pendingTurnEvents || pendingTurnEvents.discard) {
2894
+ this.pendingTurnEvents.delete(threadId);
2895
+ return;
2896
+ }
2897
+ while (!pendingTurnEvents.discard && pendingTurnEvents.events.length > 0) {
2898
+ const pendingEvent = pendingTurnEvents.events.shift();
2899
+ if (!pendingEvent) continue;
2900
+ if (pendingEvent.kind === "notification") await this.processCodexNotification(pendingEvent.message);
2901
+ else await this.processCodexServerRequest(pendingEvent.message);
2902
+ }
2903
+ if (this.pendingTurnEvents.get(threadId) === pendingTurnEvents) this.pendingTurnEvents.delete(threadId);
2904
+ }
2905
+ bufferPendingTurnEvent(threadId, event) {
2906
+ const pendingTurnEvents = this.pendingTurnEvents.get(threadId);
2907
+ if (!pendingTurnEvents) return false;
2908
+ if (pendingTurnEvents.discard) {
2909
+ if (event.kind === "serverRequest") {
2910
+ const serverRequestId = getServerRequestId(event.message.id);
2911
+ if (serverRequestId !== null) this.declineApprovalRequest(serverRequestId);
2912
+ }
2913
+ return true;
2914
+ }
2915
+ pendingTurnEvents.events.push(event);
2916
+ return true;
2917
+ }
2918
+ discardPendingTurnEvents(threadId) {
2919
+ const pendingTurnEvents = this.pendingTurnEvents.get(threadId);
2920
+ if (!pendingTurnEvents) return;
2921
+ for (const pendingEvent of pendingTurnEvents.events) {
2922
+ if (pendingEvent.kind !== "serverRequest") continue;
2923
+ const serverRequestId = getServerRequestId(pendingEvent.message.id);
2924
+ if (serverRequestId !== null) this.declineApprovalRequest(serverRequestId);
2925
+ }
2926
+ pendingTurnEvents.discard = true;
2927
+ pendingTurnEvents.events = [];
2928
+ setTimeout(() => {
2929
+ if (this.pendingTurnEvents.get(threadId) === pendingTurnEvents) this.pendingTurnEvents.delete(threadId);
2930
+ }, 3e4).unref?.();
2931
+ }
2932
+ async findChatByThreadId(threadId) {
2933
+ return (await this.store.load()).chats.find((chat) => chat.codexThreadId === threadId) ?? null;
2934
+ }
2935
+ async applyCodexStateChange(chatId, method, params) {
2936
+ if (method === "turn/started") {
2937
+ await this.updateChatStatus(chatId, "running", extractTurnId({ turn: getParamObject(params)?.turn }));
2938
+ return true;
2939
+ }
2940
+ if (method === "turn/completed") {
2941
+ const turn = getParamObject(params)?.turn;
2942
+ await this.updateChatStatus(chatId, turn?.status === "failed" ? "failed" : "idle", null);
2943
+ return true;
2944
+ }
2945
+ if (method === "serverRequest/resolved") {
2946
+ const serverRequestId = extractServerRequestIdFromParams(params);
2947
+ if (serverRequestId === null) return false;
2948
+ if (!this.deleteApprovalRequestByServerId(chatId, serverRequestId)) return false;
2949
+ await this.updateChatStatus(chatId, "running", void 0);
2950
+ }
2951
+ return true;
2952
+ }
2953
+ deleteApprovalRequestByServerId(chatId, serverRequestId) {
2954
+ let wasTracked = false;
2955
+ for (const [approvalRequestId, approvalRequest] of this.approvalRequests) if (approvalRequest.chatId === chatId && approvalRequest.serverRequestId === serverRequestId) {
2956
+ this.approvalRequests.delete(approvalRequestId);
2957
+ wasTracked = true;
2958
+ }
2959
+ return wasTracked;
2960
+ }
2961
+ async updateChatStatus(chatId, status, activeTurnId) {
2962
+ await this.store.update((state) => ({
2963
+ ...state,
2964
+ chats: state.chats.map((chat) => chat.id === chatId ? {
2965
+ ...chat,
2966
+ status,
2967
+ activeTurnId: activeTurnId === void 0 ? chat.activeTurnId : activeTurnId,
2968
+ updatedAt: createTimestamp()
2969
+ } : chat)
2970
+ }));
2971
+ }
2972
+ async addMessageFromCodexEvent(chatId, method, params) {
2973
+ const paramObject = getParamObject(params);
2974
+ if (!paramObject) return;
2975
+ if (method === "item/agentMessage/delta") {
2976
+ const itemId = typeof paramObject.itemId === "string" ? paramObject.itemId : void 0;
2977
+ const delta = typeof paramObject.delta === "string" ? paramObject.delta : "";
2978
+ if (!itemId || !delta) return;
2979
+ await this.store.update((state) => {
2980
+ const existingMessage = state.messages.find((message) => message.chatId === chatId && message.itemId === itemId);
2981
+ if (!existingMessage) return {
2982
+ ...state,
2983
+ messages: [...state.messages, createMessage(chatId, "assistant", delta, method, itemId)]
2984
+ };
2985
+ return {
2986
+ ...state,
2987
+ messages: state.messages.map((message) => message.id === existingMessage.id ? {
2988
+ ...message,
2989
+ text: `${message.text}${delta}`
2990
+ } : message)
2991
+ };
2992
+ });
2993
+ return;
2994
+ }
2995
+ if (method === "item/started" || method === "item/completed" || method === "turn/completed" || method === "error") {
2996
+ const role = method === "error" ? "error" : "event";
2997
+ await this.store.update((state) => ({
2998
+ ...state,
2999
+ messages: [...state.messages, createMessage(chatId, role, summarizeCodexEvent(method, params), method)]
3000
+ }));
3001
+ }
3002
+ }
3003
+ requireProject(state, projectId) {
3004
+ const project = state.projects.find((candidate) => candidate.id === projectId);
3005
+ if (!project) throw new Error(`Project '${projectId}' not found`);
3006
+ return project;
3007
+ }
3008
+ requireChat(state, chatId) {
3009
+ const chat = state.chats.find((candidate) => candidate.id === chatId);
3010
+ if (!chat) throw new Error(`Chat '${chatId}' not found`);
3011
+ return chat;
3012
+ }
3013
+ };
3014
+ function createMessage(chatId, role, text, eventType, itemId) {
3015
+ return {
3016
+ id: createRecordId("msg"),
3017
+ chatId,
3018
+ role,
3019
+ text,
3020
+ eventType,
3021
+ itemId,
3022
+ createdAt: createTimestamp()
3023
+ };
3024
+ }
3025
+ function latestChatForWorktree(chats) {
3026
+ const sortedChats = [...chats].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
3027
+ return sortedChats.find((chat) => chat.codexThreadId) ?? sortedChats[0] ?? null;
3028
+ }
3029
+ function projectWorktreesFromPersistedChats(state, projectId) {
3030
+ const chatsByPath = /* @__PURE__ */ new Map();
3031
+ for (const chat of state.chats.filter((chat) => chat.projectId === projectId)) chatsByPath.set(chat.worktreePath, [...chatsByPath.get(chat.worktreePath) ?? [], chat]);
3032
+ return Array.from(chatsByPath.values()).map((chats) => latestChatForWorktree(chats)).filter((chat) => Boolean(chat)).sort((left, right) => left.worktreeName.localeCompare(right.worktreeName)).map((chat) => ({
3033
+ name: chat.worktreeName,
3034
+ path: chat.worktreePath,
3035
+ pathToDisplay: chat.worktreePath,
3036
+ branch: chat.branchName,
3037
+ isClean: true,
3038
+ chatId: chat.id,
3039
+ chatStatus: chat.status,
3040
+ chatTitle: chat.title
3041
+ }));
3042
+ }
3043
+ function shouldSyncProjectWorktreeChats({ importedSessions, projectId, state, worktrees }) {
3044
+ const existingChatsByPath = new Map(state.chats.filter((chat) => chat.projectId === projectId).map((chat) => [chat.worktreePath, chat]));
3045
+ const importedWorktreePaths = new Set(importedSessions.map((session) => session.chat.worktreePath));
3046
+ return worktrees.some((worktree) => !existingChatsByPath.has(worktree.path) && !importedWorktreePaths.has(worktree.path)) || importedSessions.some((session) => !state.chats.some((chat) => shouldPreferExistingChatOverImport(chat, session.chat)));
3047
+ }
3048
+ function shouldPreferExistingChatOverImport(chat, importedChat) {
3049
+ if (chat.projectId !== importedChat.projectId) return false;
3050
+ if (chat.codexThreadId !== null && chat.codexThreadId === importedChat.codexThreadId) return true;
3051
+ if (chat.codexThreadId !== null) return false;
3052
+ return Boolean(chat.activeTurnId || chat.status === "running" || chat.status === "waitingForApproval") && chat.worktreePath === importedChat.worktreePath;
3053
+ }
3054
+ function findExistingChatForImportedSession(chats, projectId, importedChat) {
3055
+ return chats.find((chat) => chat.projectId === projectId && (chat.id === importedChat.id || shouldPreferExistingChatOverImport(chat, importedChat))) ?? null;
3056
+ }
3057
+ function mergeImportedSessionsForProject(state, projectId, importedSessions) {
3058
+ const importableSessions = importedSessions.filter((session) => !state.chats.some((chat) => shouldPreferExistingChatOverImport(chat, session.chat)));
3059
+ const chatsToRemove = state.chats.filter((chat) => !shouldPreserveChatDuringImport(chat, projectId, importableSessions));
3060
+ const chatIdsToRemove = new Set(chatsToRemove.map((chat) => chat.id));
3061
+ return {
3062
+ ...state,
3063
+ chats: [...state.chats.filter((chat) => !chatIdsToRemove.has(chat.id)), ...importableSessions.map((session) => session.chat)],
3064
+ messages: [...state.messages.filter((message) => !chatIdsToRemove.has(message.chatId)), ...importableSessions.flatMap((session) => session.messages)]
3065
+ };
3066
+ }
3067
+ function shouldPreserveChatDuringImport(chat, projectId, importedSessions) {
3068
+ if (chat.projectId !== projectId) return true;
3069
+ if (chat.activeTurnId || chat.status === "running" || chat.status === "waitingForApproval") return true;
3070
+ return !importedSessions.some((session) => {
3071
+ const importedChat = session.chat;
3072
+ return chat.id === importedChat.id || chat.codexThreadId !== null && chat.codexThreadId === importedChat.codexThreadId || chat.codexThreadId === null && chat.worktreePath === importedChat.worktreePath;
3073
+ });
3074
+ }
3075
+ function findImportedReplacement(selectedChatId, removedChats, importedSessions) {
3076
+ const removedChat = removedChats.find((chat) => chat.id === selectedChatId);
3077
+ if (!removedChat) return null;
3078
+ return importedSessions.find((session) => {
3079
+ const importedChat = session.chat;
3080
+ return removedChat.codexThreadId === importedChat.codexThreadId || removedChat.worktreePath === importedChat.worktreePath;
3081
+ }) ?? null;
3082
+ }
3083
+ function normalizeTurnContextItems(items) {
3084
+ return (items ?? []).map((item) => ({
3085
+ name: item.name.trim(),
3086
+ path: item.path.trim()
3087
+ })).filter((item) => item.name && item.path);
3088
+ }
3089
+ async function normalizeFileContextItems(items, worktreePath) {
3090
+ const normalized = normalizeTurnContextItems(items);
3091
+ if (normalized.length === 0) return [];
3092
+ const realWorktreePath = await realpath(worktreePath);
3093
+ return await Promise.all(normalized.map(async (item) => {
3094
+ const resolvedPath = resolve(item.path);
3095
+ if (!isPathInside(worktreePath, resolvedPath)) throw new Error(`File context path must be within the chat worktree: ${item.path}`);
3096
+ let realFilePath;
3097
+ try {
3098
+ realFilePath = await realpath(resolvedPath);
3099
+ } catch {
3100
+ throw new Error(`File context path is not an existing file: ${item.path}`);
3101
+ }
3102
+ if (!isPathInside(realWorktreePath, realFilePath)) throw new Error(`File context path must resolve within the chat worktree: ${item.path}`);
3103
+ if (!(await stat(realFilePath)).isFile()) throw new Error(`File context path is not a file: ${item.path}`);
3104
+ return {
3105
+ name: item.name,
3106
+ path: resolvedPath
3107
+ };
3108
+ }));
3109
+ }
3110
+ function isPathInside(rootPath, candidatePath) {
3111
+ const relativePath = relative(resolve(rootPath), resolve(candidatePath));
3112
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
3113
+ }
3114
+ function normalizeModelRecords(value) {
3115
+ return (getRecordArray(value, "data") ?? getRecordArray(value, "models") ?? []).map((model) => {
3116
+ const id = getRecordString(model, "id") ?? getRecordString(model, "model");
3117
+ if (!id) return null;
3118
+ return {
3119
+ id,
3120
+ model: getRecordString(model, "model") ?? id,
3121
+ displayName: getRecordString(model, "displayName") ?? id,
3122
+ description: getRecordString(model, "description") ?? "",
3123
+ defaultReasoningEffort: getRecordString(model, "defaultReasoningEffort") ?? null,
3124
+ inputModalities: getStringArray(model.inputModalities),
3125
+ isDefault: model.isDefault === true,
3126
+ supportedReasoningEfforts: getReasoningEfforts(model)
3127
+ };
3128
+ }).filter((model) => Boolean(model));
3129
+ }
3130
+ function normalizeSkillRecords(value) {
3131
+ return (getRecordArray(value, "data") ?? getRecordArray(value, "skills") ?? []).flatMap((entry) => {
3132
+ if (Array.isArray(entry.skills)) return entry.skills.filter(isRecord);
3133
+ return isRecord(entry) ? [entry] : [];
3134
+ }).map((skill) => {
3135
+ const name = getRecordString(skill, "name");
3136
+ const path = getRecordString(skill, "path");
3137
+ if (!name || !path) return null;
3138
+ const skillInterface = isRecord(skill.interface) ? skill.interface : null;
3139
+ const shortDescription = getRecordString(skillInterface, "shortDescription") ?? getRecordString(skill, "shortDescription") ?? null;
3140
+ return {
3141
+ name,
3142
+ path,
3143
+ displayName: getRecordString(skillInterface, "displayName") ?? name,
3144
+ description: getRecordString(skill, "description") ?? "",
3145
+ shortDescription,
3146
+ enabled: skill.enabled !== false
3147
+ };
3148
+ }).filter((skill) => Boolean(skill));
3149
+ }
3150
+ function normalizeFileRecords(value) {
3151
+ return (getRecordArray(value, "files") ?? []).map((file) => {
3152
+ const matchType = getRecordString(file, "match_type");
3153
+ if (matchType && matchType !== "file") return null;
3154
+ const root = getRecordString(file, "root");
3155
+ const relativePath = getRecordString(file, "path");
3156
+ if (!root || !relativePath) return null;
3157
+ return {
3158
+ name: getRecordString(file, "file_name") ?? relativePath.split("/").pop() ?? relativePath,
3159
+ path: join(root, relativePath),
3160
+ relativePath,
3161
+ root,
3162
+ score: typeof file.score === "number" && Number.isFinite(file.score) ? file.score : 0
3163
+ };
3164
+ }).filter((file) => Boolean(file));
3165
+ }
3166
+ function getReasoningEfforts(model) {
3167
+ const supported = model.supportedReasoningEfforts;
3168
+ if (!Array.isArray(supported)) return [];
3169
+ return supported.map((effort) => typeof effort === "string" ? effort : getRecordString(effort, "reasoningEffort")).filter((effort) => Boolean(effort));
3170
+ }
3171
+ function getRecordArray(value, key) {
3172
+ if (!isRecord(value)) return;
3173
+ const candidate = value[key];
3174
+ if (!Array.isArray(candidate)) return;
3175
+ return candidate.filter(isRecord);
3176
+ }
3177
+ function getRecordString(value, key) {
3178
+ if (!isRecord(value)) return;
3179
+ const candidate = value[key];
3180
+ return typeof candidate === "string" ? candidate : void 0;
3181
+ }
3182
+ function getStringArray(value) {
3183
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
3184
+ }
3185
+ function isRecord(value) {
3186
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
3187
+ }
3188
+ function toErrorMessage(error) {
3189
+ return error instanceof Error ? error.message : String(error);
3190
+ }
3191
+ async function rollbackCreatedWorktree(gitRoot, worktreePath, branchName) {
3192
+ const errors = [];
3193
+ try {
3194
+ await removeWorktree(gitRoot, worktreePath, true);
3195
+ } catch (error) {
3196
+ errors.push(`worktree remove failed: ${toErrorMessage(error)}`);
3197
+ }
3198
+ const branchResult = await deleteBranch(gitRoot, branchName);
3199
+ if (!branchResult.ok) errors.push(`branch delete failed: ${branchResult.error.message}`);
3200
+ if (errors.length > 0) throw new Error(errors.join("; "));
3201
+ }
3202
+ var services = null;
3203
+ function getServeServices() {
3204
+ services ??= new ServeServices();
3205
+ return services;
3206
+ }
3207
+ var Route$12 = createFileRoute("/api/projects")({ server: { handlers: {
3208
+ GET: async () => {
3209
+ try {
3210
+ return json({ projects: await getServeServices().listProjects() });
3211
+ } catch (error) {
3212
+ return handleApiError(error);
3213
+ }
3214
+ },
3215
+ POST: async ({ request }) => {
3216
+ try {
3217
+ const path = getString$1(await readJsonObject(request), "path");
3218
+ if (!path) throw new Error("Project path is required");
3219
+ return json({ project: await getServeServices().addProject(path) }, { status: 201 });
3220
+ } catch (error) {
3221
+ return handleApiError(error);
3222
+ }
3223
+ }
3224
+ } } });
3225
+ var Route$11 = createFileRoute("/api/models")({ server: { handlers: { GET: async () => {
3226
+ try {
3227
+ return json({ models: await getServeServices().listModels() });
3228
+ } catch (error) {
3229
+ return handleApiError(error);
3230
+ }
3231
+ } } } });
3232
+ var Route$10 = createFileRoute("/api/health")({ server: { handlers: { GET: async () => {
3233
+ try {
3234
+ return json(await getServeServices().getHealth());
3235
+ } catch (error) {
3236
+ return handleApiError(error);
3237
+ }
3238
+ } } } });
3239
+ var Route$9 = createFileRoute("/api/events")({ server: { handlers: { GET: async ({ request }) => {
3240
+ return createSseResponse(getServeServices().eventHub.subscribe((event) => event.scope === "global", parseLastEventId(request)));
3241
+ } } } });
3242
+ var Route$8 = createFileRoute("/api/auth")({ server: { handlers: { GET: async () => {
3243
+ try {
3244
+ return json({ auth: await getServeServices().readAuth() });
3245
+ } catch (error) {
3246
+ return handleApiError(error);
3247
+ }
3248
+ } } } });
3249
+ var Route$7 = createFileRoute("/api/projects/$projectId")({ server: { handlers: { DELETE: async ({ params }) => {
3250
+ try {
3251
+ if (!params.projectId) throw new Error("Project id is required");
3252
+ await getServeServices().removeProject(params.projectId);
3253
+ return json({});
3254
+ } catch (error) {
3255
+ return handleApiError(error);
3256
+ }
3257
+ } } } });
3258
+ var Route$6 = createFileRoute("/api/chats/$chatId")({ server: { handlers: { GET: async ({ request, params }) => {
3259
+ try {
3260
+ if (!params.chatId) throw new Error("Chat id is required");
3261
+ const services = getServeServices();
3262
+ const searchParams = new URL(request.url).searchParams;
3263
+ if (searchParams.get("context") === "skills") return json({ skills: await services.listSkills(params.chatId) });
3264
+ const fileQuery = searchParams.get("fileQuery");
3265
+ if (fileQuery !== null) return json({ files: await services.searchFiles(params.chatId, fileQuery) });
3266
+ return json({ chat: await services.getChat(params.chatId) });
3267
+ } catch (error) {
3268
+ return handleApiError(error);
3269
+ }
3270
+ } } } });
3271
+ var Route$5 = createFileRoute("/api/projects/$projectId/chats")({ server: { handlers: {
3272
+ GET: async ({ request, params }) => {
3273
+ try {
3274
+ if (!params.projectId) throw new Error("Project id is required");
3275
+ const services = getServeServices();
3276
+ const shouldSync = new URL(request.url).searchParams.get("sync") === "1";
3277
+ const worktrees = await services.listProjectWorktrees(params.projectId, { sync: shouldSync });
3278
+ return json({
3279
+ chats: await services.listChats(params.projectId),
3280
+ worktrees
3281
+ });
3282
+ } catch (error) {
3283
+ return handleApiError(error);
3284
+ }
3285
+ },
3286
+ POST: async ({ request, params }) => {
3287
+ try {
3288
+ if (!params.projectId) throw new Error("Project id is required");
3289
+ const body = await readJsonObject(request);
3290
+ return json({ chat: await getServeServices().createChat(params.projectId, {
3291
+ name: getString$1(body, "name"),
3292
+ base: getString$1(body, "base")
3293
+ }) }, { status: 201 });
3294
+ } catch (error) {
3295
+ return handleApiError(error);
3296
+ }
3297
+ }
3298
+ } } });
3299
+ var Route$4 = createFileRoute("/api/chats/$chatId/steer")({ server: { handlers: { POST: async ({ request, params }) => {
3300
+ try {
3301
+ if (!params.chatId) throw new Error("Chat id is required");
3302
+ const text = getString$1(await readJsonObject(request), "text");
3303
+ if (!text) throw new Error("Message text is required");
3304
+ return json({ chat: await getServeServices().steerMessage(params.chatId, { text }) });
3305
+ } catch (error) {
3306
+ return handleApiError(error);
3307
+ }
3308
+ } } } });
3309
+ var Route$3 = createFileRoute("/api/chats/$chatId/messages")({ server: { handlers: {
3310
+ GET: async ({ params }) => {
3311
+ try {
3312
+ if (!params.chatId) throw new Error("Chat id is required");
3313
+ return json({ messages: await getServeServices().getMessages(params.chatId) });
3314
+ } catch (error) {
3315
+ return handleApiError(error);
3316
+ }
3317
+ },
3318
+ POST: async ({ request, params }) => {
3319
+ try {
3320
+ if (!params.chatId) throw new Error("Chat id is required");
3321
+ const body = await readJsonObject(request);
3322
+ const text = getString$1(body, "text");
3323
+ if (!text) throw new Error("Message text is required");
3324
+ return json({ chat: await getServeServices().sendMessage(params.chatId, {
3325
+ effort: getString$1(body, "effort"),
3326
+ files: getContextItems(body, "files"),
3327
+ model: getString$1(body, "model"),
3328
+ skills: getContextItems(body, "skills"),
3329
+ text
3330
+ }) });
3331
+ } catch (error) {
3332
+ return handleApiError(error);
3333
+ }
3334
+ }
3335
+ } } });
3336
+ function getContextItems(body, key) {
3337
+ const value = body[key];
3338
+ if (!Array.isArray(value)) return;
3339
+ return value.map((item) => {
3340
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
3341
+ const record = item;
3342
+ return {
3343
+ name: typeof record.name === "string" ? record.name : "",
3344
+ path: typeof record.path === "string" ? record.path : ""
3345
+ };
3346
+ }).filter((item) => Boolean(item?.name && item.path));
3347
+ }
3348
+ var Route$2 = createFileRoute("/api/chats/$chatId/interrupt")({ server: { handlers: { POST: async ({ params }) => {
3349
+ try {
3350
+ if (!params.chatId) throw new Error("Chat id is required");
3351
+ await getServeServices().interruptChat(params.chatId);
3352
+ return json({});
3353
+ } catch (error) {
3354
+ return handleApiError(error);
3355
+ }
3356
+ } } } });
3357
+ var Route$1 = createFileRoute("/api/chats/$chatId/events")({ server: { handlers: { GET: async ({ request, params }) => {
3358
+ const chatId = params.chatId;
3359
+ return createSseResponse(getServeServices().eventHub.subscribe((event) => event.scope === "global" || Boolean(chatId) && event.chatId === chatId, parseLastEventId(request)));
3360
+ } } } });
3361
+ var allowedDecisions = new Set([
3362
+ "accept",
3363
+ "acceptForSession",
3364
+ "decline",
3365
+ "cancel"
3366
+ ]);
3367
+ var Route = createFileRoute("/api/chats/$chatId/approvals/$requestId")({ server: { handlers: { POST: async ({ request, params }) => {
3368
+ try {
3369
+ if (!params.chatId || !params.requestId) throw new Error("Chat id and request id are required");
3370
+ const decision = getString$1(await readJsonObject(request), "decision");
3371
+ if (!decision || !allowedDecisions.has(decision)) throw new Error("Approval decision is invalid");
3372
+ await getServeServices().answerApproval(params.chatId, params.requestId, { decision });
3373
+ return json({});
3374
+ } catch (error) {
3375
+ return handleApiError(error);
3376
+ }
3377
+ } } } });
3378
+ var IndexRoute = Route$13.update({
3379
+ id: "/",
3380
+ path: "/",
3381
+ getParentRoute: () => Route$14
3382
+ });
3383
+ var ApiProjectsRoute = Route$12.update({
3384
+ id: "/api/projects",
3385
+ path: "/api/projects",
3386
+ getParentRoute: () => Route$14
3387
+ });
3388
+ var ApiModelsRoute = Route$11.update({
3389
+ id: "/api/models",
3390
+ path: "/api/models",
3391
+ getParentRoute: () => Route$14
3392
+ });
3393
+ var ApiHealthRoute = Route$10.update({
3394
+ id: "/api/health",
3395
+ path: "/api/health",
3396
+ getParentRoute: () => Route$14
3397
+ });
3398
+ var ApiEventsRoute = Route$9.update({
3399
+ id: "/api/events",
3400
+ path: "/api/events",
3401
+ getParentRoute: () => Route$14
3402
+ });
3403
+ var ApiAuthRoute = Route$8.update({
3404
+ id: "/api/auth",
3405
+ path: "/api/auth",
3406
+ getParentRoute: () => Route$14
3407
+ });
3408
+ var ApiProjectsProjectIdRoute = Route$7.update({
3409
+ id: "/$projectId",
3410
+ path: "/$projectId",
3411
+ getParentRoute: () => ApiProjectsRoute
3412
+ });
3413
+ var ApiChatsChatIdRoute = Route$6.update({
3414
+ id: "/api/chats/$chatId",
3415
+ path: "/api/chats/$chatId",
3416
+ getParentRoute: () => Route$14
3417
+ });
3418
+ var ApiProjectsProjectIdChatsRoute = Route$5.update({
3419
+ id: "/chats",
3420
+ path: "/chats",
3421
+ getParentRoute: () => ApiProjectsProjectIdRoute
3422
+ });
3423
+ var ApiChatsChatIdSteerRoute = Route$4.update({
3424
+ id: "/steer",
3425
+ path: "/steer",
3426
+ getParentRoute: () => ApiChatsChatIdRoute
3427
+ });
3428
+ var ApiChatsChatIdMessagesRoute = Route$3.update({
3429
+ id: "/messages",
3430
+ path: "/messages",
3431
+ getParentRoute: () => ApiChatsChatIdRoute
3432
+ });
3433
+ var ApiChatsChatIdInterruptRoute = Route$2.update({
3434
+ id: "/interrupt",
3435
+ path: "/interrupt",
3436
+ getParentRoute: () => ApiChatsChatIdRoute
3437
+ });
3438
+ var ApiChatsChatIdEventsRoute = Route$1.update({
3439
+ id: "/events",
3440
+ path: "/events",
3441
+ getParentRoute: () => ApiChatsChatIdRoute
3442
+ });
3443
+ var ApiChatsChatIdApprovalsRequestIdRoute = Route.update({
3444
+ id: "/approvals/$requestId",
3445
+ path: "/approvals/$requestId",
3446
+ getParentRoute: () => ApiChatsChatIdRoute
3447
+ });
3448
+ var ApiProjectsProjectIdRouteChildren = { ApiProjectsProjectIdChatsRoute };
3449
+ var ApiProjectsRouteChildren = { ApiProjectsProjectIdRoute: ApiProjectsProjectIdRoute._addFileChildren(ApiProjectsProjectIdRouteChildren) };
3450
+ var ApiProjectsRouteWithChildren = ApiProjectsRoute._addFileChildren(ApiProjectsRouteChildren);
3451
+ var ApiChatsChatIdRouteChildren = {
3452
+ ApiChatsChatIdEventsRoute,
3453
+ ApiChatsChatIdInterruptRoute,
3454
+ ApiChatsChatIdMessagesRoute,
3455
+ ApiChatsChatIdSteerRoute,
3456
+ ApiChatsChatIdApprovalsRequestIdRoute
3457
+ };
3458
+ var rootRouteChildren = {
3459
+ IndexRoute,
3460
+ ApiAuthRoute,
3461
+ ApiEventsRoute,
3462
+ ApiHealthRoute,
3463
+ ApiModelsRoute,
3464
+ ApiProjectsRoute: ApiProjectsRouteWithChildren,
3465
+ ApiChatsChatIdRoute: ApiChatsChatIdRoute._addFileChildren(ApiChatsChatIdRouteChildren)
3466
+ };
3467
+ var routeTree = Route$14._addFileChildren(rootRouteChildren)._addFileTypes();
3468
+ function getRouter() {
3469
+ return createRouter({
3470
+ routeTree,
3471
+ scrollRestoration: true
3472
+ });
3473
+ }
3474
+ //#endregion
3475
+ export { getRouter };