@maintainer-pro/ai-cli 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,6 +2,43 @@
2
2
  import { execa as execa2 } from "execa";
3
3
 
4
4
  // src/prompt.ts
5
+ var PROMPT_SECTION = {
6
+ system: { begin: "<<<SYSTEM_BEGIN>>>", end: "<<<SYSTEM_END>>>" },
7
+ clientContext: {
8
+ begin: "<<<CLIENT_CONTEXT_BEGIN>>>",
9
+ end: "<<<CLIENT_CONTEXT_END>>>"
10
+ },
11
+ priorConversations: {
12
+ begin: "<<<PRIOR_CONVERSATIONS_BEGIN>>>",
13
+ end: "<<<PRIOR_CONVERSATIONS_END>>>"
14
+ },
15
+ parentChain: {
16
+ begin: "<<<PARENT_CHAIN_BEGIN>>>",
17
+ end: "<<<PARENT_CHAIN_END>>>"
18
+ },
19
+ history: {
20
+ begin: "<<<CONVERSATION_HISTORY_BEGIN>>>",
21
+ end: "<<<CONVERSATION_HISTORY_END>>>"
22
+ },
23
+ attachments: {
24
+ begin: "<<<ATTACHMENTS_BEGIN>>>",
25
+ end: "<<<ATTACHMENTS_END>>>"
26
+ },
27
+ currentRequest: {
28
+ begin: "<<<CURRENT_REQUEST_BEGIN>>>",
29
+ end: "<<<CURRENT_REQUEST_END>>>"
30
+ }
31
+ };
32
+ function wrapSection(tag, body, title) {
33
+ const trimmed = body.trim();
34
+ if (!trimmed) return "";
35
+ const header = title ? `${tag.begin} ${title}` : tag.begin;
36
+ return `${header}
37
+ ${trimmed}
38
+ ${tag.end}
39
+
40
+ `;
41
+ }
5
42
  function buildConversationPrompt(messages) {
6
43
  if (messages.length === 0) return "";
7
44
  const lines = messages.map(
@@ -43,50 +80,67 @@ function splitMessages(messages) {
43
80
  const history = latestUserIndex > 0 ? messages.slice(0, latestUserIndex) : [];
44
81
  return { request, history };
45
82
  }
46
- function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical) {
83
+ function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
47
84
  const { request, history } = splitMessages(messages);
48
- const historyBlock = history.length > 0 ? `Conversation so far:
49
- ${buildConversationPrompt(history)}
50
-
51
- ` : "";
52
- const contextBlock = formatClientContext(context);
53
- const contextSection = contextBlock ? `${contextBlock}
54
-
55
- ` : "";
56
- const systemSection = systemPrompt ? `${systemPrompt}
57
-
58
- ` : "";
59
- const priorSection = priorConversationsContext?.trim() ? `${priorConversationsContext.trim()}
60
-
61
- ` : "";
62
- const attachmentsSection = attachmentPaths && attachmentPaths.length > 0 ? `Screenshots / images attached by the user (open and inspect these image files):
63
- ${attachmentPaths.map((p) => `- ${p}`).join("\n")}
64
-
65
- ` : "";
85
+ const systemSection = wrapSection(
86
+ PROMPT_SECTION.system,
87
+ systemPrompt ?? "",
88
+ "instructions"
89
+ );
90
+ const contextSection = wrapSection(
91
+ PROMPT_SECTION.clientContext,
92
+ formatClientContext(context),
93
+ "live UI snapshot"
94
+ );
95
+ const priorSection = wrapSection(
96
+ PROMPT_SECTION.priorConversations,
97
+ priorConversationsContext ?? "",
98
+ "other chats (secondary)"
99
+ );
100
+ const parentSection = wrapSection(
101
+ PROMPT_SECTION.parentChain,
102
+ parentChainContext ?? "",
103
+ "reply-to parents \u2014 primary thread context"
104
+ );
105
+ const historySection = wrapSection(
106
+ PROMPT_SECTION.history,
107
+ history.length > 0 ? buildConversationPrompt(history) : "",
108
+ "earlier turns in this conversation"
109
+ );
110
+ const attachmentsSection = wrapSection(
111
+ PROMPT_SECTION.attachments,
112
+ attachmentPaths && attachmentPaths.length > 0 ? `Screenshots / images attached by the user (open and inspect these image files):
113
+ ${attachmentPaths.map((p) => `- ${p}`).join("\n")}` : "",
114
+ "user attachments"
115
+ );
116
+ const currentSection = wrapSection(
117
+ PROMPT_SECTION.currentRequest,
118
+ request || "(See the attached screenshot(s).)",
119
+ "answer this message"
120
+ );
66
121
  const closer = technical ? "Do the work now. Follow the requested output format exactly." : "Do the work now (or ask one short clarifying question if needed). Reply in English, non-technical and user-facing. If it's not resolving, offer a quick call.";
67
- return `${systemSection}${contextSection}${priorSection}${historyBlock}${attachmentsSection}Current user request:
68
- ${request || "(See the attached screenshot(s).)"}
69
-
70
- ${closer}`;
122
+ return `${systemSection}${contextSection}${priorSection}${parentSection}${historySection}${attachmentsSection}${currentSection}${closer}`;
71
123
  }
72
- function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical) {
124
+ function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
73
125
  return buildUserFacingPrompt(
74
126
  systemPrompt,
75
127
  messages,
76
128
  context,
77
129
  attachmentPaths,
78
130
  priorConversationsContext,
79
- technical
131
+ technical,
132
+ parentChainContext
80
133
  );
81
134
  }
82
- function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext, technical) {
135
+ function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
83
136
  return buildUserFacingPrompt(
84
137
  null,
85
138
  messages,
86
139
  context,
87
140
  attachmentPaths,
88
141
  priorConversationsContext,
89
- technical
142
+ technical,
143
+ parentChainContext
90
144
  );
91
145
  }
92
146
 
@@ -200,7 +254,8 @@ async function callClaudeCli(command, messages, context, options) {
200
254
  context,
201
255
  options.attachmentPaths,
202
256
  options.priorConversationsContext,
203
- options.technical
257
+ options.technical,
258
+ options.parentChainContext
204
259
  );
205
260
  const resolved = resolveCliCommand("claude", command);
206
261
  const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
@@ -240,7 +295,8 @@ async function callCursorCli(command, messages, context, options) {
240
295
  context,
241
296
  options.attachmentPaths,
242
297
  options.priorConversationsContext,
243
- options.technical
298
+ options.technical,
299
+ options.parentChainContext
244
300
  );
245
301
  const resolved = resolveCliCommand("cursor", command);
246
302
  const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
@@ -282,7 +338,8 @@ async function callAntigravityCli(command, messages, context, options) {
282
338
  context,
283
339
  options.attachmentPaths,
284
340
  options.priorConversationsContext,
285
- options.technical
341
+ options.technical,
342
+ options.parentChainContext
286
343
  );
287
344
  const prompt = `You are operating as a coding agent with full permission to read and edit files in this workspace. Do not introduce yourself. Do not ask what you are. Execute the user's latest request now (edit files and/or emit runtime tool JSON as instructed).
288
345
 
@@ -323,8 +380,8 @@ async function resolveCommandPath(command) {
323
380
  }
324
381
  const result = await execa2("which", [command], { reject: false });
325
382
  if (result.exitCode !== 0) return null;
326
- const path5 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
327
- return path5 || null;
383
+ const path6 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
384
+ return path6 || null;
328
385
  } catch {
329
386
  return null;
330
387
  }
@@ -410,6 +467,51 @@ async function callAi(messages, context, options) {
410
467
  return provider.call(messages, context, options);
411
468
  }
412
469
 
470
+ // src/parent-chain.ts
471
+ function collectParentChain(rows, startId) {
472
+ if (!startId) return [];
473
+ const byId = new Map(rows.map((row) => [row.id, row]));
474
+ const chain = [];
475
+ const seen = /* @__PURE__ */ new Set();
476
+ let current = byId.get(startId);
477
+ while (current && !seen.has(current.id)) {
478
+ seen.add(current.id);
479
+ const content = String(current.content ?? "").trim();
480
+ if (content && current.provider !== "working") {
481
+ const role = current.role === "assistant" || current.role === "system" ? current.role : "user";
482
+ chain.unshift({
483
+ id: current.id,
484
+ role,
485
+ content,
486
+ ...current.senderName?.trim() ? { senderName: current.senderName.trim() } : {}
487
+ });
488
+ }
489
+ const nextId = current.parentMessageId?.trim();
490
+ current = nextId ? byId.get(nextId) : void 0;
491
+ }
492
+ return chain;
493
+ }
494
+ function parentChainForTurn(rows, currentMessageId, fallbackParentId) {
495
+ if (!currentMessageId && !fallbackParentId) return [];
496
+ const byId = new Map(rows.map((row) => [row.id, row]));
497
+ const current = currentMessageId ? byId.get(currentMessageId) : void 0;
498
+ const replyToId = current?.parentMessageId && String(current.parentMessageId).trim() || fallbackParentId && String(fallbackParentId).trim() || null;
499
+ return collectParentChain(rows, replyToId);
500
+ }
501
+ function formatParentChainContext(chain) {
502
+ if (chain.length === 0) return "";
503
+ const lines = [
504
+ "context: this message has parents hierarchy linked with it",
505
+ "Prefer this chain over unrelated queued or later messages when interpreting the current request.",
506
+ ""
507
+ ];
508
+ chain.forEach((entry, index) => {
509
+ const who = entry.senderName || (entry.role === "assistant" ? "Assistant" : entry.role === "system" ? "System" : "Human");
510
+ lines.push(`${index + 1}. [${who}] ${entry.content}`);
511
+ });
512
+ return lines.join("\n");
513
+ }
514
+
413
515
  // src/system-prompt.ts
414
516
  function createDefaultSystemPrompt(input) {
415
517
  const files = input.relevantFilesHint ? `Key UI files (use internally only; never name them in your reply):
@@ -420,6 +522,14 @@ Only when the user wants to change live in-app data (not source code), return a
420
522
  ${input.runtimeToolsHint}
421
523
 
422
524
  Only use those runtime tools for live state. Never invent runtime tools.` : "";
525
+ const access = input.ignorePaths !== void 0 ? `
526
+
527
+ ## File access boundaries (mandatory)
528
+ - You may only read or edit files inside the current project workspace directory.
529
+ - Never read, write, move, or delete files outside that workspace.
530
+ - Never touch paths that match this ignore list (relative to the workspace):
531
+ ${input.ignorePaths.length ? input.ignorePaths.map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)"}
532
+ - If a request requires something outside the workspace or on the ignore list, refuse that part and explain you can only change allowed project files.` : "";
423
533
  return `You are a helpful product assistant for an app the user is looking at right now.
424
534
 
425
535
  ${input.productDescription}
@@ -440,6 +550,7 @@ Behind the scenes you can edit this repository and update live app data, but the
440
550
  ## When to edit the codebase
441
551
  Edit source files when the user asks to change labels, layout, styling, copy, or behavior in the app.
442
552
  ${files}
553
+ ${access}
443
554
 
444
555
  ${tools}
445
556
 
@@ -447,9 +558,191 @@ Do not refuse UI/label changes \u2014 implement them in source files.
447
558
  Answer the user's latest request directly \u2014 never reply with a generic greeting.`;
448
559
  }
449
560
 
561
+ // src/access-policy.ts
562
+ import path2 from "path";
563
+ var DEFAULT_AI_IGNORE_PATHS = [
564
+ ".env",
565
+ ".env.*",
566
+ "**/.env",
567
+ "**/.env.*"
568
+ ];
569
+ function parseIgnorePathsEnv(raw) {
570
+ if (!raw?.trim()) return [];
571
+ const trimmed = raw.trim();
572
+ try {
573
+ const parsed = JSON.parse(trimmed);
574
+ if (Array.isArray(parsed)) {
575
+ return normalizeIgnorePaths(parsed.map(String));
576
+ }
577
+ } catch {
578
+ }
579
+ return normalizeIgnorePaths(trimmed.split(/[\n,]+/));
580
+ }
581
+ function normalizeIgnorePaths(paths) {
582
+ const out = [];
583
+ const seen = /* @__PURE__ */ new Set();
584
+ for (const raw of paths) {
585
+ const p = raw.trim().replace(/\\/g, "/");
586
+ if (!p || p.startsWith("/") || p.includes("..")) continue;
587
+ if (seen.has(p)) continue;
588
+ seen.add(p);
589
+ out.push(p);
590
+ }
591
+ return out;
592
+ }
593
+ function resolveIgnorePaths(partnerPaths) {
594
+ return normalizeIgnorePaths([
595
+ ...DEFAULT_AI_IGNORE_PATHS,
596
+ ...partnerPaths
597
+ ]);
598
+ }
599
+ function isInsideWorkspace(workspaceDir, targetPath) {
600
+ const root = path2.resolve(workspaceDir);
601
+ const target = path2.resolve(targetPath);
602
+ const rel = path2.relative(root, target);
603
+ return rel === "" || !rel.startsWith("..") && !path2.isAbsolute(rel);
604
+ }
605
+ function globToRegExp(pattern) {
606
+ let p = pattern.replace(/\\/g, "/");
607
+ if (p.endsWith("/")) p = `${p}**`;
608
+ let i = 0;
609
+ let out = "^";
610
+ while (i < p.length) {
611
+ if (p.startsWith("**/", i)) {
612
+ out += "(?:.*/)?";
613
+ i += 3;
614
+ continue;
615
+ }
616
+ if (p[i] === "*" && p[i + 1] !== "*") {
617
+ out += "[^/]*";
618
+ i += 1;
619
+ continue;
620
+ }
621
+ if (p.startsWith("**", i)) {
622
+ out += ".*";
623
+ i += 2;
624
+ continue;
625
+ }
626
+ if (p[i] === "?") {
627
+ out += "[^/]";
628
+ i += 1;
629
+ continue;
630
+ }
631
+ const ch = p[i];
632
+ if (/[.+^${}()|[\]\\]/.test(ch)) out += `\\${ch}`;
633
+ else out += ch;
634
+ i += 1;
635
+ }
636
+ out += "$";
637
+ return new RegExp(out, "i");
638
+ }
639
+ function isIgnoredRelative(relativePath, patterns) {
640
+ const rel = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
641
+ if (!rel) return false;
642
+ return patterns.some((pattern) => {
643
+ const re = globToRegExp(pattern);
644
+ if (re.test(rel)) return true;
645
+ if (!pattern.includes("*") && !pattern.endsWith("/")) {
646
+ return rel === pattern || rel.startsWith(`${pattern}/`);
647
+ }
648
+ return false;
649
+ });
650
+ }
651
+ function isPathAllowed(workspaceDir, targetPath, ignorePaths) {
652
+ if (!isInsideWorkspace(workspaceDir, targetPath)) return false;
653
+ const rel = path2.relative(path2.resolve(workspaceDir), path2.resolve(targetPath));
654
+ return !isIgnoredRelative(rel, ignorePaths);
655
+ }
656
+ function formatAccessPolicyPromptSection(input) {
657
+ const ignores = input.ignorePaths.length ? input.ignorePaths.map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)";
658
+ return `## File access boundaries (mandatory)
659
+ - You may only read or edit files inside the current project workspace directory.
660
+ - Never read, write, move, or delete files outside that workspace.
661
+ - Never touch paths that match this ignore list (relative to the workspace):
662
+ ${ignores}
663
+ - If a request requires something outside the workspace or on the ignore list, refuse that part and explain you can only change allowed project files.`;
664
+ }
665
+ var ACCESS_IGNORE_BEGIN = "# maintainer-pro:access-begin";
666
+ var ACCESS_IGNORE_END = "# maintainer-pro:access-end";
667
+ function renderManagedIgnoreBlock(ignorePaths) {
668
+ const lines = [
669
+ ACCESS_IGNORE_BEGIN,
670
+ "# Managed by Maintainer Pro \u2014 do not edit this block by hand.",
671
+ ...ignorePaths,
672
+ ACCESS_IGNORE_END
673
+ ];
674
+ return `${lines.join("\n")}
675
+ `;
676
+ }
677
+ function upsertManagedIgnoreFile(existing, ignorePaths) {
678
+ const block = renderManagedIgnoreBlock(ignorePaths);
679
+ const begin = existing.indexOf(ACCESS_IGNORE_BEGIN);
680
+ const end = existing.indexOf(ACCESS_IGNORE_END);
681
+ if (begin >= 0 && end > begin) {
682
+ const afterEnd = end + ACCESS_IGNORE_END.length;
683
+ const before = existing.slice(0, begin).replace(/\s+$/, "");
684
+ let after = existing.slice(afterEnd).replace(/^\r?\n/, "");
685
+ const parts = [before, block.trimEnd(), after.trimStart()].filter(
686
+ (s) => s.length > 0
687
+ );
688
+ return `${parts.join("\n\n")}
689
+ `;
690
+ }
691
+ const trimmed = existing.replace(/\s+$/, "");
692
+ return trimmed ? `${trimmed}
693
+
694
+ ${block}` : block;
695
+ }
696
+
697
+ // src/http/handler.ts
698
+ import { randomUUID } from "crypto";
699
+
700
+ // src/log.ts
701
+ import pino from "pino";
702
+ function isDevMode() {
703
+ if (process.env.AI_DEV === "1" || process.env.AI_DEV === "true") return true;
704
+ const env = (process.env.NODE_ENV || process.env.AI_ENV || "").trim().toLowerCase();
705
+ return env === "development" || env === "dev" || env === "test";
706
+ }
707
+ function resolveLogLevel() {
708
+ const explicit = process.env.LOG_LEVEL?.trim() || process.env.AI_LOG_LEVEL?.trim();
709
+ if (explicit) return explicit;
710
+ return isDevMode() ? "debug" : "info";
711
+ }
712
+ function createLogger(name) {
713
+ const level = resolveLogLevel();
714
+ const pretty = process.env.LOG_PRETTY !== "0" && typeof process.stdout?.isTTY === "boolean" && process.stdout.isTTY;
715
+ if (pretty) {
716
+ return pino({
717
+ name,
718
+ level,
719
+ transport: {
720
+ target: "pino-pretty",
721
+ options: {
722
+ colorize: true,
723
+ translateTime: "HH:MM:ss",
724
+ ignore: "pid,hostname"
725
+ }
726
+ }
727
+ });
728
+ }
729
+ return pino({ name, level });
730
+ }
731
+ function createInfoLogger(name) {
732
+ const log = createLogger(name);
733
+ return (msg) => {
734
+ log.info(msg);
735
+ };
736
+ }
737
+ function previewText(text, max = 160) {
738
+ const t = String(text ?? "").replace(/\s+/g, " ").trim();
739
+ if (!t) return "";
740
+ return t.length <= max ? t : `${t.slice(0, max)}\u2026`;
741
+ }
742
+
450
743
  // src/http/attachments.ts
451
744
  import fs2 from "fs/promises";
452
- import path2 from "path";
745
+ import path3 from "path";
453
746
  var MAX_ATTACHMENTS = 5;
454
747
  var MAX_BYTES = 4 * 1024 * 1024;
455
748
  var ALLOWED = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]);
@@ -469,7 +762,7 @@ function extForMime(mime) {
469
762
  async function saveChatAttachments(attachments, workspaceDir) {
470
763
  if (!attachments?.length) return [];
471
764
  const selected = attachments.slice(0, MAX_ATTACHMENTS);
472
- const dir = path2.join(workspaceDir, ".maintainer-pro", "uploads");
765
+ const dir = path3.join(workspaceDir, ".maintainer-pro", "uploads");
473
766
  await fs2.mkdir(dir, { recursive: true });
474
767
  const paths = [];
475
768
  const stamp = Date.now();
@@ -488,7 +781,10 @@ async function saveChatAttachments(attachments, workspaceDir) {
488
781
  }
489
782
  const safeBase = (item.name || `screenshot-${i + 1}`).replace(/[^\w.\-]+/g, "_").slice(0, 64);
490
783
  const fileName = `${stamp}-${i + 1}-${safeBase}${extForMime(mime)}`;
491
- const filePath = path2.join(dir, fileName);
784
+ const filePath = path3.resolve(dir, fileName);
785
+ if (!isInsideWorkspace(workspaceDir, filePath)) {
786
+ throw new Error("Attachment path escaped the project workspace");
787
+ }
492
788
  await fs2.writeFile(filePath, buffer);
493
789
  paths.push(filePath);
494
790
  }
@@ -575,7 +871,7 @@ function createToolValidator(schemas) {
575
871
 
576
872
  // src/http/handler.ts
577
873
  var WORKING_PROVIDER = "working";
578
- async function setWorkingMessage(db, conversationId) {
874
+ async function setWorkingMessage(db, conversationId, parentMessageId) {
579
875
  await db.ensureConversation(conversationId);
580
876
  await db.clearWorkingMessages?.(conversationId);
581
877
  await db.saveMessage({
@@ -583,9 +879,26 @@ async function setWorkingMessage(db, conversationId) {
583
879
  role: "assistant",
584
880
  content: "",
585
881
  provider: WORKING_PROVIDER,
586
- senderType: "ai"
882
+ senderType: "ai",
883
+ parentMessageId: parentMessageId ?? void 0
587
884
  });
588
885
  }
886
+ function savedMessageFields(result) {
887
+ if (!result || typeof result !== "object") return {};
888
+ const row = result;
889
+ const next = row.nextQueued && typeof row.nextQueued === "object" && typeof row.nextQueued.id === "string" ? {
890
+ id: row.nextQueued.id,
891
+ content: String(
892
+ row.nextQueued.content ?? ""
893
+ )
894
+ } : null;
895
+ return {
896
+ id: typeof row.id === "string" ? row.id : void 0,
897
+ queueStatus: typeof row.queueStatus === "string" || row.queueStatus === null ? row.queueStatus : void 0,
898
+ queuePosition: typeof row.queuePosition === "number" || row.queuePosition === null ? row.queuePosition : void 0,
899
+ nextQueued: next
900
+ };
901
+ }
589
902
  var SHARED_CONVERSATION_ID = "shared";
590
903
  var conversationTurnSeq = /* @__PURE__ */ new Map();
591
904
  function beginConversationTurn(conversationId) {
@@ -620,6 +933,7 @@ async function resolveSharedConversationId(request, options, bodyId) {
620
933
  return SHARED_CONVERSATION_ID;
621
934
  }
622
935
  function createChatHandler(options) {
936
+ const logger = options.logger ?? createLogger("ai-cli:chat");
623
937
  const validate = options.tools ? createToolValidator(options.tools) : null;
624
938
  const workspaceDir = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
625
939
  const baseCallOptions = {
@@ -628,6 +942,7 @@ function createChatHandler(options) {
628
942
  providerPreference: options.providerPreference,
629
943
  providers: options.providers
630
944
  };
945
+ logger.debug({ workspaceDir }, "chat handler ready");
631
946
  return {
632
947
  async GET(request) {
633
948
  try {
@@ -643,6 +958,14 @@ function createChatHandler(options) {
643
958
  if (options.db?.listMessages) {
644
959
  messages = await options.db.listMessages(conversationId);
645
960
  }
961
+ logger.debug(
962
+ {
963
+ conversationId,
964
+ provider: provider.id,
965
+ messageCount: messages?.length ?? 0
966
+ },
967
+ "GET /api/chat"
968
+ );
646
969
  return Response.json({
647
970
  provider: provider.id,
648
971
  providerLabel: provider.label || providerLabel(provider.id),
@@ -650,6 +973,7 @@ function createChatHandler(options) {
650
973
  messages: messages ?? []
651
974
  });
652
975
  } catch (err) {
976
+ logger.error({ err }, "GET /api/chat failed");
653
977
  const message = err instanceof Error ? err.message : "No AI CLI provider available";
654
978
  return Response.json(
655
979
  { error: message, provider: null },
@@ -659,7 +983,9 @@ function createChatHandler(options) {
659
983
  },
660
984
  async POST(request) {
661
985
  let trackedConversationId;
986
+ let trackedParentUserMessageId;
662
987
  let trackedTurn = 0;
988
+ const startedAt = Date.now();
663
989
  try {
664
990
  const body = await request.json();
665
991
  const conversationId = await resolveSharedConversationId(
@@ -668,6 +994,19 @@ function createChatHandler(options) {
668
994
  body.conversationId
669
995
  );
670
996
  trackedConversationId = conversationId;
997
+ logger.debug(
998
+ {
999
+ conversationId,
1000
+ type: body.type ?? "turn",
1001
+ skipPersistUser: Boolean(body.skipPersistUser),
1002
+ userMessageId: body.userMessageId,
1003
+ messageCount: body.messages?.length ?? 0,
1004
+ attachmentCount: body.attachments?.length ?? 0,
1005
+ queueStatus: body.queueStatus,
1006
+ preview: previewText(body.userMessage || body.content)
1007
+ },
1008
+ "POST /api/chat"
1009
+ );
671
1010
  if (body.type === "working-clear") {
672
1011
  if (!conversationId) {
673
1012
  return Response.json(
@@ -692,11 +1031,54 @@ function createChatHandler(options) {
692
1031
  await options.db.ensureConversation(conversationId);
693
1032
  await options.db.saveMessage({
694
1033
  conversationId,
1034
+ id: body.messageId,
695
1035
  role: "assistant",
696
1036
  content,
697
1037
  provider: "developer",
698
1038
  senderType: "developer",
699
- senderName: body.senderName?.trim() || void 0
1039
+ senderName: body.senderName?.trim() || void 0,
1040
+ parentMessageId: body.parentMessageId
1041
+ });
1042
+ }
1043
+ return Response.json({ ok: true, conversationId });
1044
+ }
1045
+ if (body.type === "user") {
1046
+ const content = body.content?.trim();
1047
+ if (!conversationId || !content) {
1048
+ return Response.json(
1049
+ { error: "conversationId and content are required" },
1050
+ { status: 400 }
1051
+ );
1052
+ }
1053
+ if (options.db) {
1054
+ await options.db.ensureConversation(conversationId);
1055
+ let persistAttachmentPaths2 = [];
1056
+ if (options.db.uploadAttachments && body.attachments?.length) {
1057
+ const uploaded = await options.db.uploadAttachments(
1058
+ conversationId,
1059
+ body.attachments
1060
+ );
1061
+ persistAttachmentPaths2 = uploaded.refs;
1062
+ }
1063
+ const saved = await options.db.saveMessage({
1064
+ conversationId,
1065
+ id: body.messageId,
1066
+ role: "user",
1067
+ content,
1068
+ attachmentPaths: persistAttachmentPaths2.length > 0 ? persistAttachmentPaths2 : void 0,
1069
+ senderType: body.senderType === "client" ? "client" : void 0,
1070
+ senderName: body.senderName?.trim() || void 0,
1071
+ parentMessageId: body.parentMessageId,
1072
+ intent: "queue",
1073
+ queueStatus: body.queueStatus === "queued" || body.queueStatus === "working" ? body.queueStatus : void 0
1074
+ });
1075
+ const fields = savedMessageFields(saved);
1076
+ return Response.json({
1077
+ ok: true,
1078
+ conversationId,
1079
+ message: saved,
1080
+ queueStatus: fields.queueStatus ?? null,
1081
+ queuePosition: fields.queuePosition ?? null
700
1082
  });
701
1083
  }
702
1084
  return Response.json({ ok: true, conversationId });
@@ -727,27 +1109,65 @@ function createChatHandler(options) {
727
1109
  const turn = beginConversationTurn(conversationId ?? SHARED_CONVERSATION_ID);
728
1110
  trackedTurn = turn;
729
1111
  const signal = request.signal;
1112
+ let parentUserMessageId = typeof body.userMessageId === "string" && body.userMessageId ? body.userMessageId : void 0;
1113
+ trackedParentUserMessageId = parentUserMessageId;
730
1114
  if (options.db && conversationId) {
731
1115
  const latest = messages[messages.length - 1];
732
1116
  const persistContent = body.userMessage?.trim() || (latest?.role === "user" ? latest.content : "");
733
- if (persistContent) {
1117
+ if (persistContent && !body.skipPersistUser && !parentUserMessageId) {
734
1118
  await options.db.ensureConversation(conversationId);
735
- await options.db.saveMessage({
1119
+ const saved = await options.db.saveMessage({
736
1120
  conversationId,
737
1121
  role: "user",
738
1122
  content: persistContent,
739
1123
  attachmentPaths: persistAttachmentPaths.length > 0 ? persistAttachmentPaths : void 0,
740
1124
  senderType: body.senderType === "client" ? "client" : void 0,
741
- senderName: body.senderName?.trim() || void 0
1125
+ senderName: body.senderName?.trim() || void 0,
1126
+ intent: "run"
742
1127
  });
1128
+ parentUserMessageId = savedMessageFields(saved).id ?? parentUserMessageId;
1129
+ trackedParentUserMessageId = parentUserMessageId;
1130
+ }
1131
+ if (!body.skipPersistUser) {
1132
+ await setWorkingMessage(
1133
+ options.db,
1134
+ conversationId,
1135
+ parentUserMessageId
1136
+ );
743
1137
  }
744
- await setWorkingMessage(options.db, conversationId);
745
1138
  }
746
1139
  const latestUser = [...messages].reverse().find((m) => m.role === "user");
747
1140
  const priorConversationsContext = options.db ? await buildPriorConversationsContext(options.db, {
748
1141
  excludeConversationId: conversationId,
749
1142
  currentRequest: latestUser?.content ?? ""
750
1143
  }) : "";
1144
+ let parentChainContext = "";
1145
+ if (options.db?.listMessages && conversationId) {
1146
+ const stored = await options.db.listMessages(conversationId);
1147
+ const chain = parentChainForTurn(
1148
+ stored.map((row) => ({
1149
+ id: row.id,
1150
+ role: row.role,
1151
+ content: row.content,
1152
+ parentMessageId: row.parentMessageId,
1153
+ senderName: row.senderName,
1154
+ provider: row.provider
1155
+ })),
1156
+ parentUserMessageId,
1157
+ body.parentMessageId
1158
+ );
1159
+ parentChainContext = formatParentChainContext(chain);
1160
+ if (parentChainContext) {
1161
+ logger.debug(
1162
+ {
1163
+ conversationId,
1164
+ parentUserMessageId,
1165
+ chainLength: chain.length
1166
+ },
1167
+ "parent chain context"
1168
+ );
1169
+ }
1170
+ }
751
1171
  if (!isActiveConversationTurn(
752
1172
  conversationId ?? SHARED_CONVERSATION_ID,
753
1173
  turn
@@ -758,23 +1178,58 @@ function createChatHandler(options) {
758
1178
  });
759
1179
  }
760
1180
  if (signal.aborted) {
761
- if (options.db?.clearWorkingMessages && conversationId) {
762
- await options.db.clearWorkingMessages(conversationId);
1181
+ logger.debug({ conversationId, turn }, "aborted before AI");
1182
+ if (conversationId && options.db) {
1183
+ if (options.db.releaseWorkingTurn) {
1184
+ await options.db.releaseWorkingTurn(
1185
+ conversationId,
1186
+ parentUserMessageId
1187
+ );
1188
+ } else {
1189
+ await options.db.clearWorkingMessages?.(conversationId);
1190
+ }
763
1191
  }
764
1192
  return Response.json({
765
1193
  superseded: true,
766
1194
  conversationId: conversationId ?? null
767
1195
  });
768
1196
  }
1197
+ logger.debug(
1198
+ {
1199
+ conversationId,
1200
+ turn,
1201
+ parentUserMessageId,
1202
+ historyLength: messages.length,
1203
+ hasParentChain: Boolean(parentChainContext),
1204
+ preview: previewText(
1205
+ [...messages].reverse().find((m) => m.role === "user")?.content
1206
+ )
1207
+ },
1208
+ "calling AI provider"
1209
+ );
1210
+ const aiStarted = Date.now();
769
1211
  const aiResponse = await callAi(messages, context, {
770
1212
  ...baseCallOptions,
771
1213
  attachmentPaths,
772
- priorConversationsContext: priorConversationsContext || void 0
1214
+ priorConversationsContext: priorConversationsContext || void 0,
1215
+ parentChainContext: parentChainContext || void 0
773
1216
  });
1217
+ logger.debug(
1218
+ {
1219
+ conversationId,
1220
+ turn,
1221
+ provider: aiResponse.provider,
1222
+ toolCalls: aiResponse.toolCalls.length,
1223
+ ms: Date.now() - aiStarted,
1224
+ preview: previewText(aiResponse.text)
1225
+ },
1226
+ "AI provider returned"
1227
+ );
774
1228
  if (!isActiveConversationTurn(
775
1229
  conversationId ?? SHARED_CONVERSATION_ID,
776
1230
  turn
777
1231
  )) {
1232
+ logger.debug({ conversationId, turn }, "superseded after AI");
778
1233
  return Response.json({
779
1234
  superseded: true,
780
1235
  conversationId: conversationId ?? null
@@ -782,38 +1237,108 @@ function createChatHandler(options) {
782
1237
  }
783
1238
  const validatedToolCalls = validate ? aiResponse.toolCalls.filter((tc) => validate(tc).valid) : aiResponse.toolCalls;
784
1239
  if (options.onToolCalls && validatedToolCalls.length > 0) {
1240
+ logger.debug(
1241
+ { conversationId, count: validatedToolCalls.length },
1242
+ "applying tool calls"
1243
+ );
785
1244
  await options.onToolCalls(validatedToolCalls, context);
786
1245
  }
1246
+ let nextQueued = null;
1247
+ const assistantMessageId = typeof body.assistantMessageId === "string" && body.assistantMessageId ? body.assistantMessageId : randomUUID();
1248
+ let replyId = assistantMessageId;
787
1249
  if (options.db && conversationId) {
788
1250
  await options.db.ensureConversation(conversationId);
789
- await options.db.clearWorkingMessages?.(conversationId);
790
- await options.db.saveMessage({
1251
+ const savedReply = await options.db.saveMessage({
791
1252
  conversationId,
1253
+ id: assistantMessageId,
792
1254
  role: "assistant",
793
1255
  content: aiResponse.text,
794
1256
  provider: aiResponse.provider,
795
- senderType: "ai"
1257
+ senderType: "ai",
1258
+ parentMessageId: parentUserMessageId
796
1259
  });
1260
+ replyId = savedMessageFields(savedReply).id ?? assistantMessageId;
1261
+ if (options.db.releaseWorkingTurn) {
1262
+ let released = void 0;
1263
+ for (let attempt = 0; attempt < 3; attempt++) {
1264
+ try {
1265
+ released = await options.db.releaseWorkingTurn(
1266
+ conversationId,
1267
+ parentUserMessageId
1268
+ );
1269
+ break;
1270
+ } catch (err) {
1271
+ logger.warn(
1272
+ { err, conversationId, parentUserMessageId, attempt },
1273
+ "releaseWorkingTurn failed; retrying"
1274
+ );
1275
+ if (attempt === 2) throw err;
1276
+ await new Promise((r) => setTimeout(r, 250 * (attempt + 1)));
1277
+ }
1278
+ }
1279
+ nextQueued = released && typeof released === "object" && released.nextQueued && typeof released.nextQueued.id === "string" ? {
1280
+ id: released.nextQueued.id,
1281
+ content: String(released.nextQueued.content ?? "")
1282
+ } : null;
1283
+ logger.debug(
1284
+ { conversationId, parentUserMessageId, nextQueued: nextQueued?.id },
1285
+ "released working turn"
1286
+ );
1287
+ } else {
1288
+ await options.db.clearWorkingMessages?.(conversationId);
1289
+ nextQueued = savedMessageFields(savedReply).nextQueued ?? null;
1290
+ }
797
1291
  if (validatedToolCalls.length > 0) {
798
1292
  await options.db.saveToolEvents?.(
799
1293
  conversationId,
800
1294
  validatedToolCalls
801
1295
  );
802
1296
  }
1297
+ logger.debug(
1298
+ {
1299
+ conversationId,
1300
+ replyId,
1301
+ nextQueued: nextQueued?.id
1302
+ },
1303
+ "persisted AI reply"
1304
+ );
803
1305
  }
1306
+ logger.debug(
1307
+ { conversationId, turn, ms: Date.now() - startedAt },
1308
+ "POST /api/chat done"
1309
+ );
804
1310
  return Response.json({
805
1311
  text: aiResponse.text,
806
1312
  toolCalls: validatedToolCalls,
807
1313
  provider: aiResponse.provider,
808
1314
  providerLabel: providerLabel(aiResponse.provider),
809
- conversationId: conversationId ?? null
1315
+ conversationId: conversationId ?? null,
1316
+ messageId: replyId,
1317
+ nextQueued,
1318
+ parentMessageId: parentUserMessageId ?? null
810
1319
  });
811
1320
  } catch (err) {
812
1321
  console.error("Chat API error:", err);
813
- if (trackedConversationId && options.db?.clearWorkingMessages && isActiveConversationTurn(trackedConversationId, trackedTurn)) {
814
- await options.db.clearWorkingMessages(trackedConversationId).catch(
815
- () => void 0
816
- );
1322
+ logger.error(
1323
+ {
1324
+ err,
1325
+ conversationId: trackedConversationId,
1326
+ turn: trackedTurn,
1327
+ ms: Date.now() - startedAt
1328
+ },
1329
+ "POST /api/chat failed"
1330
+ );
1331
+ if (trackedConversationId && options.db && isActiveConversationTurn(trackedConversationId, trackedTurn)) {
1332
+ if (options.db.releaseWorkingTurn) {
1333
+ await options.db.releaseWorkingTurn(
1334
+ trackedConversationId,
1335
+ trackedParentUserMessageId
1336
+ ).catch(() => void 0);
1337
+ } else {
1338
+ await options.db.clearWorkingMessages?.(trackedConversationId).catch(
1339
+ () => void 0
1340
+ );
1341
+ }
817
1342
  }
818
1343
  return Response.json(
819
1344
  {
@@ -834,8 +1359,13 @@ function toNextRoute(handlers) {
834
1359
 
835
1360
  // src/http/local-store.ts
836
1361
  import fs3 from "fs/promises";
837
- import path3 from "path";
838
- import { randomUUID } from "crypto";
1362
+ import path4 from "path";
1363
+ import { randomUUID as randomUUID2 } from "crypto";
1364
+ function userHasAiReply(rows, workingId) {
1365
+ return rows.some(
1366
+ (m) => m.role === "assistant" && m.parentMessageId === workingId && m.provider !== "working" && Boolean(m.content?.trim())
1367
+ );
1368
+ }
839
1369
  async function readConversation(filePath) {
840
1370
  try {
841
1371
  const raw = await fs3.readFile(filePath, "utf8");
@@ -846,11 +1376,11 @@ async function readConversation(filePath) {
846
1376
  }
847
1377
  }
848
1378
  async function writeConversation(filePath, data) {
849
- await fs3.mkdir(path3.dirname(filePath), { recursive: true });
1379
+ await fs3.mkdir(path4.dirname(filePath), { recursive: true });
850
1380
  await fs3.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
851
1381
  }
852
1382
  function createLocalDirectoryStore(baseDir) {
853
- const fileFor = (id) => path3.join(baseDir, `${id}.json`);
1383
+ const fileFor = (id) => path4.join(baseDir, `${id}.json`);
854
1384
  return {
855
1385
  async ensureConversation(id) {
856
1386
  const existing = await readConversation(fileFor(id));
@@ -868,21 +1398,273 @@ function createLocalDirectoryStore(baseDir) {
868
1398
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
869
1399
  messages: []
870
1400
  };
1401
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1402
+ const isClientUser = input.role === "user" && input.senderType !== "developer" && input.provider !== "developer";
1403
+ const isWorking = input.provider === "working";
1404
+ const isAi = input.role === "assistant" && input.provider !== "working" && input.senderType !== "developer" && input.provider !== "developer";
1405
+ const busy = existing.messages.some((m) => m.queueStatus === "working");
1406
+ const makeWorking = (parentMessageId) => ({
1407
+ id: randomUUID2(),
1408
+ conversationId: input.conversationId,
1409
+ role: "assistant",
1410
+ content: "",
1411
+ provider: "working",
1412
+ createdAt: now,
1413
+ senderType: "ai",
1414
+ parentMessageId
1415
+ });
1416
+ const promoteNext = () => {
1417
+ const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
1418
+ (a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
1419
+ );
1420
+ const next = waiting[0];
1421
+ if (!next) return null;
1422
+ next.queueStatus = "working";
1423
+ next.queuePosition = null;
1424
+ existing.messages.push(makeWorking(next.id));
1425
+ return next;
1426
+ };
1427
+ if (isClientUser) {
1428
+ if (input.intent === "run") {
1429
+ for (const row of existing.messages) {
1430
+ if (row.queueStatus === "working") {
1431
+ row.queueStatus = null;
1432
+ row.queuePosition = null;
1433
+ }
1434
+ }
1435
+ existing.messages = existing.messages.filter(
1436
+ (m) => m.provider !== "working"
1437
+ );
1438
+ }
1439
+ const queueStatus = input.intent === "run" ? "working" : busy ? "queued" : "working";
1440
+ const existingById2 = input.id ? existing.messages.find((m) => m.id === input.id) : void 0;
1441
+ if (existingById2) {
1442
+ return { ...existingById2, nextQueued: null };
1443
+ }
1444
+ const message2 = {
1445
+ id: input.id ?? randomUUID2(),
1446
+ conversationId: input.conversationId,
1447
+ role: input.role,
1448
+ content: input.content,
1449
+ provider: input.provider ?? null,
1450
+ createdAt: now,
1451
+ attachmentPaths: input.attachmentPaths,
1452
+ senderType: input.senderType ?? null,
1453
+ senderName: input.senderName ?? null,
1454
+ parentMessageId: input.parentMessageId ?? null,
1455
+ queueStatus,
1456
+ queuePosition: null
1457
+ };
1458
+ existing.messages.push(message2);
1459
+ existing.updatedAt = now;
1460
+ await writeConversation(filePath, existing);
1461
+ return { ...message2, nextQueued: null };
1462
+ }
1463
+ if (isWorking) {
1464
+ existing.messages = existing.messages.filter(
1465
+ (m) => m.provider !== "working"
1466
+ );
1467
+ if (input.parentMessageId) {
1468
+ const parent = existing.messages.find(
1469
+ (m) => m.id === input.parentMessageId
1470
+ );
1471
+ if (parent && parent.queueStatus !== "working") {
1472
+ parent.queueStatus = "working";
1473
+ parent.queuePosition = null;
1474
+ }
1475
+ }
1476
+ const message2 = {
1477
+ id: input.id ?? randomUUID2(),
1478
+ conversationId: input.conversationId,
1479
+ role: "assistant",
1480
+ content: "",
1481
+ provider: "working",
1482
+ createdAt: now,
1483
+ senderType: "ai",
1484
+ parentMessageId: input.parentMessageId ?? null
1485
+ };
1486
+ existing.messages.push(message2);
1487
+ existing.updatedAt = now;
1488
+ await writeConversation(filePath, existing);
1489
+ return { ...message2, nextQueued: null };
1490
+ }
1491
+ const existingById = input.id ? existing.messages.find((m) => m.id === input.id) : void 0;
1492
+ if (existingById) {
1493
+ return { ...existingById, nextQueued: null };
1494
+ }
871
1495
  const message = {
872
- id: randomUUID(),
1496
+ id: input.id ?? randomUUID2(),
873
1497
  conversationId: input.conversationId,
874
1498
  role: input.role,
875
1499
  content: input.content,
876
1500
  provider: input.provider ?? null,
877
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1501
+ createdAt: now,
878
1502
  attachmentPaths: input.attachmentPaths,
879
1503
  senderType: input.senderType ?? null,
880
- senderName: input.senderName ?? null
1504
+ senderName: input.senderName ?? null,
1505
+ parentMessageId: input.parentMessageId ?? null
881
1506
  };
882
1507
  existing.messages.push(message);
883
- existing.updatedAt = message.createdAt;
1508
+ let nextQueued = null;
1509
+ if (isAi) {
1510
+ existing.messages = existing.messages.filter(
1511
+ (m) => m.provider !== "working"
1512
+ );
1513
+ if (input.parentMessageId) {
1514
+ const parent = existing.messages.find(
1515
+ (m) => m.id === input.parentMessageId
1516
+ );
1517
+ if (parent) {
1518
+ parent.queueStatus = null;
1519
+ parent.queuePosition = null;
1520
+ }
1521
+ } else {
1522
+ for (const row of existing.messages) {
1523
+ if (row.queueStatus === "working") {
1524
+ row.queueStatus = null;
1525
+ row.queuePosition = null;
1526
+ }
1527
+ }
1528
+ }
1529
+ nextQueued = promoteNext();
1530
+ }
1531
+ existing.updatedAt = now;
884
1532
  await writeConversation(filePath, existing);
885
- return message;
1533
+ return {
1534
+ ...message,
1535
+ nextQueued: nextQueued ? { id: nextQueued.id, content: nextQueued.content } : null
1536
+ };
1537
+ },
1538
+ async recoverQueue(conversationId) {
1539
+ const filePath = fileFor(conversationId);
1540
+ const existing = await readConversation(filePath);
1541
+ if (!existing) {
1542
+ return {
1543
+ action: "noop",
1544
+ workingId: null,
1545
+ promotedId: null,
1546
+ nextQueued: null
1547
+ };
1548
+ }
1549
+ const working = existing.messages.find(
1550
+ (m) => m.role === "user" && m.queueStatus === "working"
1551
+ );
1552
+ if (working) {
1553
+ if (userHasAiReply(existing.messages, working.id)) {
1554
+ existing.messages = existing.messages.filter(
1555
+ (m) => m.provider !== "working"
1556
+ );
1557
+ working.queueStatus = null;
1558
+ working.queuePosition = null;
1559
+ const waiting2 = existing.messages.filter((m) => m.queueStatus === "queued").sort(
1560
+ (a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
1561
+ );
1562
+ const next2 = waiting2[0] ?? null;
1563
+ if (next2) {
1564
+ next2.queueStatus = "working";
1565
+ next2.queuePosition = null;
1566
+ existing.messages.push({
1567
+ id: randomUUID2(),
1568
+ conversationId,
1569
+ role: "assistant",
1570
+ content: "",
1571
+ provider: "working",
1572
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1573
+ senderType: "ai",
1574
+ parentMessageId: next2.id
1575
+ });
1576
+ }
1577
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1578
+ await writeConversation(filePath, existing);
1579
+ return {
1580
+ action: "completed_and_promoted",
1581
+ workingId: working.id,
1582
+ promotedId: next2?.id ?? null,
1583
+ nextQueued: next2 ? { id: next2.id, content: next2.content } : null
1584
+ };
1585
+ }
1586
+ return {
1587
+ action: "redispatched_working",
1588
+ workingId: working.id,
1589
+ promotedId: null,
1590
+ nextQueued: null
1591
+ };
1592
+ }
1593
+ const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
1594
+ (a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
1595
+ );
1596
+ const next = waiting[0] ?? null;
1597
+ if (!next) {
1598
+ return {
1599
+ action: "noop",
1600
+ workingId: null,
1601
+ promotedId: null,
1602
+ nextQueued: null
1603
+ };
1604
+ }
1605
+ next.queueStatus = "working";
1606
+ next.queuePosition = null;
1607
+ existing.messages.push({
1608
+ id: randomUUID2(),
1609
+ conversationId,
1610
+ role: "assistant",
1611
+ content: "",
1612
+ provider: "working",
1613
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1614
+ senderType: "ai",
1615
+ parentMessageId: next.id
1616
+ });
1617
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1618
+ await writeConversation(filePath, existing);
1619
+ return {
1620
+ action: "promoted_queued",
1621
+ workingId: null,
1622
+ promotedId: next.id,
1623
+ nextQueued: { id: next.id, content: next.content }
1624
+ };
1625
+ },
1626
+ async releaseWorkingTurn(conversationId, parentMessageId) {
1627
+ const filePath = fileFor(conversationId);
1628
+ const existing = await readConversation(filePath);
1629
+ if (!existing) return { nextQueued: null };
1630
+ existing.messages = existing.messages.filter(
1631
+ (m) => m.provider !== "working"
1632
+ );
1633
+ if (parentMessageId) {
1634
+ const parent = existing.messages.find((m) => m.id === parentMessageId);
1635
+ if (parent) {
1636
+ parent.queueStatus = null;
1637
+ parent.queuePosition = null;
1638
+ }
1639
+ } else {
1640
+ for (const row of existing.messages) {
1641
+ if (row.queueStatus === "working") {
1642
+ row.queueStatus = null;
1643
+ row.queuePosition = null;
1644
+ }
1645
+ }
1646
+ }
1647
+ const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
1648
+ (a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
1649
+ );
1650
+ const next = waiting[0] ?? null;
1651
+ if (next) {
1652
+ next.queueStatus = "working";
1653
+ next.queuePosition = null;
1654
+ existing.messages.push({
1655
+ id: randomUUID2(),
1656
+ conversationId,
1657
+ role: "assistant",
1658
+ content: "",
1659
+ provider: "working",
1660
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1661
+ senderType: "ai",
1662
+ parentMessageId: next.id
1663
+ });
1664
+ }
1665
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1666
+ await writeConversation(filePath, existing);
1667
+ return next ? { nextQueued: { id: next.id, content: next.content } } : { nextQueued: null };
886
1668
  },
887
1669
  async clearWorkingMessages(conversationId) {
888
1670
  const filePath = fileFor(conversationId);
@@ -904,7 +1686,7 @@ function createLocalDirectoryStore(baseDir) {
904
1686
  const conversations = [];
905
1687
  for (const entry of entries) {
906
1688
  if (!entry.endsWith(".json")) continue;
907
- const filePath = path3.join(baseDir, entry);
1689
+ const filePath = path4.join(baseDir, entry);
908
1690
  const existing = await readConversation(filePath);
909
1691
  if (!existing?.id) continue;
910
1692
  conversations.push({
@@ -921,7 +1703,7 @@ function createLocalDirectoryStore(baseDir) {
921
1703
  if (events.length === 0) return;
922
1704
  const existing = await readConversation(fileFor(conversationId));
923
1705
  if (!existing) return;
924
- const toolFile = path3.join(baseDir, `${conversationId}.tools.jsonl`);
1706
+ const toolFile = path4.join(baseDir, `${conversationId}.tools.jsonl`);
925
1707
  const lines = events.map(
926
1708
  (e) => JSON.stringify({
927
1709
  conversationId,
@@ -938,7 +1720,353 @@ function createLocalDirectoryStore(baseDir) {
938
1720
  // src/http/maintainer-pro-store.ts
939
1721
  import fs4 from "fs/promises";
940
1722
  import os from "os";
941
- import path4 from "path";
1723
+ import path5 from "path";
1724
+
1725
+ // src/http/synced-store.ts
1726
+ function userHasAiReply2(rows, workingId) {
1727
+ return rows.some(
1728
+ (row) => row.role === "assistant" && row.parentMessageId === workingId && row.provider !== "working" && row.senderType !== "developer" && Boolean(row.content?.trim())
1729
+ );
1730
+ }
1731
+ function nowIso() {
1732
+ return (/* @__PURE__ */ new Date()).toISOString();
1733
+ }
1734
+ function toWsUrl(baseUrl, apiKey) {
1735
+ const u = new URL(`${baseUrl.replace(/\/$/, "")}/api/v1/ws`);
1736
+ u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
1737
+ u.searchParams.set("apiKey", apiKey);
1738
+ return u.toString();
1739
+ }
1740
+ function asCachedMessage(raw) {
1741
+ const id = typeof raw.id === "string" ? raw.id : "";
1742
+ const role = typeof raw.role === "string" ? raw.role : "";
1743
+ if (!id || !role) return null;
1744
+ return {
1745
+ id,
1746
+ role,
1747
+ content: typeof raw.content === "string" ? raw.content : "",
1748
+ provider: typeof raw.provider === "string" ? raw.provider : null,
1749
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : void 0,
1750
+ attachmentPaths: Array.isArray(raw.attachmentPaths) ? raw.attachmentPaths.filter((p) => typeof p === "string") : void 0,
1751
+ senderType: raw.senderType === "client" || raw.senderType === "developer" || raw.senderType === "ai" ? raw.senderType : null,
1752
+ senderName: typeof raw.senderName === "string" ? raw.senderName : null,
1753
+ parentMessageId: typeof raw.parentMessageId === "string" ? raw.parentMessageId : null,
1754
+ queueStatus: raw.queueStatus === "working" || raw.queueStatus === "queued" ? raw.queueStatus : raw.queueStatus === null ? null : void 0,
1755
+ queuePosition: typeof raw.queuePosition === "number" ? raw.queuePosition : null
1756
+ };
1757
+ }
1758
+ function cloneMessages(messages) {
1759
+ return messages.map((m) => ({ ...m }));
1760
+ }
1761
+ function createSyncedChatStore(options) {
1762
+ const { remote, maintainerProUrl, apiKey, onWorkingTurn } = options;
1763
+ const logger = options.logger ?? createLogger("ai-cli");
1764
+ const info = (msg) => {
1765
+ if (options.log) options.log(msg);
1766
+ else logger.info(msg);
1767
+ };
1768
+ const conversations = /* @__PURE__ */ new Map();
1769
+ const inflightRefresh = /* @__PURE__ */ new Map();
1770
+ let stopped = false;
1771
+ let socket = null;
1772
+ let reconnectAttempt = 0;
1773
+ let reconnectTimer = null;
1774
+ let pingTimer = null;
1775
+ const stats = () => {
1776
+ let messages = 0;
1777
+ for (const conv of conversations.values()) messages += conv.messages.length;
1778
+ return { conversations: conversations.size, messages };
1779
+ };
1780
+ const putConversation = (id, messages, updatedAt = nowIso()) => {
1781
+ conversations.set(id, { id, updatedAt, messages: cloneMessages(messages) });
1782
+ };
1783
+ const refreshConversation = (id) => {
1784
+ const existing = inflightRefresh.get(id);
1785
+ if (existing) return existing;
1786
+ const pending = (async () => {
1787
+ const messages = remote.listMessages ? await remote.listMessages(id) : [];
1788
+ putConversation(id, messages);
1789
+ return cloneMessages(messages);
1790
+ })().finally(() => {
1791
+ inflightRefresh.delete(id);
1792
+ });
1793
+ inflightRefresh.set(id, pending);
1794
+ return pending;
1795
+ };
1796
+ const loadSnapshot = async () => {
1797
+ if (remote.listSnapshot) {
1798
+ try {
1799
+ return await remote.listSnapshot();
1800
+ } catch {
1801
+ }
1802
+ }
1803
+ if (!remote.listConversations || !remote.listMessages) return [];
1804
+ const summaries = await remote.listConversations();
1805
+ const loaded = [];
1806
+ for (const row of summaries) {
1807
+ loaded.push({
1808
+ id: row.id,
1809
+ updatedAt: row.updatedAt,
1810
+ messages: await remote.listMessages(row.id)
1811
+ });
1812
+ }
1813
+ return loaded;
1814
+ };
1815
+ const hydrate = async () => {
1816
+ const snapshot = await loadSnapshot();
1817
+ conversations.clear();
1818
+ for (const row of snapshot) {
1819
+ putConversation(row.id, row.messages, row.updatedAt);
1820
+ }
1821
+ const next = stats();
1822
+ info(
1823
+ `cache hydrated (${next.conversations} conversations, ${next.messages} messages)`
1824
+ );
1825
+ for (const conv of conversations.values()) {
1826
+ const working = conv.messages.find(
1827
+ (row) => row.role === "user" && row.queueStatus === "working" && row.senderType !== "developer"
1828
+ );
1829
+ if (!working) continue;
1830
+ if (!userHasAiReply2(conv.messages, working.id)) {
1831
+ logger.debug(
1832
+ { conversationId: conv.id, messageId: working.id },
1833
+ "resume working turn"
1834
+ );
1835
+ onWorkingTurn?.({ conversationId: conv.id, message: working });
1836
+ }
1837
+ }
1838
+ return next;
1839
+ };
1840
+ const upsertMessage = (conversationId, message) => {
1841
+ const conv = conversations.get(conversationId) ?? {
1842
+ id: conversationId,
1843
+ updatedAt: nowIso(),
1844
+ messages: []
1845
+ };
1846
+ const index = conv.messages.findIndex((row) => row.id === message.id);
1847
+ const wasWorking = index >= 0 && conv.messages[index]?.queueStatus === "working";
1848
+ if (index >= 0) {
1849
+ conv.messages[index] = { ...conv.messages[index], ...message };
1850
+ } else {
1851
+ conv.messages.push(message);
1852
+ }
1853
+ conv.updatedAt = nowIso();
1854
+ conversations.set(conversationId, conv);
1855
+ const becameWorking = message.role === "user" && message.queueStatus === "working" && message.senderType !== "developer" && !wasWorking;
1856
+ if (becameWorking) {
1857
+ logger.debug(
1858
+ { conversationId, messageId: message.id },
1859
+ "working turn"
1860
+ );
1861
+ onWorkingTurn?.({ conversationId, message });
1862
+ }
1863
+ };
1864
+ const deleteMessages = (conversationId, ids) => {
1865
+ const conv = conversations.get(conversationId);
1866
+ if (!conv || ids.length === 0) return;
1867
+ const drop = new Set(ids);
1868
+ conv.messages = conv.messages.filter((row) => !drop.has(row.id));
1869
+ conv.updatedAt = nowIso();
1870
+ };
1871
+ const applyEvent = (msg) => {
1872
+ const externalId = typeof msg.externalId === "string" && msg.externalId ? msg.externalId : typeof msg.conversationId === "string" ? msg.conversationId : "";
1873
+ if (!externalId) return;
1874
+ if (msg.type === "message.created") {
1875
+ const raw = msg.message && typeof msg.message === "object" ? msg.message : null;
1876
+ const cached = raw ? asCachedMessage(raw) : null;
1877
+ if (cached) {
1878
+ upsertMessage(externalId, cached);
1879
+ return;
1880
+ }
1881
+ void refreshConversation(externalId).catch(() => void 0);
1882
+ return;
1883
+ }
1884
+ if (msg.type === "message.updated") {
1885
+ const raw = msg.message && typeof msg.message === "object" ? msg.message : null;
1886
+ const cached = raw ? asCachedMessage(raw) : null;
1887
+ if (cached) {
1888
+ upsertMessage(externalId, cached);
1889
+ return;
1890
+ }
1891
+ void refreshConversation(externalId).catch(() => void 0);
1892
+ return;
1893
+ }
1894
+ if (msg.type === "message.deleted") {
1895
+ const ids = Array.isArray(msg.ids) ? msg.ids.filter((id) => typeof id === "string") : [];
1896
+ deleteMessages(externalId, ids);
1897
+ }
1898
+ };
1899
+ const stopSocket = () => {
1900
+ if (pingTimer) {
1901
+ clearInterval(pingTimer);
1902
+ pingTimer = null;
1903
+ }
1904
+ if (reconnectTimer) {
1905
+ clearTimeout(reconnectTimer);
1906
+ reconnectTimer = null;
1907
+ }
1908
+ try {
1909
+ socket?.close();
1910
+ } catch {
1911
+ }
1912
+ socket = null;
1913
+ };
1914
+ const connectRealtime = () => {
1915
+ if (stopped) return;
1916
+ const WebSocketCtor = globalThis.WebSocket;
1917
+ if (!WebSocketCtor) {
1918
+ info("WebSocket unavailable; cache will refresh after local writes only");
1919
+ return;
1920
+ }
1921
+ const ws = new WebSocketCtor(toWsUrl(maintainerProUrl, apiKey));
1922
+ socket = ws;
1923
+ ws.addEventListener("open", () => {
1924
+ reconnectAttempt = 0;
1925
+ logger.debug("store websocket open");
1926
+ if (pingTimer) clearInterval(pingTimer);
1927
+ pingTimer = setInterval(() => {
1928
+ try {
1929
+ if (ws.readyState === 1) ws.send(JSON.stringify({ type: "ping" }));
1930
+ } catch {
1931
+ }
1932
+ }, 2e4);
1933
+ });
1934
+ ws.addEventListener("message", (event) => {
1935
+ let payload;
1936
+ try {
1937
+ payload = JSON.parse(String(event.data));
1938
+ } catch {
1939
+ return;
1940
+ }
1941
+ if (payload.type === "hello") {
1942
+ const meta = payload.meta && typeof payload.meta === "object" ? payload.meta : null;
1943
+ const sandboxId = typeof meta?.sandboxId === "string" ? meta.sandboxId : "";
1944
+ if (sandboxId && ws.readyState === 1) {
1945
+ ws.send(
1946
+ JSON.stringify({ type: "subscribe", channels: [`sandbox:${sandboxId}`] })
1947
+ );
1948
+ }
1949
+ logger.debug({ sandboxId }, "store websocket hello");
1950
+ return;
1951
+ }
1952
+ if (payload.type === "message.created" || payload.type === "message.updated" || payload.type === "message.deleted") {
1953
+ logger.debug({ type: payload.type }, "store websocket event");
1954
+ applyEvent(payload);
1955
+ }
1956
+ });
1957
+ ws.addEventListener("close", () => {
1958
+ if (socket === ws) socket = null;
1959
+ if (pingTimer) {
1960
+ clearInterval(pingTimer);
1961
+ pingTimer = null;
1962
+ }
1963
+ if (stopped) return;
1964
+ const delay = Math.min(3e4, 1e3 * 2 ** Math.min(reconnectAttempt, 5));
1965
+ reconnectAttempt += 1;
1966
+ logger.debug({ delay, reconnectAttempt }, "store websocket reconnect");
1967
+ reconnectTimer = setTimeout(() => {
1968
+ reconnectTimer = null;
1969
+ void hydrate().catch((err) => {
1970
+ logger.warn(
1971
+ `cache rehydrate failed: ${err instanceof Error ? err.message : String(err)}`
1972
+ );
1973
+ }).finally(() => {
1974
+ connectRealtime();
1975
+ });
1976
+ }, delay);
1977
+ });
1978
+ };
1979
+ const ready = hydrate().catch((err) => {
1980
+ logger.warn(
1981
+ `cache hydrate failed: ${err instanceof Error ? err.message : String(err)}`
1982
+ );
1983
+ return stats();
1984
+ });
1985
+ void ready.then(() => {
1986
+ if (!stopped) connectRealtime();
1987
+ });
1988
+ const store = {
1989
+ ready,
1990
+ stop: () => {
1991
+ stopped = true;
1992
+ stopSocket();
1993
+ },
1994
+ async ensureConversation(id) {
1995
+ await remote.ensureConversation(id);
1996
+ if (!conversations.has(id)) {
1997
+ putConversation(id, []);
1998
+ }
1999
+ },
2000
+ async saveMessage(input) {
2001
+ const saved = await remote.saveMessage(input);
2002
+ await refreshConversation(input.conversationId).catch(() => void 0);
2003
+ return saved;
2004
+ },
2005
+ async listMessages(conversationId) {
2006
+ await ready.catch(() => void 0);
2007
+ const hit = conversations.get(conversationId);
2008
+ if (hit) return cloneMessages(hit.messages);
2009
+ return refreshConversation(conversationId);
2010
+ },
2011
+ async clearWorkingMessages(conversationId) {
2012
+ await remote.clearWorkingMessages?.(conversationId);
2013
+ await refreshConversation(conversationId).catch(() => void 0);
2014
+ },
2015
+ async releaseWorkingTurn(conversationId, parentMessageId) {
2016
+ const result = await remote.releaseWorkingTurn?.(
2017
+ conversationId,
2018
+ parentMessageId
2019
+ );
2020
+ await refreshConversation(conversationId).catch(() => void 0);
2021
+ return result ?? { nextQueued: null };
2022
+ },
2023
+ async recoverQueue(conversationId) {
2024
+ const result = await remote.recoverQueue?.(conversationId);
2025
+ await refreshConversation(conversationId).catch(() => void 0);
2026
+ const conv = conversations.get(conversationId);
2027
+ if (conv) {
2028
+ const working = conv.messages.find(
2029
+ (row) => row.role === "user" && row.queueStatus === "working" && row.senderType !== "developer"
2030
+ );
2031
+ if (working && !userHasAiReply2(conv.messages, working.id)) {
2032
+ logger.info(
2033
+ { conversationId, messageId: working.id, action: result?.action },
2034
+ "recoverQueue \u2192 resume working turn"
2035
+ );
2036
+ onWorkingTurn?.({ conversationId, message: working });
2037
+ }
2038
+ }
2039
+ return result ?? {
2040
+ action: "noop",
2041
+ workingId: null,
2042
+ promotedId: null,
2043
+ nextQueued: null
2044
+ };
2045
+ },
2046
+ async listConversations() {
2047
+ await ready.catch(() => void 0);
2048
+ return [...conversations.values()].map((conv) => ({
2049
+ id: conv.id,
2050
+ updatedAt: conv.updatedAt,
2051
+ messages: cloneMessages(conv.messages)
2052
+ })).sort(
2053
+ (a, b) => (Date.parse(b.updatedAt) || 0) - (Date.parse(a.updatedAt) || 0)
2054
+ );
2055
+ },
2056
+ async saveToolEvents(conversationId, events) {
2057
+ await remote.saveToolEvents?.(conversationId, events);
2058
+ },
2059
+ async uploadAttachments(conversationId, attachments) {
2060
+ if (!remote.uploadAttachments) {
2061
+ return { refs: [], localPaths: [] };
2062
+ }
2063
+ return remote.uploadAttachments(conversationId, attachments);
2064
+ }
2065
+ };
2066
+ return store;
2067
+ }
2068
+
2069
+ // src/http/maintainer-pro-store.ts
942
2070
  async function mpFetch(baseUrl, apiKey, pathName, init, fetchImpl) {
943
2071
  const url = `${baseUrl.replace(/\/$/, "")}${pathName}`;
944
2072
  const res = await fetchImpl(url, {
@@ -965,7 +2093,7 @@ function createMaintainerProStore(options) {
965
2093
  const baseUrl = options.baseUrl;
966
2094
  const apiKey = options.apiKey;
967
2095
  const fetchImpl = options.fetchImpl ?? fetch;
968
- const tempDir = options.tempDir ?? path4.join(os.tmpdir(), "maintainer-pro-store");
2096
+ const tempDir = options.tempDir ?? path5.join(os.tmpdir(), "maintainer-pro-store");
969
2097
  const store = {
970
2098
  async ensureConversation(id) {
971
2099
  await mpFetch(
@@ -988,17 +2116,54 @@ function createMaintainerProStore(options) {
988
2116
  {
989
2117
  method: "POST",
990
2118
  body: JSON.stringify({
2119
+ id: input.id,
991
2120
  role: input.role,
992
2121
  content: input.content,
993
2122
  provider: input.provider,
994
2123
  senderType: input.senderType,
995
2124
  senderName: input.senderName,
996
- attachmentIds
2125
+ parentMessageId: input.parentMessageId ?? void 0,
2126
+ attachmentIds,
2127
+ intent: input.intent,
2128
+ queueStatus: input.queueStatus ?? void 0
2129
+ })
2130
+ },
2131
+ fetchImpl
2132
+ );
2133
+ return {
2134
+ ...data.message,
2135
+ nextQueued: data.nextQueued ?? null
2136
+ };
2137
+ },
2138
+ async releaseWorkingTurn(conversationId, parentMessageId) {
2139
+ const data = await mpFetch(
2140
+ baseUrl,
2141
+ apiKey,
2142
+ `/api/v1/store/conversations/${encodeURIComponent(conversationId)}/complete-turn`,
2143
+ {
2144
+ method: "POST",
2145
+ body: JSON.stringify({
2146
+ parentMessageId: parentMessageId ?? void 0
997
2147
  })
998
2148
  },
999
2149
  fetchImpl
1000
2150
  );
1001
- return data.message;
2151
+ return { nextQueued: data.nextQueued ?? null };
2152
+ },
2153
+ async recoverQueue(conversationId) {
2154
+ const data = await mpFetch(
2155
+ baseUrl,
2156
+ apiKey,
2157
+ `/api/v1/store/conversations/${encodeURIComponent(conversationId)}/recover-queue`,
2158
+ { method: "POST", body: "{}" },
2159
+ fetchImpl
2160
+ );
2161
+ return {
2162
+ action: data.action ?? "noop",
2163
+ workingId: data.workingId ?? null,
2164
+ promotedId: data.promotedId ?? null,
2165
+ nextQueued: data.nextQueued ?? null
2166
+ };
1002
2167
  },
1003
2168
  async clearWorkingMessages(conversationId) {
1004
2169
  await mpFetch(
@@ -1029,6 +2194,16 @@ function createMaintainerProStore(options) {
1029
2194
  );
1030
2195
  return data.conversations ?? [];
1031
2196
  },
2197
+ async listSnapshot() {
2198
+ const data = await mpFetch(
2199
+ baseUrl,
2200
+ apiKey,
2201
+ `/api/v1/store/snapshot`,
2202
+ { method: "GET" },
2203
+ fetchImpl
2204
+ );
2205
+ return data.conversations ?? [];
2206
+ },
1032
2207
  async saveToolEvents(conversationId, events) {
1033
2208
  if (!events.length) return;
1034
2209
  await mpFetch(
@@ -1070,7 +2245,7 @@ function createMaintainerProStore(options) {
1070
2245
  attachmentIds.push(id);
1071
2246
  refs.push(data.attachment.ref || `maintainer-pro://${id}`);
1072
2247
  const ext = item.mimeType.includes("jpeg") || item.mimeType.includes("jpg") ? ".jpg" : item.mimeType.includes("webp") ? ".webp" : item.mimeType.includes("gif") ? ".gif" : ".png";
1073
- const localPath = path4.join(
2248
+ const localPath = path5.join(
1074
2249
  tempDir,
1075
2250
  `${conversationId}-${id}${ext}`
1076
2251
  );
@@ -1080,13 +2255,34 @@ function createMaintainerProStore(options) {
1080
2255
  return { refs, localPaths, attachmentIds };
1081
2256
  }
1082
2257
  };
1083
- return store;
2258
+ if (options.sync === false) {
2259
+ return Object.assign(store, {
2260
+ ready: Promise.resolve({ conversations: 0, messages: 0 }),
2261
+ stop() {
2262
+ }
2263
+ });
2264
+ }
2265
+ return createSyncedChatStore({
2266
+ remote: store,
2267
+ maintainerProUrl: baseUrl,
2268
+ apiKey,
2269
+ logger: options.logger,
2270
+ log: options.log,
2271
+ onWorkingTurn: options.onWorkingTurn
2272
+ });
1084
2273
  }
1085
- function createMaintainerProStoreFromEnv() {
2274
+ function createMaintainerProStoreFromEnv(options) {
1086
2275
  const baseUrl = process.env.MAINTAINER_PRO_URL?.trim();
1087
2276
  const apiKey = process.env.MAINTAINER_PRO_API_KEY?.trim();
1088
2277
  if (!baseUrl || !apiKey) return null;
1089
- return createMaintainerProStore({ baseUrl, apiKey });
2278
+ return createMaintainerProStore({
2279
+ baseUrl,
2280
+ apiKey,
2281
+ logger: options?.logger,
2282
+ log: options?.log,
2283
+ sync: options?.sync,
2284
+ onWorkingTurn: options?.onWorkingTurn
2285
+ });
1090
2286
  }
1091
2287
 
1092
2288
  // src/workspace-inspect.ts
@@ -1195,12 +2391,17 @@ async function inspectAndRepairWorkspace(input) {
1195
2391
  };
1196
2392
  }
1197
2393
  export {
2394
+ ACCESS_IGNORE_BEGIN,
2395
+ ACCESS_IGNORE_END,
2396
+ DEFAULT_AI_IGNORE_PATHS,
2397
+ PROMPT_SECTION,
1198
2398
  WORKING_PROVIDER,
1199
2399
  buildClaudeUserPrompt,
1200
2400
  buildConversationPrompt,
1201
2401
  buildCursorPrompt,
1202
2402
  buildPriorConversationsContext,
1203
2403
  callAi,
2404
+ collectParentChain,
1204
2405
  commandExists,
1205
2406
  createAntigravityProvider,
1206
2407
  createBuiltinProviders,
@@ -1208,17 +2409,34 @@ export {
1208
2409
  createClaudeProvider,
1209
2410
  createCursorProvider,
1210
2411
  createDefaultSystemPrompt,
2412
+ createInfoLogger,
1211
2413
  createLocalDirectoryStore,
2414
+ createLogger,
1212
2415
  createMaintainerProStore,
1213
2416
  createMaintainerProStoreFromEnv,
2417
+ createSyncedChatStore,
1214
2418
  createToolValidator,
2419
+ formatAccessPolicyPromptSection,
1215
2420
  formatClientContext,
2421
+ formatParentChainContext,
1216
2422
  getProviderPreference,
1217
2423
  inspectAndRepairWorkspace,
2424
+ isDevMode,
2425
+ isIgnoredRelative,
2426
+ isInsideWorkspace,
2427
+ isPathAllowed,
2428
+ normalizeIgnorePaths,
2429
+ parentChainForTurn,
1218
2430
  parseAiResponse,
2431
+ parseIgnorePathsEnv,
2432
+ previewText,
1219
2433
  providerLabel,
2434
+ renderManagedIgnoreBlock,
1220
2435
  resolveCliBinary,
2436
+ resolveIgnorePaths,
2437
+ resolveLogLevel,
1221
2438
  resolveProvider,
1222
2439
  saveChatAttachments,
1223
- toNextRoute
2440
+ toNextRoute,
2441
+ upsertManagedIgnoreFile
1224
2442
  };