oh-my-opencode 2.6.2 → 2.7.1

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
@@ -222,8 +222,8 @@ var require_utils = __commonJS((exports) => {
222
222
  }
223
223
  return output;
224
224
  };
225
- exports.basename = (path5, { windows } = {}) => {
226
- const segs = path5.split(windows ? /[\\/]/ : "/");
225
+ exports.basename = (path4, { windows } = {}) => {
226
+ const segs = path4.split(windows ? /[\\/]/ : "/");
227
227
  const last = segs[segs.length - 1];
228
228
  if (last === "") {
229
229
  return segs[segs.length - 2];
@@ -3276,9 +3276,6 @@ function getUserConfigDir() {
3276
3276
  import * as path3 from "path";
3277
3277
  import * as os3 from "os";
3278
3278
  function getDataDir() {
3279
- if (process.platform === "win32") {
3280
- return process.env.LOCALAPPDATA ?? path3.join(os3.homedir(), "AppData", "Local");
3281
- }
3282
3279
  return process.env.XDG_DATA_HOME ?? path3.join(os3.homedir(), ".local", "share");
3283
3280
  }
3284
3281
  function getOpenCodeStorageDir() {
@@ -4374,23 +4371,6 @@ function injectHookMessage(sessionID, hookContent, originalMessage) {
4374
4371
  return false;
4375
4372
  }
4376
4373
  }
4377
- // src/hooks/non-interactive-env/detector.ts
4378
- function isNonInteractive() {
4379
- if (process.env.CI === "true" || process.env.CI === "1") {
4380
- return true;
4381
- }
4382
- if (process.env.OPENCODE_RUN === "true" || process.env.OPENCODE_NON_INTERACTIVE === "true") {
4383
- return true;
4384
- }
4385
- if (process.env.GITHUB_ACTIONS === "true") {
4386
- return true;
4387
- }
4388
- if (process.stdout.isTTY !== true) {
4389
- return true;
4390
- }
4391
- return false;
4392
- }
4393
-
4394
4374
  // src/hooks/todo-continuation-enforcer.ts
4395
4375
  var HOOK_NAME = "todo-continuation-enforcer";
4396
4376
  var CONTINUATION_PROMPT = `[SYSTEM REMINDER - TODO CONTINUATION]
@@ -4400,6 +4380,9 @@ Incomplete tasks remain in your todo list. Continue working on the next pending
4400
4380
  - Proceed without asking for permission
4401
4381
  - Mark each task complete when finished
4402
4382
  - Do not stop until all tasks are done`;
4383
+ var COUNTDOWN_SECONDS = 2;
4384
+ var TOAST_DURATION_MS = 900;
4385
+ var ERROR_COOLDOWN_MS = 3000;
4403
4386
  function getMessageDir(sessionID) {
4404
4387
  if (!existsSync6(MESSAGE_STORAGE))
4405
4388
  return null;
@@ -4413,7 +4396,7 @@ function getMessageDir(sessionID) {
4413
4396
  }
4414
4397
  return null;
4415
4398
  }
4416
- function detectInterrupt(error) {
4399
+ function isAbortError(error) {
4417
4400
  if (!error)
4418
4401
  return false;
4419
4402
  if (typeof error === "object") {
@@ -4433,270 +4416,233 @@ function detectInterrupt(error) {
4433
4416
  }
4434
4417
  return false;
4435
4418
  }
4436
- var COUNTDOWN_SECONDS = 2;
4437
- var TOAST_DURATION_MS = 900;
4419
+ function getIncompleteCount(todos) {
4420
+ return todos.filter((t) => t.status !== "completed" && t.status !== "cancelled").length;
4421
+ }
4438
4422
  function createTodoContinuationEnforcer(ctx, options = {}) {
4439
4423
  const { backgroundManager } = options;
4440
- const remindedSessions = new Set;
4441
- const interruptedSessions = new Set;
4442
- const errorSessions = new Set;
4443
- const recoveringSessions = new Set;
4444
- const pendingCountdowns = new Map;
4445
- const preemptivelyInjectedSessions = new Set;
4424
+ const sessions = new Map;
4425
+ function getState(sessionID) {
4426
+ let state2 = sessions.get(sessionID);
4427
+ if (!state2) {
4428
+ state2 = {};
4429
+ sessions.set(sessionID, state2);
4430
+ }
4431
+ return state2;
4432
+ }
4433
+ function cancelCountdown(sessionID) {
4434
+ const state2 = sessions.get(sessionID);
4435
+ if (!state2)
4436
+ return;
4437
+ if (state2.countdownTimer) {
4438
+ clearTimeout(state2.countdownTimer);
4439
+ state2.countdownTimer = undefined;
4440
+ }
4441
+ if (state2.countdownInterval) {
4442
+ clearInterval(state2.countdownInterval);
4443
+ state2.countdownInterval = undefined;
4444
+ }
4445
+ }
4446
+ function cleanup(sessionID) {
4447
+ cancelCountdown(sessionID);
4448
+ sessions.delete(sessionID);
4449
+ }
4446
4450
  const markRecovering = (sessionID) => {
4447
- recoveringSessions.add(sessionID);
4451
+ const state2 = getState(sessionID);
4452
+ state2.isRecovering = true;
4453
+ cancelCountdown(sessionID);
4454
+ log(`[${HOOK_NAME}] Session marked as recovering`, { sessionID });
4448
4455
  };
4449
4456
  const markRecoveryComplete = (sessionID) => {
4450
- recoveringSessions.delete(sessionID);
4457
+ const state2 = sessions.get(sessionID);
4458
+ if (state2) {
4459
+ state2.isRecovering = false;
4460
+ log(`[${HOOK_NAME}] Session recovery complete`, { sessionID });
4461
+ }
4451
4462
  };
4463
+ async function showCountdownToast(seconds, incompleteCount) {
4464
+ await ctx.client.tui.showToast({
4465
+ body: {
4466
+ title: "Todo Continuation",
4467
+ message: `Resuming in ${seconds}s... (${incompleteCount} tasks remaining)`,
4468
+ variant: "warning",
4469
+ duration: TOAST_DURATION_MS
4470
+ }
4471
+ }).catch(() => {});
4472
+ }
4473
+ async function injectContinuation(sessionID, incompleteCount, total) {
4474
+ const state2 = sessions.get(sessionID);
4475
+ if (state2?.isRecovering) {
4476
+ log(`[${HOOK_NAME}] Skipped injection: in recovery`, { sessionID });
4477
+ return;
4478
+ }
4479
+ if (state2?.lastErrorAt && Date.now() - state2.lastErrorAt < ERROR_COOLDOWN_MS) {
4480
+ log(`[${HOOK_NAME}] Skipped injection: recent error`, { sessionID });
4481
+ return;
4482
+ }
4483
+ const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((t) => t.status === "running") : false;
4484
+ if (hasRunningBgTasks) {
4485
+ log(`[${HOOK_NAME}] Skipped injection: background tasks running`, { sessionID });
4486
+ return;
4487
+ }
4488
+ let todos = [];
4489
+ try {
4490
+ const response = await ctx.client.session.todo({ path: { id: sessionID } });
4491
+ todos = response.data ?? response;
4492
+ } catch (err) {
4493
+ log(`[${HOOK_NAME}] Failed to fetch todos`, { sessionID, error: String(err) });
4494
+ return;
4495
+ }
4496
+ const freshIncompleteCount = getIncompleteCount(todos);
4497
+ if (freshIncompleteCount === 0) {
4498
+ log(`[${HOOK_NAME}] Skipped injection: no incomplete todos`, { sessionID });
4499
+ return;
4500
+ }
4501
+ const messageDir = getMessageDir(sessionID);
4502
+ const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null;
4503
+ const hasWritePermission = !prevMessage?.tools || prevMessage.tools.write !== false && prevMessage.tools.edit !== false;
4504
+ if (!hasWritePermission) {
4505
+ log(`[${HOOK_NAME}] Skipped: agent lacks write permission`, { sessionID, agent: prevMessage?.agent });
4506
+ return;
4507
+ }
4508
+ const agentName = prevMessage?.agent?.toLowerCase() ?? "";
4509
+ if (agentName === "plan" || agentName === "planner-sisyphus") {
4510
+ log(`[${HOOK_NAME}] Skipped: plan mode agent`, { sessionID, agent: prevMessage?.agent });
4511
+ return;
4512
+ }
4513
+ const prompt = `${CONTINUATION_PROMPT}
4514
+
4515
+ [Status: ${todos.length - freshIncompleteCount}/${todos.length} completed, ${freshIncompleteCount} remaining]`;
4516
+ try {
4517
+ log(`[${HOOK_NAME}] Injecting continuation`, { sessionID, agent: prevMessage?.agent, incompleteCount: freshIncompleteCount });
4518
+ await ctx.client.session.prompt({
4519
+ path: { id: sessionID },
4520
+ body: {
4521
+ agent: prevMessage?.agent,
4522
+ parts: [{ type: "text", text: prompt }]
4523
+ },
4524
+ query: { directory: ctx.directory }
4525
+ });
4526
+ log(`[${HOOK_NAME}] Injection successful`, { sessionID });
4527
+ } catch (err) {
4528
+ log(`[${HOOK_NAME}] Injection failed`, { sessionID, error: String(err) });
4529
+ }
4530
+ }
4531
+ function startCountdown(sessionID, incompleteCount, total) {
4532
+ const state2 = getState(sessionID);
4533
+ cancelCountdown(sessionID);
4534
+ let secondsRemaining = COUNTDOWN_SECONDS;
4535
+ showCountdownToast(secondsRemaining, incompleteCount);
4536
+ state2.countdownInterval = setInterval(() => {
4537
+ secondsRemaining--;
4538
+ if (secondsRemaining > 0) {
4539
+ showCountdownToast(secondsRemaining, incompleteCount);
4540
+ }
4541
+ }, 1000);
4542
+ state2.countdownTimer = setTimeout(() => {
4543
+ cancelCountdown(sessionID);
4544
+ injectContinuation(sessionID, incompleteCount, total);
4545
+ }, COUNTDOWN_SECONDS * 1000);
4546
+ log(`[${HOOK_NAME}] Countdown started`, { sessionID, seconds: COUNTDOWN_SECONDS, incompleteCount });
4547
+ }
4452
4548
  const handler = async ({ event }) => {
4453
4549
  const props = event.properties;
4454
4550
  if (event.type === "session.error") {
4455
4551
  const sessionID = props?.sessionID;
4456
- if (sessionID) {
4457
- const isInterrupt = detectInterrupt(props?.error);
4458
- errorSessions.add(sessionID);
4459
- if (isInterrupt) {
4460
- interruptedSessions.add(sessionID);
4461
- }
4462
- log(`[${HOOK_NAME}] session.error received`, { sessionID, isInterrupt, error: props?.error });
4463
- const countdown = pendingCountdowns.get(sessionID);
4464
- if (countdown) {
4465
- clearInterval(countdown.intervalId);
4466
- pendingCountdowns.delete(sessionID);
4467
- }
4468
- }
4552
+ if (!sessionID)
4553
+ return;
4554
+ const state2 = getState(sessionID);
4555
+ state2.lastErrorAt = Date.now();
4556
+ cancelCountdown(sessionID);
4557
+ log(`[${HOOK_NAME}] session.error`, { sessionID, isAbort: isAbortError(props?.error) });
4469
4558
  return;
4470
4559
  }
4471
4560
  if (event.type === "session.idle") {
4472
4561
  const sessionID = props?.sessionID;
4473
4562
  if (!sessionID)
4474
4563
  return;
4475
- log(`[${HOOK_NAME}] session.idle received`, { sessionID });
4564
+ log(`[${HOOK_NAME}] session.idle`, { sessionID });
4476
4565
  const mainSessionID2 = getMainSessionID();
4477
4566
  if (mainSessionID2 && sessionID !== mainSessionID2) {
4478
- log(`[${HOOK_NAME}] Skipped: not main session`, { sessionID, mainSessionID: mainSessionID2 });
4567
+ log(`[${HOOK_NAME}] Skipped: not main session`, { sessionID });
4479
4568
  return;
4480
4569
  }
4481
- const existingCountdown = pendingCountdowns.get(sessionID);
4482
- if (existingCountdown) {
4483
- clearInterval(existingCountdown.intervalId);
4484
- pendingCountdowns.delete(sessionID);
4485
- log(`[${HOOK_NAME}] Cancelled existing countdown`, { sessionID });
4486
- }
4487
- if (recoveringSessions.has(sessionID)) {
4488
- log(`[${HOOK_NAME}] Skipped: session in recovery mode`, { sessionID });
4570
+ const state2 = getState(sessionID);
4571
+ if (state2.isRecovering) {
4572
+ log(`[${HOOK_NAME}] Skipped: in recovery`, { sessionID });
4489
4573
  return;
4490
4574
  }
4491
- const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID);
4492
- if (shouldBypass) {
4493
- interruptedSessions.delete(sessionID);
4494
- errorSessions.delete(sessionID);
4495
- log(`[${HOOK_NAME}] Skipped: error/interrupt bypass`, { sessionID });
4575
+ if (state2.lastErrorAt && Date.now() - state2.lastErrorAt < ERROR_COOLDOWN_MS) {
4576
+ log(`[${HOOK_NAME}] Skipped: recent error (cooldown)`, { sessionID });
4496
4577
  return;
4497
4578
  }
4498
- if (remindedSessions.has(sessionID)) {
4499
- log(`[${HOOK_NAME}] Skipped: already reminded this session`, { sessionID });
4579
+ const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((t) => t.status === "running") : false;
4580
+ if (hasRunningBgTasks) {
4581
+ log(`[${HOOK_NAME}] Skipped: background tasks running`, { sessionID });
4500
4582
  return;
4501
4583
  }
4502
4584
  let todos = [];
4503
4585
  try {
4504
- log(`[${HOOK_NAME}] Fetching todos for session`, { sessionID });
4505
- const response = await ctx.client.session.todo({
4506
- path: { id: sessionID }
4507
- });
4586
+ const response = await ctx.client.session.todo({ path: { id: sessionID } });
4508
4587
  todos = response.data ?? response;
4509
- log(`[${HOOK_NAME}] Todo API response`, { sessionID, todosCount: todos?.length ?? 0 });
4510
4588
  } catch (err) {
4511
- log(`[${HOOK_NAME}] Todo API error`, { sessionID, error: String(err) });
4589
+ log(`[${HOOK_NAME}] Todo fetch failed`, { sessionID, error: String(err) });
4512
4590
  return;
4513
4591
  }
4514
4592
  if (!todos || todos.length === 0) {
4515
- log(`[${HOOK_NAME}] No todos found`, { sessionID });
4593
+ log(`[${HOOK_NAME}] No todos`, { sessionID });
4516
4594
  return;
4517
4595
  }
4518
- const incomplete = todos.filter((t) => t.status !== "completed" && t.status !== "cancelled");
4519
- if (incomplete.length === 0) {
4520
- log(`[${HOOK_NAME}] All todos completed`, { sessionID, total: todos.length });
4596
+ const incompleteCount = getIncompleteCount(todos);
4597
+ if (incompleteCount === 0) {
4598
+ log(`[${HOOK_NAME}] All todos complete`, { sessionID, total: todos.length });
4521
4599
  return;
4522
4600
  }
4523
- log(`[${HOOK_NAME}] Found incomplete todos, starting countdown`, { sessionID, incomplete: incomplete.length, total: todos.length });
4524
- const showCountdownToast = async (seconds) => {
4525
- await ctx.client.tui.showToast({
4526
- body: {
4527
- title: "Todo Continuation",
4528
- message: `Resuming in ${seconds}s... (${incomplete.length} tasks remaining)`,
4529
- variant: "warning",
4530
- duration: TOAST_DURATION_MS
4531
- }
4532
- }).catch(() => {});
4533
- };
4534
- const executeAfterCountdown = async () => {
4535
- pendingCountdowns.delete(sessionID);
4536
- log(`[${HOOK_NAME}] Countdown finished, executing continuation`, { sessionID });
4537
- if (recoveringSessions.has(sessionID)) {
4538
- log(`[${HOOK_NAME}] Abort: session entered recovery mode during countdown`, { sessionID });
4539
- return;
4540
- }
4541
- if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) {
4542
- log(`[${HOOK_NAME}] Abort: error/interrupt occurred during countdown`, { sessionID });
4543
- interruptedSessions.delete(sessionID);
4544
- errorSessions.delete(sessionID);
4545
- return;
4546
- }
4547
- let freshTodos = [];
4548
- try {
4549
- log(`[${HOOK_NAME}] Re-verifying todos after countdown`, { sessionID });
4550
- const response = await ctx.client.session.todo({
4551
- path: { id: sessionID }
4552
- });
4553
- freshTodos = response.data ?? response;
4554
- log(`[${HOOK_NAME}] Fresh todo count`, { sessionID, todosCount: freshTodos?.length ?? 0 });
4555
- } catch (err) {
4556
- log(`[${HOOK_NAME}] Failed to re-verify todos`, { sessionID, error: String(err) });
4557
- return;
4558
- }
4559
- const freshIncomplete = freshTodos.filter((t) => t.status !== "completed" && t.status !== "cancelled");
4560
- if (freshIncomplete.length === 0) {
4561
- log(`[${HOOK_NAME}] Abort: no incomplete todos after countdown`, { sessionID, total: freshTodos.length });
4562
- return;
4563
- }
4564
- log(`[${HOOK_NAME}] Confirmed incomplete todos, proceeding with injection`, { sessionID, incomplete: freshIncomplete.length, total: freshTodos.length });
4565
- remindedSessions.add(sessionID);
4566
- try {
4567
- const messageDir = getMessageDir(sessionID);
4568
- const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null;
4569
- const agentHasWritePermission = !prevMessage?.tools || prevMessage.tools.write !== false && prevMessage.tools.edit !== false;
4570
- if (!agentHasWritePermission) {
4571
- log(`[${HOOK_NAME}] Skipped: previous agent lacks write permission`, { sessionID, agent: prevMessage?.agent, tools: prevMessage?.tools });
4572
- remindedSessions.delete(sessionID);
4573
- return;
4574
- }
4575
- log(`[${HOOK_NAME}] Injecting continuation prompt`, { sessionID, agent: prevMessage?.agent });
4576
- await ctx.client.session.prompt({
4577
- path: { id: sessionID },
4578
- body: {
4579
- agent: prevMessage?.agent,
4580
- parts: [
4581
- {
4582
- type: "text",
4583
- text: `${CONTINUATION_PROMPT}
4584
-
4585
- [Status: ${freshTodos.length - freshIncomplete.length}/${freshTodos.length} completed, ${freshIncomplete.length} remaining]`
4586
- }
4587
- ]
4588
- },
4589
- query: { directory: ctx.directory }
4590
- });
4591
- log(`[${HOOK_NAME}] Continuation prompt injected successfully`, { sessionID });
4592
- } catch (err) {
4593
- log(`[${HOOK_NAME}] Prompt injection failed`, { sessionID, error: String(err) });
4594
- remindedSessions.delete(sessionID);
4595
- }
4596
- };
4597
- let secondsRemaining = COUNTDOWN_SECONDS;
4598
- showCountdownToast(secondsRemaining).catch(() => {});
4599
- const intervalId = setInterval(() => {
4600
- secondsRemaining--;
4601
- if (secondsRemaining <= 0) {
4602
- clearInterval(intervalId);
4603
- pendingCountdowns.delete(sessionID);
4604
- executeAfterCountdown();
4605
- return;
4606
- }
4607
- const countdown = pendingCountdowns.get(sessionID);
4608
- if (!countdown) {
4609
- clearInterval(intervalId);
4610
- return;
4611
- }
4612
- countdown.secondsRemaining = secondsRemaining;
4613
- showCountdownToast(secondsRemaining).catch(() => {});
4614
- }, 1000);
4615
- pendingCountdowns.set(sessionID, { secondsRemaining, intervalId });
4601
+ startCountdown(sessionID, incompleteCount, todos.length);
4602
+ return;
4616
4603
  }
4617
4604
  if (event.type === "message.updated") {
4618
4605
  const info = props?.info;
4619
4606
  const sessionID = info?.sessionID;
4620
4607
  const role = info?.role;
4621
- const finish = info?.finish;
4622
- log(`[${HOOK_NAME}] message.updated received`, { sessionID, role, finish });
4623
- if (sessionID && role === "user") {
4624
- const countdown = pendingCountdowns.get(sessionID);
4625
- if (countdown) {
4626
- clearInterval(countdown.intervalId);
4627
- pendingCountdowns.delete(sessionID);
4628
- log(`[${HOOK_NAME}] Cancelled countdown on user message`, { sessionID });
4629
- }
4630
- remindedSessions.delete(sessionID);
4631
- preemptivelyInjectedSessions.delete(sessionID);
4632
- }
4633
- if (sessionID && role === "assistant" && finish) {
4634
- remindedSessions.delete(sessionID);
4635
- preemptivelyInjectedSessions.delete(sessionID);
4636
- log(`[${HOOK_NAME}] Cleared reminded/preemptive state on assistant finish`, { sessionID });
4637
- const isTerminalFinish = finish && !["tool-calls", "unknown"].includes(finish);
4638
- if (isTerminalFinish && isNonInteractive()) {
4639
- log(`[${HOOK_NAME}] Terminal finish in non-interactive mode`, { sessionID, finish });
4640
- const mainSessionID2 = getMainSessionID();
4641
- if (mainSessionID2 && sessionID !== mainSessionID2) {
4642
- log(`[${HOOK_NAME}] Skipped preemptive: not main session`, { sessionID, mainSessionID: mainSessionID2 });
4643
- return;
4644
- }
4645
- if (preemptivelyInjectedSessions.has(sessionID)) {
4646
- log(`[${HOOK_NAME}] Skipped preemptive: already injected`, { sessionID });
4647
- return;
4648
- }
4649
- if (recoveringSessions.has(sessionID) || errorSessions.has(sessionID) || interruptedSessions.has(sessionID)) {
4650
- log(`[${HOOK_NAME}] Skipped preemptive: session in error/recovery state`, { sessionID });
4651
- return;
4652
- }
4653
- const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((t) => t.status === "running") : false;
4654
- let hasIncompleteTodos = false;
4655
- try {
4656
- const response = await ctx.client.session.todo({ path: { id: sessionID } });
4657
- const todos = response.data ?? response;
4658
- hasIncompleteTodos = todos?.some((t) => t.status !== "completed" && t.status !== "cancelled") ?? false;
4659
- } catch {
4660
- log(`[${HOOK_NAME}] Failed to fetch todos for preemptive check`, { sessionID });
4661
- }
4662
- if (hasRunningBgTasks || hasIncompleteTodos) {
4663
- log(`[${HOOK_NAME}] Preemptive injection needed`, { sessionID, hasRunningBgTasks, hasIncompleteTodos });
4664
- preemptivelyInjectedSessions.add(sessionID);
4665
- try {
4666
- const messageDir = getMessageDir(sessionID);
4667
- const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null;
4668
- const prompt = hasRunningBgTasks ? "[SYSTEM] Background tasks are still running. Wait for their completion before proceeding." : CONTINUATION_PROMPT;
4669
- await ctx.client.session.prompt({
4670
- path: { id: sessionID },
4671
- body: {
4672
- agent: prevMessage?.agent,
4673
- parts: [{ type: "text", text: prompt }]
4674
- },
4675
- query: { directory: ctx.directory }
4676
- });
4677
- log(`[${HOOK_NAME}] Preemptive injection successful`, { sessionID });
4678
- } catch (err) {
4679
- log(`[${HOOK_NAME}] Preemptive injection failed`, { sessionID, error: String(err) });
4680
- preemptivelyInjectedSessions.delete(sessionID);
4681
- }
4682
- }
4608
+ if (!sessionID)
4609
+ return;
4610
+ if (role === "user") {
4611
+ const state2 = sessions.get(sessionID);
4612
+ if (state2) {
4613
+ state2.lastErrorAt = undefined;
4683
4614
  }
4615
+ cancelCountdown(sessionID);
4616
+ log(`[${HOOK_NAME}] User message: cleared error state`, { sessionID });
4617
+ }
4618
+ if (role === "assistant") {
4619
+ cancelCountdown(sessionID);
4620
+ }
4621
+ return;
4622
+ }
4623
+ if (event.type === "message.part.updated") {
4624
+ const info = props?.info;
4625
+ const sessionID = info?.sessionID;
4626
+ const role = info?.role;
4627
+ if (sessionID && role === "assistant") {
4628
+ cancelCountdown(sessionID);
4629
+ }
4630
+ return;
4631
+ }
4632
+ if (event.type === "tool.execute.before" || event.type === "tool.execute.after") {
4633
+ const sessionID = props?.sessionID;
4634
+ if (sessionID) {
4635
+ cancelCountdown(sessionID);
4684
4636
  }
4637
+ return;
4685
4638
  }
4686
4639
  if (event.type === "session.deleted") {
4687
4640
  const sessionInfo = props?.info;
4688
4641
  if (sessionInfo?.id) {
4689
- remindedSessions.delete(sessionInfo.id);
4690
- interruptedSessions.delete(sessionInfo.id);
4691
- errorSessions.delete(sessionInfo.id);
4692
- recoveringSessions.delete(sessionInfo.id);
4693
- preemptivelyInjectedSessions.delete(sessionInfo.id);
4694
- const countdown = pendingCountdowns.get(sessionInfo.id);
4695
- if (countdown) {
4696
- clearInterval(countdown.intervalId);
4697
- pendingCountdowns.delete(sessionInfo.id);
4698
- }
4642
+ cleanup(sessionInfo.id);
4643
+ log(`[${HOOK_NAME}] Session deleted: cleaned up`, { sessionID: sessionInfo.id });
4699
4644
  }
4645
+ return;
4700
4646
  }
4701
4647
  };
4702
4648
  return {
@@ -5154,28 +5100,7 @@ import { join as join10 } from "path";
5154
5100
 
5155
5101
  // src/hooks/session-recovery/constants.ts
5156
5102
  import { join as join9 } from "path";
5157
-
5158
- // node_modules/xdg-basedir/index.js
5159
- import os4 from "os";
5160
- import path4 from "path";
5161
- var homeDirectory = os4.homedir();
5162
- var { env } = process;
5163
- var xdgData = env.XDG_DATA_HOME || (homeDirectory ? path4.join(homeDirectory, ".local", "share") : undefined);
5164
- var xdgConfig = env.XDG_CONFIG_HOME || (homeDirectory ? path4.join(homeDirectory, ".config") : undefined);
5165
- var xdgState = env.XDG_STATE_HOME || (homeDirectory ? path4.join(homeDirectory, ".local", "state") : undefined);
5166
- var xdgCache = env.XDG_CACHE_HOME || (homeDirectory ? path4.join(homeDirectory, ".cache") : undefined);
5167
- var xdgRuntime = env.XDG_RUNTIME_DIR || undefined;
5168
- var xdgDataDirectories = (env.XDG_DATA_DIRS || "/usr/local/share/:/usr/share/").split(":");
5169
- if (xdgData) {
5170
- xdgDataDirectories.unshift(xdgData);
5171
- }
5172
- var xdgConfigDirectories = (env.XDG_CONFIG_DIRS || "/etc/xdg").split(":");
5173
- if (xdgConfig) {
5174
- xdgConfigDirectories.unshift(xdgConfig);
5175
- }
5176
-
5177
- // src/hooks/session-recovery/constants.ts
5178
- var OPENCODE_STORAGE2 = join9(xdgData ?? "", "opencode", "storage");
5103
+ var OPENCODE_STORAGE2 = getOpenCodeStorageDir();
5179
5104
  var MESSAGE_STORAGE2 = join9(OPENCODE_STORAGE2, "message");
5180
5105
  var PART_STORAGE2 = join9(OPENCODE_STORAGE2, "part");
5181
5106
  var THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"]);
@@ -5297,7 +5222,16 @@ function findEmptyMessages(sessionID) {
5297
5222
  }
5298
5223
  function findEmptyMessageByIndex(sessionID, targetIndex) {
5299
5224
  const messages = readMessages(sessionID);
5300
- const indicesToTry = [targetIndex, targetIndex - 1, targetIndex - 2];
5225
+ const indicesToTry = [
5226
+ targetIndex,
5227
+ targetIndex - 1,
5228
+ targetIndex + 1,
5229
+ targetIndex - 2,
5230
+ targetIndex + 2,
5231
+ targetIndex - 3,
5232
+ targetIndex - 4,
5233
+ targetIndex - 5
5234
+ ];
5301
5235
  for (const idx of indicesToTry) {
5302
5236
  if (idx < 0 || idx >= messages.length)
5303
5237
  continue;
@@ -5723,8 +5657,8 @@ var PLATFORM_MAP = {
5723
5657
  "win32-x64": { os: "windows", arch: "amd64", ext: "zip" }
5724
5658
  };
5725
5659
  function getCacheDir() {
5726
- const xdgCache2 = process.env.XDG_CACHE_HOME;
5727
- const base = xdgCache2 || join11(homedir5(), ".cache");
5660
+ const xdgCache = process.env.XDG_CACHE_HOME;
5661
+ const base = xdgCache || join11(homedir5(), ".cache");
5728
5662
  return join11(base, "oh-my-opencode", "bin");
5729
5663
  }
5730
5664
  function getBinaryName() {
@@ -5785,8 +5719,8 @@ async function downloadCommentChecker() {
5785
5719
  return binaryPath;
5786
5720
  }
5787
5721
  const version = getPackageVersion();
5788
- const { os: os5, arch, ext } = platformInfo;
5789
- const assetName = `comment-checker_v${version}_${os5}_${arch}.${ext}`;
5722
+ const { os: os4, arch, ext } = platformInfo;
5723
+ const assetName = `comment-checker_v${version}_${os4}_${arch}.${ext}`;
5790
5724
  const downloadUrl = `https://github.com/${REPO}/releases/download/v${version}/${assetName}`;
5791
5725
  debugLog(`Downloading from: ${downloadUrl}`);
5792
5726
  console.log(`[oh-my-opencode] Downloading comment-checker binary...`);
@@ -5898,15 +5832,15 @@ async function getCommentCheckerPath() {
5898
5832
  function startBackgroundInit() {
5899
5833
  if (!initPromise) {
5900
5834
  initPromise = getCommentCheckerPath();
5901
- initPromise.then((path5) => {
5902
- debugLog2("background init complete:", path5 || "no binary");
5835
+ initPromise.then((path4) => {
5836
+ debugLog2("background init complete:", path4 || "no binary");
5903
5837
  }).catch((err) => {
5904
5838
  debugLog2("background init error:", err);
5905
5839
  });
5906
5840
  }
5907
5841
  }
5908
5842
  var COMMENT_CHECKER_CLI_PATH = findCommentCheckerPathSync();
5909
- async function runCommentChecker(input, cliPath) {
5843
+ async function runCommentChecker(input, cliPath, customPrompt) {
5910
5844
  const binaryPath = cliPath ?? resolvedCliPath ?? COMMENT_CHECKER_CLI_PATH;
5911
5845
  if (!binaryPath) {
5912
5846
  debugLog2("comment-checker binary not found");
@@ -5919,7 +5853,11 @@ async function runCommentChecker(input, cliPath) {
5919
5853
  const jsonInput = JSON.stringify(input);
5920
5854
  debugLog2("running comment-checker with input:", jsonInput.substring(0, 200));
5921
5855
  try {
5922
- const proc = spawn4([binaryPath], {
5856
+ const args = [binaryPath];
5857
+ if (customPrompt) {
5858
+ args.push("--prompt", customPrompt);
5859
+ }
5860
+ const proc = spawn4(args, {
5923
5861
  stdin: "pipe",
5924
5862
  stdout: "pipe",
5925
5863
  stderr: "pipe"
@@ -5961,6 +5899,7 @@ function debugLog3(...args) {
5961
5899
  var pendingCalls = new Map;
5962
5900
  var PENDING_CALL_TTL = 60000;
5963
5901
  var cliPathPromise = null;
5902
+ var cleanupIntervalStarted = false;
5964
5903
  function cleanupOldPendingCalls() {
5965
5904
  const now = Date.now();
5966
5905
  for (const [callID, call] of pendingCalls) {
@@ -5969,13 +5908,16 @@ function cleanupOldPendingCalls() {
5969
5908
  }
5970
5909
  }
5971
5910
  }
5972
- setInterval(cleanupOldPendingCalls, 1e4);
5973
- function createCommentCheckerHooks() {
5974
- debugLog3("createCommentCheckerHooks called");
5911
+ function createCommentCheckerHooks(config) {
5912
+ debugLog3("createCommentCheckerHooks called", { config });
5913
+ if (!cleanupIntervalStarted) {
5914
+ cleanupIntervalStarted = true;
5915
+ setInterval(cleanupOldPendingCalls, 1e4);
5916
+ }
5975
5917
  startBackgroundInit();
5976
5918
  cliPathPromise = getCommentCheckerPath();
5977
- cliPathPromise.then((path5) => {
5978
- debugLog3("CLI path resolved:", path5 || "disabled (no binary)");
5919
+ cliPathPromise.then((path4) => {
5920
+ debugLog3("CLI path resolved:", path4 || "disabled (no binary)");
5979
5921
  }).catch((err) => {
5980
5922
  debugLog3("CLI path resolution error:", err);
5981
5923
  });
@@ -6031,14 +5973,14 @@ function createCommentCheckerHooks() {
6031
5973
  return;
6032
5974
  }
6033
5975
  debugLog3("using CLI:", cliPath);
6034
- await processWithCli(input, pendingCall, output, cliPath);
5976
+ await processWithCli(input, pendingCall, output, cliPath, config?.custom_prompt);
6035
5977
  } catch (err) {
6036
5978
  debugLog3("tool.execute.after failed:", err);
6037
5979
  }
6038
5980
  }
6039
5981
  };
6040
5982
  }
6041
- async function processWithCli(input, pendingCall, output, cliPath) {
5983
+ async function processWithCli(input, pendingCall, output, cliPath, customPrompt) {
6042
5984
  debugLog3("using CLI mode with path:", cliPath);
6043
5985
  const hookInput = {
6044
5986
  session_id: pendingCall.sessionID,
@@ -6054,7 +5996,7 @@ async function processWithCli(input, pendingCall, output, cliPath) {
6054
5996
  edits: pendingCall.edits
6055
5997
  }
6056
5998
  };
6057
- const result = await runCommentChecker(hookInput, cliPath);
5999
+ const result = await runCommentChecker(hookInput, cliPath, customPrompt);
6058
6000
  if (result.hasComments && result.message) {
6059
6001
  debugLog3("CLI detected comments, appending message");
6060
6002
  output.output += `
@@ -6113,7 +6055,7 @@ import { join as join15 } from "path";
6113
6055
 
6114
6056
  // src/hooks/directory-agents-injector/constants.ts
6115
6057
  import { join as join14 } from "path";
6116
- var OPENCODE_STORAGE3 = join14(xdgData ?? "", "opencode", "storage");
6058
+ var OPENCODE_STORAGE3 = getOpenCodeStorageDir();
6117
6059
  var AGENTS_INJECTOR_STORAGE = join14(OPENCODE_STORAGE3, "directory-agents");
6118
6060
  var AGENTS_FILENAME = "AGENTS.md";
6119
6061
 
@@ -6162,12 +6104,12 @@ function createDirectoryAgentsInjectorHook(ctx) {
6162
6104
  }
6163
6105
  return sessionCaches.get(sessionID);
6164
6106
  }
6165
- function resolveFilePath2(path5) {
6166
- if (!path5)
6107
+ function resolveFilePath2(path4) {
6108
+ if (!path4)
6167
6109
  return null;
6168
- if (path5.startsWith("/"))
6169
- return path5;
6170
- return resolve2(ctx.directory, path5);
6110
+ if (path4.startsWith("/"))
6111
+ return path4;
6112
+ return resolve2(ctx.directory, path4);
6171
6113
  }
6172
6114
  function findAgentsMdUp(startDir) {
6173
6115
  const found = [];
@@ -6285,7 +6227,7 @@ import { join as join18 } from "path";
6285
6227
 
6286
6228
  // src/hooks/directory-readme-injector/constants.ts
6287
6229
  import { join as join17 } from "path";
6288
- var OPENCODE_STORAGE4 = join17(xdgData ?? "", "opencode", "storage");
6230
+ var OPENCODE_STORAGE4 = getOpenCodeStorageDir();
6289
6231
  var README_INJECTOR_STORAGE = join17(OPENCODE_STORAGE4, "directory-readme");
6290
6232
  var README_FILENAME = "README.md";
6291
6233
 
@@ -6334,12 +6276,12 @@ function createDirectoryReadmeInjectorHook(ctx) {
6334
6276
  }
6335
6277
  return sessionCaches.get(sessionID);
6336
6278
  }
6337
- function resolveFilePath2(path5) {
6338
- if (!path5)
6279
+ function resolveFilePath2(path4) {
6280
+ if (!path4)
6339
6281
  return null;
6340
- if (path5.startsWith("/"))
6341
- return path5;
6342
- return resolve3(ctx.directory, path5);
6282
+ if (path4.startsWith("/"))
6283
+ return path4;
6284
+ return resolve3(ctx.directory, path4);
6343
6285
  }
6344
6286
  function findReadmeMdUp(startDir) {
6345
6287
  const found = [];
@@ -6478,7 +6420,8 @@ var TOKEN_LIMIT_KEYWORDS = [
6478
6420
  "token limit",
6479
6421
  "context length",
6480
6422
  "too many tokens",
6481
- "non-empty content"
6423
+ "non-empty content",
6424
+ "invalid_request_error"
6482
6425
  ];
6483
6426
  var MESSAGE_INDEX_PATTERN = /messages\.(\d+)/;
6484
6427
  function extractTokensFromMessage(message) {
@@ -6566,9 +6509,9 @@ function parseAnthropicTokenLimitError(err) {
6566
6509
  if (typeof responseBody === "string") {
6567
6510
  try {
6568
6511
  const jsonPatterns = [
6569
- /data:\s*(\{[\s\S]*?\})\s*$/m,
6570
- /(\{"type"\s*:\s*"error"[\s\S]*?\})/,
6571
- /(\{[\s\S]*?"error"[\s\S]*?\})/
6512
+ /data:\s*(\{[\s\S]*\})\s*$/m,
6513
+ /(\{"type"\s*:\s*"error"[\s\S]*\})/,
6514
+ /(\{[\s\S]*"error"[\s\S]*\})/
6572
6515
  ];
6573
6516
  for (const pattern of jsonPatterns) {
6574
6517
  const dataMatch = responseBody.match(pattern);
@@ -6655,7 +6598,6 @@ function estimateTokens2(text) {
6655
6598
  }
6656
6599
 
6657
6600
  // src/hooks/anthropic-auto-compact/pruning-deduplication.ts
6658
- var MESSAGE_STORAGE3 = join20(process.env.HOME || process.env.USERPROFILE || "", ".config", "opencode", "sessions");
6659
6601
  function createToolSignature(toolName, input) {
6660
6602
  const sortedInput = sortObject(input);
6661
6603
  return `${toolName}::${JSON.stringify(sortedInput)}`;
@@ -6675,13 +6617,13 @@ function sortObject(obj) {
6675
6617
  return sorted;
6676
6618
  }
6677
6619
  function getMessageDir3(sessionID) {
6678
- if (!existsSync15(MESSAGE_STORAGE3))
6620
+ if (!existsSync15(MESSAGE_STORAGE))
6679
6621
  return null;
6680
- const directPath = join20(MESSAGE_STORAGE3, sessionID);
6622
+ const directPath = join20(MESSAGE_STORAGE, sessionID);
6681
6623
  if (existsSync15(directPath))
6682
6624
  return directPath;
6683
- for (const dir of readdirSync4(MESSAGE_STORAGE3)) {
6684
- const sessionPath = join20(MESSAGE_STORAGE3, dir, sessionID);
6625
+ for (const dir of readdirSync4(MESSAGE_STORAGE)) {
6626
+ const sessionPath = join20(MESSAGE_STORAGE, dir, sessionID);
6685
6627
  if (existsSync15(sessionPath))
6686
6628
  return sessionPath;
6687
6629
  }
@@ -6793,15 +6735,14 @@ function findToolOutput(messages, callID) {
6793
6735
  // src/hooks/anthropic-auto-compact/pruning-supersede.ts
6794
6736
  import { existsSync as existsSync16, readdirSync as readdirSync5, readFileSync as readFileSync10 } from "fs";
6795
6737
  import { join as join21 } from "path";
6796
- var MESSAGE_STORAGE4 = join21(process.env.HOME || process.env.USERPROFILE || "", ".config", "opencode", "sessions");
6797
6738
  function getMessageDir4(sessionID) {
6798
- if (!existsSync16(MESSAGE_STORAGE4))
6739
+ if (!existsSync16(MESSAGE_STORAGE))
6799
6740
  return null;
6800
- const directPath = join21(MESSAGE_STORAGE4, sessionID);
6741
+ const directPath = join21(MESSAGE_STORAGE, sessionID);
6801
6742
  if (existsSync16(directPath))
6802
6743
  return directPath;
6803
- for (const dir of readdirSync5(MESSAGE_STORAGE4)) {
6804
- const sessionPath = join21(MESSAGE_STORAGE4, dir, sessionID);
6744
+ for (const dir of readdirSync5(MESSAGE_STORAGE)) {
6745
+ const sessionPath = join21(MESSAGE_STORAGE, dir, sessionID);
6805
6746
  if (existsSync16(sessionPath))
6806
6747
  return sessionPath;
6807
6748
  }
@@ -6956,15 +6897,14 @@ function findToolInput(messages, callID) {
6956
6897
  // src/hooks/anthropic-auto-compact/pruning-purge-errors.ts
6957
6898
  import { existsSync as existsSync17, readdirSync as readdirSync6, readFileSync as readFileSync11 } from "fs";
6958
6899
  import { join as join22 } from "path";
6959
- var MESSAGE_STORAGE5 = join22(process.env.HOME || process.env.USERPROFILE || "", ".config", "opencode", "sessions");
6960
6900
  function getMessageDir5(sessionID) {
6961
- if (!existsSync17(MESSAGE_STORAGE5))
6901
+ if (!existsSync17(MESSAGE_STORAGE))
6962
6902
  return null;
6963
- const directPath = join22(MESSAGE_STORAGE5, sessionID);
6903
+ const directPath = join22(MESSAGE_STORAGE, sessionID);
6964
6904
  if (existsSync17(directPath))
6965
6905
  return directPath;
6966
- for (const dir of readdirSync6(MESSAGE_STORAGE5)) {
6967
- const sessionPath = join22(MESSAGE_STORAGE5, dir, sessionID);
6906
+ for (const dir of readdirSync6(MESSAGE_STORAGE)) {
6907
+ const sessionPath = join22(MESSAGE_STORAGE, dir, sessionID);
6968
6908
  if (existsSync17(sessionPath))
6969
6909
  return sessionPath;
6970
6910
  }
@@ -7062,15 +7002,14 @@ function executePurgeErrors(sessionID, state2, config, protectedTools) {
7062
7002
  // src/hooks/anthropic-auto-compact/pruning-storage.ts
7063
7003
  import { existsSync as existsSync18, readdirSync as readdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
7064
7004
  import { join as join23 } from "path";
7065
- var MESSAGE_STORAGE6 = join23(process.env.HOME || process.env.USERPROFILE || "", ".config", "opencode", "sessions");
7066
7005
  function getMessageDir6(sessionID) {
7067
- if (!existsSync18(MESSAGE_STORAGE6))
7006
+ if (!existsSync18(MESSAGE_STORAGE))
7068
7007
  return null;
7069
- const directPath = join23(MESSAGE_STORAGE6, sessionID);
7008
+ const directPath = join23(MESSAGE_STORAGE, sessionID);
7070
7009
  if (existsSync18(directPath))
7071
7010
  return directPath;
7072
- for (const dir of readdirSync7(MESSAGE_STORAGE6)) {
7073
- const sessionPath = join23(MESSAGE_STORAGE6, dir, sessionID);
7011
+ for (const dir of readdirSync7(MESSAGE_STORAGE)) {
7012
+ const sessionPath = join23(MESSAGE_STORAGE, dir, sessionID);
7074
7013
  if (existsSync18(sessionPath))
7075
7014
  return sessionPath;
7076
7015
  }
@@ -7212,27 +7151,20 @@ async function executeDynamicContextPruning(sessionID, config, client) {
7212
7151
 
7213
7152
  // src/hooks/anthropic-auto-compact/storage.ts
7214
7153
  import { existsSync as existsSync19, readdirSync as readdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
7215
- import { homedir as homedir6 } from "os";
7216
7154
  import { join as join24 } from "path";
7217
- var OPENCODE_STORAGE5 = join24(xdgData ?? "", "opencode", "storage");
7218
- if (process.platform === "darwin" && !existsSync19(OPENCODE_STORAGE5)) {
7219
- const localShare = join24(homedir6(), ".local", "share", "opencode", "storage");
7220
- if (existsSync19(localShare)) {
7221
- OPENCODE_STORAGE5 = localShare;
7222
- }
7223
- }
7224
- var MESSAGE_STORAGE7 = join24(OPENCODE_STORAGE5, "message");
7155
+ var OPENCODE_STORAGE5 = getOpenCodeStorageDir();
7156
+ var MESSAGE_STORAGE3 = join24(OPENCODE_STORAGE5, "message");
7225
7157
  var PART_STORAGE3 = join24(OPENCODE_STORAGE5, "part");
7226
7158
  var TRUNCATION_MESSAGE = "[TOOL RESULT TRUNCATED - Context limit exceeded. Original output was too large and has been truncated to recover the session. Please re-run this tool if you need the full output.]";
7227
7159
  function getMessageDir7(sessionID) {
7228
- if (!existsSync19(MESSAGE_STORAGE7))
7160
+ if (!existsSync19(MESSAGE_STORAGE3))
7229
7161
  return "";
7230
- const directPath = join24(MESSAGE_STORAGE7, sessionID);
7162
+ const directPath = join24(MESSAGE_STORAGE3, sessionID);
7231
7163
  if (existsSync19(directPath)) {
7232
7164
  return directPath;
7233
7165
  }
7234
- for (const dir of readdirSync8(MESSAGE_STORAGE7)) {
7235
- const sessionPath = join24(MESSAGE_STORAGE7, dir, sessionID);
7166
+ for (const dir of readdirSync8(MESSAGE_STORAGE3)) {
7167
+ const sessionPath = join24(MESSAGE_STORAGE3, dir, sessionID);
7236
7168
  if (existsSync19(sessionPath)) {
7237
7169
  return sessionPath;
7238
7170
  }
@@ -7360,6 +7292,7 @@ function truncateUntilTargetTokens(sessionID, currentTokens, maxTokens, targetRa
7360
7292
  }
7361
7293
 
7362
7294
  // src/hooks/anthropic-auto-compact/executor.ts
7295
+ var PLACEHOLDER_TEXT = "[user interrupted]";
7363
7296
  function getOrCreateRetryState(autoCompactState, sessionID) {
7364
7297
  let state2 = autoCompactState.retryStateBySession.get(sessionID);
7365
7298
  if (!state2) {
@@ -7392,6 +7325,32 @@ function getOrCreateDcpState(autoCompactState, sessionID) {
7392
7325
  }
7393
7326
  return state2;
7394
7327
  }
7328
+ function sanitizeEmptyMessagesBeforeSummarize(sessionID) {
7329
+ const emptyMessageIds = findEmptyMessages(sessionID);
7330
+ if (emptyMessageIds.length === 0) {
7331
+ return 0;
7332
+ }
7333
+ let fixedCount = 0;
7334
+ for (const messageID of emptyMessageIds) {
7335
+ const replaced = replaceEmptyTextParts(messageID, PLACEHOLDER_TEXT);
7336
+ if (replaced) {
7337
+ fixedCount++;
7338
+ } else {
7339
+ const injected = injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT);
7340
+ if (injected) {
7341
+ fixedCount++;
7342
+ }
7343
+ }
7344
+ }
7345
+ if (fixedCount > 0) {
7346
+ log("[auto-compact] pre-summarize sanitization fixed empty messages", {
7347
+ sessionID,
7348
+ fixedCount,
7349
+ totalEmpty: emptyMessageIds.length
7350
+ });
7351
+ }
7352
+ return fixedCount;
7353
+ }
7395
7354
  async function getLastMessagePair(sessionID, client, directory) {
7396
7355
  try {
7397
7356
  const resp = await client.session.messages({
@@ -7547,6 +7506,77 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
7547
7506
  try {
7548
7507
  const errorData = autoCompactState.errorDataBySession.get(sessionID);
7549
7508
  const truncateState = getOrCreateTruncateState(autoCompactState, sessionID);
7509
+ const dcpState = getOrCreateDcpState(autoCompactState, sessionID);
7510
+ if (experimental?.dcp_for_compaction && !dcpState.attempted && errorData?.currentTokens && errorData?.maxTokens && errorData.currentTokens > errorData.maxTokens) {
7511
+ dcpState.attempted = true;
7512
+ log("[auto-compact] DCP triggered FIRST on token limit error", {
7513
+ sessionID,
7514
+ currentTokens: errorData.currentTokens,
7515
+ maxTokens: errorData.maxTokens
7516
+ });
7517
+ const dcpConfig = experimental.dynamic_context_pruning ?? {
7518
+ enabled: true,
7519
+ notification: "detailed",
7520
+ protected_tools: ["task", "todowrite", "todoread", "lsp_rename", "lsp_code_action_resolve"]
7521
+ };
7522
+ try {
7523
+ const pruningResult = await executeDynamicContextPruning(sessionID, dcpConfig, client);
7524
+ if (pruningResult.itemsPruned > 0) {
7525
+ dcpState.itemsPruned = pruningResult.itemsPruned;
7526
+ log("[auto-compact] DCP successful, proceeding to compaction", {
7527
+ itemsPruned: pruningResult.itemsPruned,
7528
+ tokensSaved: pruningResult.totalTokensSaved
7529
+ });
7530
+ await client.tui.showToast({
7531
+ body: {
7532
+ title: "Dynamic Context Pruning",
7533
+ message: `Pruned ${pruningResult.itemsPruned} items (~${Math.round(pruningResult.totalTokensSaved / 1000)}k tokens). Running compaction...`,
7534
+ variant: "success",
7535
+ duration: 3000
7536
+ }
7537
+ }).catch(() => {});
7538
+ const providerID = msg.providerID;
7539
+ const modelID = msg.modelID;
7540
+ if (providerID && modelID) {
7541
+ try {
7542
+ sanitizeEmptyMessagesBeforeSummarize(sessionID);
7543
+ await client.tui.showToast({
7544
+ body: {
7545
+ title: "Auto Compact",
7546
+ message: "Summarizing session after DCP...",
7547
+ variant: "warning",
7548
+ duration: 3000
7549
+ }
7550
+ }).catch(() => {});
7551
+ await client.session.summarize({
7552
+ path: { id: sessionID },
7553
+ body: { providerID, modelID },
7554
+ query: { directory }
7555
+ });
7556
+ clearSessionState(autoCompactState, sessionID);
7557
+ setTimeout(async () => {
7558
+ try {
7559
+ await client.session.prompt_async({
7560
+ path: { sessionID },
7561
+ body: { parts: [{ type: "text", text: "Continue" }] },
7562
+ query: { directory }
7563
+ });
7564
+ } catch {}
7565
+ }, 500);
7566
+ return;
7567
+ } catch (summarizeError) {
7568
+ log("[auto-compact] summarize after DCP failed, continuing recovery", {
7569
+ error: String(summarizeError)
7570
+ });
7571
+ }
7572
+ }
7573
+ } else {
7574
+ log("[auto-compact] DCP did not prune any items", { sessionID });
7575
+ }
7576
+ } catch (error) {
7577
+ log("[auto-compact] DCP failed", { error: String(error) });
7578
+ }
7579
+ }
7550
7580
  if (experimental?.aggressive_truncation && errorData?.currentTokens && errorData?.maxTokens && errorData.currentTokens > errorData.maxTokens && truncateState.truncateAttempt < TRUNCATE_CONFIG.maxTruncateAttempts) {
7551
7581
  log("[auto-compact] aggressive truncation triggered (experimental)", {
7552
7582
  currentTokens: errorData.currentTokens,
@@ -7673,6 +7703,7 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
7673
7703
  const modelID = msg.modelID;
7674
7704
  if (providerID && modelID) {
7675
7705
  try {
7706
+ sanitizeEmptyMessagesBeforeSummarize(sessionID);
7676
7707
  await client.tui.showToast({
7677
7708
  body: {
7678
7709
  title: "Auto Compact",
@@ -7715,45 +7746,6 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
7715
7746
  }).catch(() => {});
7716
7747
  }
7717
7748
  }
7718
- const dcpState = getOrCreateDcpState(autoCompactState, sessionID);
7719
- if (experimental?.dcp_on_compaction_failure && !dcpState.attempted) {
7720
- dcpState.attempted = true;
7721
- log("[auto-compact] attempting DCP after summarize failed", { sessionID });
7722
- const dcpConfig = experimental.dynamic_context_pruning ?? {
7723
- enabled: true,
7724
- notification: "detailed",
7725
- protected_tools: ["task", "todowrite", "todoread", "lsp_rename", "lsp_code_action_resolve"]
7726
- };
7727
- try {
7728
- const pruningResult = await executeDynamicContextPruning(sessionID, dcpConfig, client);
7729
- if (pruningResult.itemsPruned > 0) {
7730
- dcpState.itemsPruned = pruningResult.itemsPruned;
7731
- log("[auto-compact] DCP successful, retrying compaction", {
7732
- itemsPruned: pruningResult.itemsPruned,
7733
- tokensSaved: pruningResult.totalTokensSaved
7734
- });
7735
- await client.tui.showToast({
7736
- body: {
7737
- title: "Dynamic Context Pruning",
7738
- message: `Pruned ${pruningResult.itemsPruned} items (~${Math.round(pruningResult.totalTokensSaved / 1000)}k tokens). Retrying compaction...`,
7739
- variant: "success",
7740
- duration: 3000
7741
- }
7742
- }).catch(() => {});
7743
- retryState.attempt = 0;
7744
- setTimeout(() => {
7745
- executeCompact(sessionID, msg, autoCompactState, client, directory, experimental);
7746
- }, 500);
7747
- return;
7748
- } else {
7749
- log("[auto-compact] DCP did not prune any items, continuing to revert", { sessionID });
7750
- }
7751
- } catch (error) {
7752
- log("[auto-compact] DCP failed, continuing to revert", {
7753
- error: String(error)
7754
- });
7755
- }
7756
- }
7757
7749
  const fallbackState = getOrCreateFallbackState(autoCompactState, sessionID);
7758
7750
  if (fallbackState.revertAttempt < FALLBACK_CONFIG.maxRevertAttempts) {
7759
7751
  const pair = await getLastMessagePair(sessionID, client, directory);
@@ -8506,21 +8498,21 @@ async function loadClaudeHooksConfig(customSettingsPath) {
8506
8498
 
8507
8499
  // src/hooks/claude-code-hooks/config-loader.ts
8508
8500
  import { existsSync as existsSync22 } from "fs";
8509
- import { homedir as homedir7 } from "os";
8501
+ import { homedir as homedir6 } from "os";
8510
8502
  import { join as join27 } from "path";
8511
- var USER_CONFIG_PATH = join27(homedir7(), ".config", "opencode", "opencode-cc-plugin.json");
8503
+ var USER_CONFIG_PATH = join27(homedir6(), ".config", "opencode", "opencode-cc-plugin.json");
8512
8504
  function getProjectConfigPath() {
8513
8505
  return join27(process.cwd(), ".opencode", "opencode-cc-plugin.json");
8514
8506
  }
8515
- async function loadConfigFromPath(path5) {
8516
- if (!existsSync22(path5)) {
8507
+ async function loadConfigFromPath(path4) {
8508
+ if (!existsSync22(path4)) {
8517
8509
  return null;
8518
8510
  }
8519
8511
  try {
8520
- const content = await Bun.file(path5).text();
8512
+ const content = await Bun.file(path4).text();
8521
8513
  return JSON.parse(content);
8522
8514
  } catch (error) {
8523
- log("Failed to load config", { path: path5, error });
8515
+ log("Failed to load config", { path: path4, error });
8524
8516
  return null;
8525
8517
  }
8526
8518
  }
@@ -8708,10 +8700,10 @@ function ensureTranscriptDir() {
8708
8700
  }
8709
8701
  function appendTranscriptEntry(sessionId, entry) {
8710
8702
  ensureTranscriptDir();
8711
- const path5 = getTranscriptPath(sessionId);
8703
+ const path4 = getTranscriptPath(sessionId);
8712
8704
  const line = JSON.stringify(entry) + `
8713
8705
  `;
8714
- appendFileSync5(path5, line);
8706
+ appendFileSync5(path4, line);
8715
8707
  }
8716
8708
  function recordToolUse(sessionId, toolName, toolInput) {
8717
8709
  appendTranscriptEntry(sessionId, {
@@ -8818,11 +8810,11 @@ async function buildTranscriptFromSession(client, sessionId, directory, currentT
8818
8810
  }
8819
8811
  }
8820
8812
  }
8821
- function deleteTempTranscript(path5) {
8822
- if (!path5)
8813
+ function deleteTempTranscript(path4) {
8814
+ if (!path4)
8823
8815
  return;
8824
8816
  try {
8825
- unlinkSync5(path5);
8817
+ unlinkSync5(path4);
8826
8818
  } catch {}
8827
8819
  }
8828
8820
 
@@ -9449,7 +9441,7 @@ ${result.message}`;
9449
9441
  }
9450
9442
  // src/hooks/rules-injector/index.ts
9451
9443
  import { readFileSync as readFileSync15 } from "fs";
9452
- import { homedir as homedir8 } from "os";
9444
+ import { homedir as homedir7 } from "os";
9453
9445
  import { relative as relative3, resolve as resolve4 } from "path";
9454
9446
 
9455
9447
  // src/hooks/rules-injector/finder.ts
@@ -9463,7 +9455,7 @@ import { dirname as dirname4, join as join31, relative } from "path";
9463
9455
 
9464
9456
  // src/hooks/rules-injector/constants.ts
9465
9457
  import { join as join30 } from "path";
9466
- var OPENCODE_STORAGE6 = join30(xdgData ?? "", "opencode", "storage");
9458
+ var OPENCODE_STORAGE6 = getOpenCodeStorageDir();
9467
9459
  var RULES_INJECTOR_STORAGE = join30(OPENCODE_STORAGE6, "rules-injector");
9468
9460
  var PROJECT_MARKERS = [
9469
9461
  ".git",
@@ -9804,12 +9796,12 @@ function createRulesInjectorHook(ctx) {
9804
9796
  }
9805
9797
  return sessionCaches.get(sessionID);
9806
9798
  }
9807
- function resolveFilePath2(path5) {
9808
- if (!path5)
9799
+ function resolveFilePath2(path4) {
9800
+ if (!path4)
9809
9801
  return null;
9810
- if (path5.startsWith("/"))
9811
- return path5;
9812
- return resolve4(ctx.directory, path5);
9802
+ if (path4.startsWith("/"))
9803
+ return path4;
9804
+ return resolve4(ctx.directory, path4);
9813
9805
  }
9814
9806
  async function processFilePathForInjection(filePath, sessionID, output) {
9815
9807
  const resolved = resolveFilePath2(filePath);
@@ -9817,7 +9809,7 @@ function createRulesInjectorHook(ctx) {
9817
9809
  return;
9818
9810
  const projectRoot = findProjectRoot(resolved);
9819
9811
  const cache2 = getSessionCache(sessionID);
9820
- const home = homedir8();
9812
+ const home = homedir7();
9821
9813
  const ruleFileCandidates = findRuleFiles(projectRoot, home, resolved);
9822
9814
  const toInject = [];
9823
9815
  for (const candidate of ruleFileCandidates) {
@@ -9932,33 +9924,33 @@ function createBackgroundNotificationHook(manager) {
9932
9924
  }
9933
9925
  // src/hooks/auto-update-checker/checker.ts
9934
9926
  import * as fs5 from "fs";
9935
- import * as path6 from "path";
9927
+ import * as path5 from "path";
9936
9928
  import { fileURLToPath } from "url";
9937
9929
 
9938
9930
  // src/hooks/auto-update-checker/constants.ts
9939
- import * as path5 from "path";
9940
- import * as os5 from "os";
9931
+ import * as path4 from "path";
9932
+ import * as os4 from "os";
9941
9933
  var PACKAGE_NAME = "oh-my-opencode";
9942
9934
  var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
9943
9935
  var NPM_FETCH_TIMEOUT = 5000;
9944
9936
  function getCacheDir2() {
9945
9937
  if (process.platform === "win32") {
9946
- return path5.join(process.env.LOCALAPPDATA ?? os5.homedir(), "opencode");
9938
+ return path4.join(process.env.LOCALAPPDATA ?? os4.homedir(), "opencode");
9947
9939
  }
9948
- return path5.join(os5.homedir(), ".cache", "opencode");
9940
+ return path4.join(os4.homedir(), ".cache", "opencode");
9949
9941
  }
9950
9942
  var CACHE_DIR = getCacheDir2();
9951
- var VERSION_FILE = path5.join(CACHE_DIR, "version");
9952
- var INSTALLED_PACKAGE_JSON = path5.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
9943
+ var VERSION_FILE = path4.join(CACHE_DIR, "version");
9944
+ var INSTALLED_PACKAGE_JSON = path4.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
9953
9945
  function getUserConfigDir2() {
9954
9946
  if (process.platform === "win32") {
9955
- return process.env.APPDATA ?? path5.join(os5.homedir(), "AppData", "Roaming");
9947
+ return process.env.APPDATA ?? path4.join(os4.homedir(), "AppData", "Roaming");
9956
9948
  }
9957
- return process.env.XDG_CONFIG_HOME ?? path5.join(os5.homedir(), ".config");
9949
+ return process.env.XDG_CONFIG_HOME ?? path4.join(os4.homedir(), ".config");
9958
9950
  }
9959
9951
  var USER_CONFIG_DIR = getUserConfigDir2();
9960
- var USER_OPENCODE_CONFIG = path5.join(USER_CONFIG_DIR, "opencode", "opencode.json");
9961
- var USER_OPENCODE_CONFIG_JSONC = path5.join(USER_CONFIG_DIR, "opencode", "opencode.jsonc");
9952
+ var USER_OPENCODE_CONFIG = path4.join(USER_CONFIG_DIR, "opencode", "opencode.json");
9953
+ var USER_OPENCODE_CONFIG_JSONC = path4.join(USER_CONFIG_DIR, "opencode", "opencode.jsonc");
9962
9954
 
9963
9955
  // src/hooks/auto-update-checker/checker.ts
9964
9956
  function stripJsonComments(json) {
@@ -9966,8 +9958,8 @@ function stripJsonComments(json) {
9966
9958
  }
9967
9959
  function getConfigPaths(directory) {
9968
9960
  return [
9969
- path6.join(directory, ".opencode", "opencode.json"),
9970
- path6.join(directory, ".opencode", "opencode.jsonc"),
9961
+ path5.join(directory, ".opencode", "opencode.json"),
9962
+ path5.join(directory, ".opencode", "opencode.jsonc"),
9971
9963
  USER_OPENCODE_CONFIG,
9972
9964
  USER_OPENCODE_CONFIG_JSONC
9973
9965
  ];
@@ -9998,9 +9990,9 @@ function getLocalDevPath(directory) {
9998
9990
  function findPackageJsonUp(startPath) {
9999
9991
  try {
10000
9992
  const stat = fs5.statSync(startPath);
10001
- let dir = stat.isDirectory() ? startPath : path6.dirname(startPath);
9993
+ let dir = stat.isDirectory() ? startPath : path5.dirname(startPath);
10002
9994
  for (let i = 0;i < 10; i++) {
10003
- const pkgPath = path6.join(dir, "package.json");
9995
+ const pkgPath = path5.join(dir, "package.json");
10004
9996
  if (fs5.existsSync(pkgPath)) {
10005
9997
  try {
10006
9998
  const content = fs5.readFileSync(pkgPath, "utf-8");
@@ -10009,7 +10001,7 @@ function findPackageJsonUp(startPath) {
10009
10001
  return pkgPath;
10010
10002
  } catch {}
10011
10003
  }
10012
- const parent = path6.dirname(dir);
10004
+ const parent = path5.dirname(dir);
10013
10005
  if (parent === dir)
10014
10006
  break;
10015
10007
  dir = parent;
@@ -10066,7 +10058,7 @@ function getCachedVersion() {
10066
10058
  }
10067
10059
  } catch {}
10068
10060
  try {
10069
- const currentDir = path6.dirname(fileURLToPath(import.meta.url));
10061
+ const currentDir = path5.dirname(fileURLToPath(import.meta.url));
10070
10062
  const pkgPath = findPackageJsonUp(currentDir);
10071
10063
  if (pkgPath) {
10072
10064
  const content = fs5.readFileSync(pkgPath, "utf-8");
@@ -10142,12 +10134,12 @@ async function getLatestVersion() {
10142
10134
 
10143
10135
  // src/hooks/auto-update-checker/cache.ts
10144
10136
  import * as fs6 from "fs";
10145
- import * as path7 from "path";
10137
+ import * as path6 from "path";
10146
10138
  function stripTrailingCommas(json) {
10147
10139
  return json.replace(/,(\s*[}\]])/g, "$1");
10148
10140
  }
10149
10141
  function removeFromBunLock(packageName) {
10150
- const lockPath = path7.join(CACHE_DIR, "bun.lock");
10142
+ const lockPath = path6.join(CACHE_DIR, "bun.lock");
10151
10143
  if (!fs6.existsSync(lockPath))
10152
10144
  return false;
10153
10145
  try {
@@ -10173,8 +10165,8 @@ function removeFromBunLock(packageName) {
10173
10165
  }
10174
10166
  function invalidatePackage(packageName = PACKAGE_NAME) {
10175
10167
  try {
10176
- const pkgDir = path7.join(CACHE_DIR, "node_modules", packageName);
10177
- const pkgJsonPath = path7.join(CACHE_DIR, "package.json");
10168
+ const pkgDir = path6.join(CACHE_DIR, "node_modules", packageName);
10169
+ const pkgJsonPath = path6.join(CACHE_DIR, "package.json");
10178
10170
  let packageRemoved = false;
10179
10171
  let dependencyRemoved = false;
10180
10172
  let lockRemoved = false;
@@ -10372,7 +10364,7 @@ import { join as join37 } from "path";
10372
10364
 
10373
10365
  // src/hooks/agent-usage-reminder/constants.ts
10374
10366
  import { join as join36 } from "path";
10375
- var OPENCODE_STORAGE7 = join36(xdgData ?? "", "opencode", "storage");
10367
+ var OPENCODE_STORAGE7 = getOpenCodeStorageDir();
10376
10368
  var AGENT_USAGE_REMINDER_STORAGE = join36(OPENCODE_STORAGE7, "agent-usage-reminder");
10377
10369
  var TARGET_TOOLS = new Set([
10378
10370
  "grep",
@@ -10756,7 +10748,7 @@ import { join as join39 } from "path";
10756
10748
 
10757
10749
  // src/hooks/interactive-bash-session/constants.ts
10758
10750
  import { join as join38 } from "path";
10759
- var OPENCODE_STORAGE8 = join38(xdgData ?? "", "opencode", "storage");
10751
+ var OPENCODE_STORAGE8 = getOpenCodeStorageDir();
10760
10752
  var INTERACTIVE_BASH_SESSION_STORAGE = join38(OPENCODE_STORAGE8, "interactive-bash-session");
10761
10753
  var OMO_SESSION_PREFIX = "omo-";
10762
10754
  function buildSessionReminderMessage(sessions) {
@@ -10979,7 +10971,7 @@ function createInteractiveBashSessionHook(_ctx) {
10979
10971
  };
10980
10972
  }
10981
10973
  // src/hooks/empty-message-sanitizer/index.ts
10982
- var PLACEHOLDER_TEXT = "[user interrupted]";
10974
+ var PLACEHOLDER_TEXT2 = "[user interrupted]";
10983
10975
  function hasTextContent(part) {
10984
10976
  if (part.type === "text") {
10985
10977
  const text = part.text;
@@ -10998,8 +10990,11 @@ function createEmptyMessageSanitizerHook() {
10998
10990
  return {
10999
10991
  "experimental.chat.messages.transform": async (_input, output) => {
11000
10992
  const { messages } = output;
11001
- for (const message of messages) {
11002
- if (message.info.role === "user")
10993
+ for (let i = 0;i < messages.length; i++) {
10994
+ const message = messages[i];
10995
+ const isLastMessage = i === messages.length - 1;
10996
+ const isAssistant = message.info.role === "assistant";
10997
+ if (isLastMessage && isAssistant)
11003
10998
  continue;
11004
10999
  const parts = message.parts;
11005
11000
  if (!hasValidContent(parts)) {
@@ -11008,7 +11003,7 @@ function createEmptyMessageSanitizerHook() {
11008
11003
  if (part.type === "text") {
11009
11004
  const textPart = part;
11010
11005
  if (!textPart.text || !textPart.text.trim()) {
11011
- textPart.text = PLACEHOLDER_TEXT;
11006
+ textPart.text = PLACEHOLDER_TEXT2;
11012
11007
  textPart.synthetic = true;
11013
11008
  injected = true;
11014
11009
  break;
@@ -11022,7 +11017,7 @@ function createEmptyMessageSanitizerHook() {
11022
11017
  messageID: message.info.id,
11023
11018
  sessionID: message.info.sessionID ?? "",
11024
11019
  type: "text",
11025
- text: PLACEHOLDER_TEXT,
11020
+ text: PLACEHOLDER_TEXT2,
11026
11021
  synthetic: true
11027
11022
  };
11028
11023
  if (insertIndex === -1) {
@@ -11036,7 +11031,7 @@ function createEmptyMessageSanitizerHook() {
11036
11031
  if (part.type === "text") {
11037
11032
  const textPart = part;
11038
11033
  if (textPart.text !== undefined && textPart.text.trim() === "") {
11039
- textPart.text = PLACEHOLDER_TEXT;
11034
+ textPart.text = PLACEHOLDER_TEXT2;
11040
11035
  textPart.synthetic = true;
11041
11036
  }
11042
11037
  }
@@ -11055,12 +11050,12 @@ function isExtendedThinkingModel(modelID) {
11055
11050
  }
11056
11051
  return lower.includes("claude-sonnet-4") || lower.includes("claude-opus-4") || lower.includes("claude-3");
11057
11052
  }
11058
- function hasToolParts(parts) {
11053
+ function hasContentParts(parts) {
11059
11054
  if (!parts || parts.length === 0)
11060
11055
  return false;
11061
11056
  return parts.some((part) => {
11062
11057
  const type = part.type;
11063
- return type === "tool" || type === "tool_use";
11058
+ return type === "tool" || type === "tool_use" || type === "text";
11064
11059
  });
11065
11060
  }
11066
11061
  function startsWithThinkingBlock(parts) {
@@ -11119,7 +11114,7 @@ function createThinkingBlockValidatorHook() {
11119
11114
  const msg = messages[i];
11120
11115
  if (msg.info.role !== "assistant")
11121
11116
  continue;
11122
- if (hasToolParts(msg.parts) && !startsWithThinkingBlock(msg.parts)) {
11117
+ if (hasContentParts(msg.parts) && !startsWithThinkingBlock(msg.parts)) {
11123
11118
  const previousThinking = findPreviousThinkingContent(messages, i);
11124
11119
  const thinkingContent = previousThinking || "[Continuing from previous reasoning]";
11125
11120
  prependThinkingBlock(msg, thinkingContent);
@@ -11392,10 +11387,66 @@ function startCallbackServer(timeoutMs = 5 * 60 * 1000) {
11392
11387
  };
11393
11388
  }
11394
11389
  // src/auth/antigravity/token.ts
11390
+ class AntigravityTokenRefreshError extends Error {
11391
+ code;
11392
+ description;
11393
+ status;
11394
+ statusText;
11395
+ responseBody;
11396
+ constructor(options) {
11397
+ super(options.message);
11398
+ this.name = "AntigravityTokenRefreshError";
11399
+ this.code = options.code;
11400
+ this.description = options.description;
11401
+ this.status = options.status;
11402
+ this.statusText = options.statusText;
11403
+ this.responseBody = options.responseBody;
11404
+ }
11405
+ get isInvalidGrant() {
11406
+ return this.code === "invalid_grant";
11407
+ }
11408
+ get isNetworkError() {
11409
+ return this.status === 0;
11410
+ }
11411
+ }
11412
+ function parseOAuthErrorPayload(text) {
11413
+ if (!text) {
11414
+ return {};
11415
+ }
11416
+ try {
11417
+ const payload = JSON.parse(text);
11418
+ let code;
11419
+ if (typeof payload.error === "string") {
11420
+ code = payload.error;
11421
+ } else if (payload.error && typeof payload.error === "object") {
11422
+ code = payload.error.status ?? payload.error.code;
11423
+ }
11424
+ return {
11425
+ code,
11426
+ description: payload.error_description
11427
+ };
11428
+ } catch {
11429
+ return { description: text };
11430
+ }
11431
+ }
11395
11432
  function isTokenExpired(tokens) {
11396
11433
  const expirationTime = tokens.timestamp + tokens.expires_in * 1000;
11397
11434
  return Date.now() >= expirationTime - ANTIGRAVITY_TOKEN_REFRESH_BUFFER_MS;
11398
11435
  }
11436
+ var MAX_REFRESH_RETRIES = 3;
11437
+ var INITIAL_RETRY_DELAY_MS = 1000;
11438
+ function calculateRetryDelay(attempt) {
11439
+ return Math.min(INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt), 1e4);
11440
+ }
11441
+ function isRetryableError(status) {
11442
+ if (status === 0)
11443
+ return true;
11444
+ if (status === 429)
11445
+ return true;
11446
+ if (status >= 500 && status < 600)
11447
+ return true;
11448
+ return false;
11449
+ }
11399
11450
  async function refreshAccessToken(refreshToken, clientId = ANTIGRAVITY_CLIENT_ID, clientSecret = ANTIGRAVITY_CLIENT_SECRET) {
11400
11451
  const params = new URLSearchParams({
11401
11452
  grant_type: "refresh_token",
@@ -11403,24 +11454,67 @@ async function refreshAccessToken(refreshToken, clientId = ANTIGRAVITY_CLIENT_ID
11403
11454
  client_id: clientId,
11404
11455
  client_secret: clientSecret
11405
11456
  });
11406
- const response = await fetch(GOOGLE_TOKEN_URL, {
11407
- method: "POST",
11408
- headers: {
11409
- "Content-Type": "application/x-www-form-urlencoded"
11410
- },
11411
- body: params
11412
- });
11413
- if (!response.ok) {
11414
- const errorText = await response.text().catch(() => "Unknown error");
11415
- throw new Error(`Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`);
11457
+ let lastError;
11458
+ for (let attempt = 0;attempt <= MAX_REFRESH_RETRIES; attempt++) {
11459
+ try {
11460
+ const response = await fetch(GOOGLE_TOKEN_URL, {
11461
+ method: "POST",
11462
+ headers: {
11463
+ "Content-Type": "application/x-www-form-urlencoded"
11464
+ },
11465
+ body: params
11466
+ });
11467
+ if (response.ok) {
11468
+ const data = await response.json();
11469
+ return {
11470
+ access_token: data.access_token,
11471
+ refresh_token: data.refresh_token || refreshToken,
11472
+ expires_in: data.expires_in,
11473
+ token_type: data.token_type
11474
+ };
11475
+ }
11476
+ const responseBody = await response.text().catch(() => {
11477
+ return;
11478
+ });
11479
+ const parsed = parseOAuthErrorPayload(responseBody);
11480
+ lastError = new AntigravityTokenRefreshError({
11481
+ message: parsed.description || `Token refresh failed: ${response.status} ${response.statusText}`,
11482
+ code: parsed.code,
11483
+ description: parsed.description,
11484
+ status: response.status,
11485
+ statusText: response.statusText,
11486
+ responseBody
11487
+ });
11488
+ if (parsed.code === "invalid_grant") {
11489
+ throw lastError;
11490
+ }
11491
+ if (!isRetryableError(response.status)) {
11492
+ throw lastError;
11493
+ }
11494
+ if (attempt < MAX_REFRESH_RETRIES) {
11495
+ const delay = calculateRetryDelay(attempt);
11496
+ await new Promise((resolve5) => setTimeout(resolve5, delay));
11497
+ }
11498
+ } catch (error) {
11499
+ if (error instanceof AntigravityTokenRefreshError) {
11500
+ throw error;
11501
+ }
11502
+ lastError = new AntigravityTokenRefreshError({
11503
+ message: error instanceof Error ? error.message : "Network error during token refresh",
11504
+ status: 0,
11505
+ statusText: "Network Error"
11506
+ });
11507
+ if (attempt < MAX_REFRESH_RETRIES) {
11508
+ const delay = calculateRetryDelay(attempt);
11509
+ await new Promise((resolve5) => setTimeout(resolve5, delay));
11510
+ }
11511
+ }
11416
11512
  }
11417
- const data = await response.json();
11418
- return {
11419
- access_token: data.access_token,
11420
- refresh_token: data.refresh_token || refreshToken,
11421
- expires_in: data.expires_in,
11422
- token_type: data.token_type
11423
- };
11513
+ throw lastError || new AntigravityTokenRefreshError({
11514
+ message: "Token refresh failed after all retries",
11515
+ status: 0,
11516
+ statusText: "Max Retries Exceeded"
11517
+ });
11424
11518
  }
11425
11519
  function parseStoredToken(stored) {
11426
11520
  const parts = stored.split("|");
@@ -11645,6 +11739,10 @@ function clearProjectContextCache(accessToken) {
11645
11739
  projectContextCache.clear();
11646
11740
  }
11647
11741
  }
11742
+ function invalidateProjectContextByRefreshToken(_refreshToken) {
11743
+ projectContextCache.clear();
11744
+ debugLog4(`[invalidateProjectContextByRefreshToken] Cleared all project context cache due to refresh token invalidation`);
11745
+ }
11648
11746
  // src/auth/antigravity/request.ts
11649
11747
  function buildRequestHeaders(accessToken) {
11650
11748
  return {
@@ -12359,7 +12457,7 @@ function debugLog7(message) {
12359
12457
  console.log(`[antigravity-fetch] ${message}`);
12360
12458
  }
12361
12459
  }
12362
- function isRetryableError(status) {
12460
+ function isRetryableError2(status) {
12363
12461
  if (status === 0)
12364
12462
  return true;
12365
12463
  if (status === 429)
@@ -12377,11 +12475,11 @@ var GCP_PERMISSION_ERROR_PATTERNS = [
12377
12475
  function isGcpPermissionError(text) {
12378
12476
  return GCP_PERMISSION_ERROR_PATTERNS.some((pattern) => text.includes(pattern));
12379
12477
  }
12380
- function calculateRetryDelay(attempt) {
12478
+ function calculateRetryDelay2(attempt) {
12381
12479
  return Math.min(200 * Math.pow(2, attempt), 2000);
12382
12480
  }
12383
12481
  async function isRetryableResponse(response) {
12384
- if (isRetryableError(response.status))
12482
+ if (isRetryableError2(response.status))
12385
12483
  return true;
12386
12484
  if (response.status === 403) {
12387
12485
  try {
@@ -12460,7 +12558,7 @@ async function attemptFetch(options) {
12460
12558
  const text = await response.clone().text();
12461
12559
  if (isGcpPermissionError(text)) {
12462
12560
  if (attempt < maxPermissionRetries) {
12463
- const delay = calculateRetryDelay(attempt);
12561
+ const delay = calculateRetryDelay2(attempt);
12464
12562
  debugLog7(`[RETRY] GCP permission error, retry ${attempt + 1}/${maxPermissionRetries} after ${delay}ms`);
12465
12563
  await new Promise((resolve5) => setTimeout(resolve5, delay));
12466
12564
  continue;
@@ -12581,6 +12679,14 @@ function createAntigravityFetch(getAuth, client, providerId, clientId, clientSec
12581
12679
  });
12582
12680
  debugLog7("Token refreshed successfully");
12583
12681
  } catch (error) {
12682
+ if (error instanceof AntigravityTokenRefreshError) {
12683
+ if (error.isInvalidGrant) {
12684
+ debugLog7(`[REFRESH] Token revoked (invalid_grant), clearing caches`);
12685
+ invalidateProjectContextByRefreshToken(refreshParts.refreshToken);
12686
+ clearProjectContextCache();
12687
+ }
12688
+ throw new Error(`Antigravity: Token refresh failed: ${error.description || error.message}${error.code ? ` (${error.code})` : ""}`);
12689
+ }
12584
12690
  throw new Error(`Antigravity: Token refresh failed: ${error instanceof Error ? error.message : "Unknown error"}`);
12585
12691
  }
12586
12692
  }
@@ -12662,10 +12768,29 @@ function createAntigravityFetch(getAuth, client, providerId, clientId, clientSec
12662
12768
  debugLog7("[401] Token refreshed, retrying request...");
12663
12769
  return executeWithEndpoints();
12664
12770
  } catch (refreshError) {
12771
+ if (refreshError instanceof AntigravityTokenRefreshError) {
12772
+ if (refreshError.isInvalidGrant) {
12773
+ debugLog7(`[401] Token revoked (invalid_grant), clearing caches`);
12774
+ invalidateProjectContextByRefreshToken(refreshParts.refreshToken);
12775
+ clearProjectContextCache();
12776
+ }
12777
+ debugLog7(`[401] Token refresh failed: ${refreshError.description || refreshError.message}`);
12778
+ return new Response(JSON.stringify({
12779
+ error: {
12780
+ message: refreshError.description || refreshError.message,
12781
+ type: refreshError.isInvalidGrant ? "token_revoked" : "unauthorized",
12782
+ code: refreshError.code || "token_refresh_failed"
12783
+ }
12784
+ }), {
12785
+ status: 401,
12786
+ statusText: "Unauthorized",
12787
+ headers: { "Content-Type": "application/json" }
12788
+ });
12789
+ }
12665
12790
  debugLog7(`[401] Token refresh failed: ${refreshError instanceof Error ? refreshError.message : "Unknown error"}`);
12666
12791
  return new Response(JSON.stringify({
12667
12792
  error: {
12668
- message: `Token refresh failed: ${refreshError instanceof Error ? refreshError.message : "Unknown error"}`,
12793
+ message: refreshError instanceof Error ? refreshError.message : "Unknown error",
12669
12794
  type: "unauthorized",
12670
12795
  code: "token_refresh_failed"
12671
12796
  }
@@ -12827,81 +12952,504 @@ async function createGoogleAntigravityAuthPlugin({
12827
12952
  auth: authHook
12828
12953
  };
12829
12954
  }
12830
- // src/features/claude-code-command-loader/loader.ts
12831
- import { existsSync as existsSync30, readdirSync as readdirSync11, readFileSync as readFileSync20 } from "fs";
12832
- import { join as join40, basename } from "path";
12833
- function loadCommandsFromDir(commandsDir, scope) {
12834
- if (!existsSync30(commandsDir)) {
12835
- return [];
12836
- }
12837
- const entries = readdirSync11(commandsDir, { withFileTypes: true });
12838
- const commands = [];
12839
- for (const entry of entries) {
12840
- if (!isMarkdownFile(entry))
12841
- continue;
12842
- const commandPath = join40(commandsDir, entry.name);
12843
- const commandName = basename(entry.name, ".md");
12844
- try {
12845
- const content = readFileSync20(commandPath, "utf-8");
12846
- const { data, body } = parseFrontmatter(content);
12847
- const wrappedTemplate = `<command-instruction>
12848
- ${body.trim()}
12955
+ // src/features/claude-code-command-loader/loader.ts
12956
+ import { existsSync as existsSync30, readdirSync as readdirSync11, readFileSync as readFileSync20 } from "fs";
12957
+ import { join as join40, basename } from "path";
12958
+ function loadCommandsFromDir(commandsDir, scope) {
12959
+ if (!existsSync30(commandsDir)) {
12960
+ return [];
12961
+ }
12962
+ const entries = readdirSync11(commandsDir, { withFileTypes: true });
12963
+ const commands = [];
12964
+ for (const entry of entries) {
12965
+ if (!isMarkdownFile(entry))
12966
+ continue;
12967
+ const commandPath = join40(commandsDir, entry.name);
12968
+ const commandName = basename(entry.name, ".md");
12969
+ try {
12970
+ const content = readFileSync20(commandPath, "utf-8");
12971
+ const { data, body } = parseFrontmatter(content);
12972
+ const wrappedTemplate = `<command-instruction>
12973
+ ${body.trim()}
12974
+ </command-instruction>
12975
+
12976
+ <user-request>
12977
+ $ARGUMENTS
12978
+ </user-request>`;
12979
+ const formattedDescription = `(${scope}) ${data.description || ""}`;
12980
+ const isOpencodeSource = scope === "opencode" || scope === "opencode-project";
12981
+ const definition = {
12982
+ name: commandName,
12983
+ description: formattedDescription,
12984
+ template: wrappedTemplate,
12985
+ agent: data.agent,
12986
+ model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
12987
+ subtask: data.subtask,
12988
+ argumentHint: data["argument-hint"]
12989
+ };
12990
+ commands.push({
12991
+ name: commandName,
12992
+ path: commandPath,
12993
+ definition,
12994
+ scope
12995
+ });
12996
+ } catch {
12997
+ continue;
12998
+ }
12999
+ }
13000
+ return commands;
13001
+ }
13002
+ function commandsToRecord(commands) {
13003
+ const result = {};
13004
+ for (const cmd of commands) {
13005
+ result[cmd.name] = cmd.definition;
13006
+ }
13007
+ return result;
13008
+ }
13009
+ function loadUserCommands() {
13010
+ const userCommandsDir = join40(getClaudeConfigDir(), "commands");
13011
+ const commands = loadCommandsFromDir(userCommandsDir, "user");
13012
+ return commandsToRecord(commands);
13013
+ }
13014
+ function loadProjectCommands() {
13015
+ const projectCommandsDir = join40(process.cwd(), ".claude", "commands");
13016
+ const commands = loadCommandsFromDir(projectCommandsDir, "project");
13017
+ return commandsToRecord(commands);
13018
+ }
13019
+ function loadOpencodeGlobalCommands() {
13020
+ const { homedir: homedir9 } = __require("os");
13021
+ const opencodeCommandsDir = join40(homedir9(), ".config", "opencode", "command");
13022
+ const commands = loadCommandsFromDir(opencodeCommandsDir, "opencode");
13023
+ return commandsToRecord(commands);
13024
+ }
13025
+ function loadOpencodeProjectCommands() {
13026
+ const opencodeProjectDir = join40(process.cwd(), ".opencode", "command");
13027
+ const commands = loadCommandsFromDir(opencodeProjectDir, "opencode-project");
13028
+ return commandsToRecord(commands);
13029
+ }
13030
+ // src/features/builtin-commands/templates/init-deep.ts
13031
+ var INIT_DEEP_TEMPLATE = `# Initialize Deep Knowledge Base
13032
+
13033
+ Generate comprehensive AGENTS.md files across project hierarchy. Combines root-level project knowledge (gen-knowledge) with complexity-based subdirectory documentation (gen-knowledge-deep).
13034
+
13035
+ ## Usage
13036
+
13037
+ \`\`\`
13038
+ /init-deep # Analyze and generate hierarchical AGENTS.md
13039
+ /init-deep --create-new # Force create from scratch (ignore existing)
13040
+ /init-deep --max-depth=2 # Limit to N directory levels (default: 3)
13041
+ \`\`\`
13042
+
13043
+ ---
13044
+
13045
+ ## Core Principles
13046
+
13047
+ - **Telegraphic Style**: Sacrifice grammar for concision ("Project uses React" \u2192 "React 18")
13048
+ - **Predict-then-Compare**: Predict standard \u2192 find actual \u2192 document ONLY deviations
13049
+ - **Hierarchy Aware**: Parent covers general, children cover specific
13050
+ - **No Redundancy**: Child AGENTS.md NEVER repeats parent content
13051
+ - **LSP-First**: Use LSP tools for accurate code intelligence when available (semantic > text search)
13052
+
13053
+ ---
13054
+
13055
+ ## Process
13056
+
13057
+ <critical>
13058
+ **MANDATORY: TodoWrite for ALL phases. Mark in_progress \u2192 completed in real-time.**
13059
+ </critical>
13060
+
13061
+ ### Phase 0: Initialize
13062
+
13063
+ \`\`\`
13064
+ TodoWrite([
13065
+ { id: "p1-analysis", content: "Parallel project structure & complexity analysis", status: "pending", priority: "high" },
13066
+ { id: "p2-scoring", content: "Score directories, determine AGENTS.md locations", status: "pending", priority: "high" },
13067
+ { id: "p3-root", content: "Generate root AGENTS.md with Predict-then-Compare", status: "pending", priority: "high" },
13068
+ { id: "p4-subdirs", content: "Generate subdirectory AGENTS.md files in parallel", status: "pending", priority: "high" },
13069
+ { id: "p5-review", content: "Review, deduplicate, validate all files", status: "pending", priority: "medium" }
13070
+ ])
13071
+ \`\`\`
13072
+
13073
+ ---
13074
+
13075
+ ## Phase 1: Parallel Project Analysis
13076
+
13077
+ **Mark "p1-analysis" as in_progress.**
13078
+
13079
+ Launch **ALL tasks simultaneously**:
13080
+
13081
+ <parallel-tasks>
13082
+
13083
+ ### Structural Analysis (bash - run in parallel)
13084
+ \`\`\`bash
13085
+ # Task A: Directory depth analysis
13086
+ find . -type d -not -path '*/\\.*' -not -path '*/node_modules/*' -not -path '*/venv/*' -not -path '*/__pycache__/*' -not -path '*/dist/*' -not -path '*/build/*' | awk -F/ '{print NF-1}' | sort -n | uniq -c
13087
+
13088
+ # Task B: File count per directory
13089
+ find . -type f -not -path '*/\\.*' -not -path '*/node_modules/*' -not -path '*/venv/*' -not -path '*/__pycache__/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -30
13090
+
13091
+ # Task C: Code concentration
13092
+ find . -type f \\( -name "*.py" -o -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" -o -name "*.go" -o -name "*.rs" -o -name "*.java" \\) -not -path '*/node_modules/*' -not -path '*/venv/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -20
13093
+
13094
+ # Task D: Existing knowledge files
13095
+ find . -type f \\( -name "AGENTS.md" -o -name "CLAUDE.md" \\) -not -path '*/node_modules/*' 2>/dev/null
13096
+ \`\`\`
13097
+
13098
+ ### Context Gathering (Explore agents - background_task in parallel)
13099
+
13100
+ \`\`\`
13101
+ background_task(agent="explore", prompt="Project structure: PREDICT standard {lang} patterns \u2192 FIND package.json/pyproject.toml/go.mod \u2192 REPORT deviations only")
13102
+
13103
+ background_task(agent="explore", prompt="Entry points: PREDICT typical (main.py, index.ts) \u2192 FIND actual \u2192 REPORT non-standard organization")
13104
+
13105
+ background_task(agent="explore", prompt="Conventions: FIND .cursor/rules, .cursorrules, eslintrc, pyproject.toml \u2192 REPORT project-specific rules DIFFERENT from defaults")
13106
+
13107
+ background_task(agent="explore", prompt="Anti-patterns: FIND comments with 'DO NOT', 'NEVER', 'ALWAYS', 'LEGACY', 'DEPRECATED' \u2192 REPORT forbidden patterns")
13108
+
13109
+ background_task(agent="explore", prompt="Build/CI: FIND .github/workflows, Makefile, justfile \u2192 REPORT non-standard build/deploy patterns")
13110
+
13111
+ background_task(agent="explore", prompt="Test patterns: FIND pytest.ini, jest.config, test structure \u2192 REPORT unique testing conventions")
13112
+ \`\`\`
13113
+
13114
+ ### Code Intelligence Analysis (LSP tools - run in parallel)
13115
+
13116
+ LSP provides semantic understanding beyond text search. Use for accurate code mapping.
13117
+
13118
+ \`\`\`
13119
+ # Step 1: Check LSP availability
13120
+ lsp_servers() # Verify language server is available
13121
+
13122
+ # Step 2: Analyze entry point files (run in parallel)
13123
+ # Find entry points first, then analyze each with lsp_document_symbols
13124
+ lsp_document_symbols(filePath="src/index.ts") # Main entry
13125
+ lsp_document_symbols(filePath="src/main.py") # Python entry
13126
+ lsp_document_symbols(filePath="cmd/main.go") # Go entry
13127
+
13128
+ # Step 3: Discover key symbols across workspace (run in parallel)
13129
+ lsp_workspace_symbols(filePath=".", query="class") # All classes
13130
+ lsp_workspace_symbols(filePath=".", query="interface") # All interfaces
13131
+ lsp_workspace_symbols(filePath=".", query="function") # Top-level functions
13132
+ lsp_workspace_symbols(filePath=".", query="type") # Type definitions
13133
+
13134
+ # Step 4: Analyze symbol centrality (for top 5-10 key symbols)
13135
+ # High reference count = central/important concept
13136
+ lsp_find_references(filePath="src/index.ts", line=X, character=Y) # Main export
13137
+ \`\`\`
13138
+
13139
+ #### LSP Analysis Output Format
13140
+
13141
+ \`\`\`
13142
+ CODE_INTELLIGENCE = {
13143
+ entry_points: [
13144
+ { file: "src/index.ts", exports: ["Plugin", "createHook"], symbol_count: 12 }
13145
+ ],
13146
+ key_symbols: [
13147
+ { name: "Plugin", type: "class", file: "src/index.ts", refs: 45, role: "Central orchestrator" },
13148
+ { name: "createHook", type: "function", file: "src/utils.ts", refs: 23, role: "Hook factory" }
13149
+ ],
13150
+ module_boundaries: [
13151
+ { dir: "src/hooks", exports: 21, imports_from: ["shared/"] },
13152
+ { dir: "src/tools", exports: 15, imports_from: ["shared/", "hooks/"] }
13153
+ ]
13154
+ }
13155
+ \`\`\`
13156
+
13157
+ <critical>
13158
+ **LSP Fallback**: If LSP unavailable (no server installed), skip this section and rely on explore agents + AST-grep patterns.
13159
+ </critical>
13160
+
13161
+ </parallel-tasks>
13162
+
13163
+ **Collect all results. Mark "p1-analysis" as completed.**
13164
+
13165
+ ---
13166
+
13167
+ ## Phase 2: Complexity Scoring & Location Decision
13168
+
13169
+ **Mark "p2-scoring" as in_progress.**
13170
+
13171
+ ### Scoring Matrix
13172
+
13173
+ | Factor | Weight | Threshold | Source |
13174
+ |--------|--------|-----------|--------|
13175
+ | File count | 3x | >20 files = high | bash |
13176
+ | Subdirectory count | 2x | >5 subdirs = high | bash |
13177
+ | Code file ratio | 2x | >70% code = high | bash |
13178
+ | Unique patterns | 1x | Has own config | explore |
13179
+ | Module boundary | 2x | Has __init__.py/index.ts | bash |
13180
+ | **Symbol density** | 2x | >30 symbols = high | LSP |
13181
+ | **Export count** | 2x | >10 exports = high | LSP |
13182
+ | **Reference centrality** | 3x | Symbols with >20 refs | LSP |
13183
+
13184
+ <lsp-scoring>
13185
+ **LSP-Enhanced Scoring** (if available):
13186
+
13187
+ \`\`\`
13188
+ For each directory in candidates:
13189
+ symbols = lsp_document_symbols(dir/index.ts or dir/__init__.py)
13190
+
13191
+ symbol_score = len(symbols) > 30 ? 6 : len(symbols) > 15 ? 3 : 0
13192
+ export_score = count(exported symbols) > 10 ? 4 : 0
13193
+
13194
+ # Check if this module is central (many things depend on it)
13195
+ for each exported symbol:
13196
+ refs = lsp_find_references(symbol)
13197
+ if refs > 20: centrality_score += 3
13198
+
13199
+ total_score += symbol_score + export_score + centrality_score
13200
+ \`\`\`
13201
+ </lsp-scoring>
13202
+
13203
+ ### Decision Rules
13204
+
13205
+ | Score | Action |
13206
+ |-------|--------|
13207
+ | **Root (.)** | ALWAYS create AGENTS.md |
13208
+ | **High (>15)** | Create dedicated AGENTS.md |
13209
+ | **Medium (8-15)** | Create if distinct domain |
13210
+ | **Low (<8)** | Skip, parent sufficient |
13211
+
13212
+ ### Output Format
13213
+
13214
+ \`\`\`
13215
+ AGENTS_LOCATIONS = [
13216
+ { path: ".", type: "root" },
13217
+ { path: "src/api", score: 18, reason: "high complexity, 45 files" },
13218
+ { path: "src/hooks", score: 12, reason: "distinct domain, unique patterns" },
13219
+ ]
13220
+ \`\`\`
13221
+
13222
+ **Mark "p2-scoring" as completed.**
13223
+
13224
+ ---
13225
+
13226
+ ## Phase 3: Generate Root AGENTS.md
13227
+
13228
+ **Mark "p3-root" as in_progress.**
13229
+
13230
+ Root AGENTS.md gets **full treatment** with Predict-then-Compare synthesis.
13231
+
13232
+ ### Required Sections
13233
+
13234
+ \`\`\`markdown
13235
+ # PROJECT KNOWLEDGE BASE
13236
+
13237
+ **Generated:** {TIMESTAMP}
13238
+ **Commit:** {SHORT_SHA}
13239
+ **Branch:** {BRANCH}
13240
+
13241
+ ## OVERVIEW
13242
+
13243
+ {1-2 sentences: what project does, core tech stack}
13244
+
13245
+ ## STRUCTURE
13246
+
13247
+ \\\`\\\`\\\`
13248
+ {project-root}/
13249
+ \u251C\u2500\u2500 {dir}/ # {non-obvious purpose only}
13250
+ \u2514\u2500\u2500 {entry} # entry point
13251
+ \\\`\\\`\\\`
13252
+
13253
+ ## WHERE TO LOOK
13254
+
13255
+ | Task | Location | Notes |
13256
+ |------|----------|-------|
13257
+ | Add feature X | \\\`src/x/\\\` | {pattern hint} |
13258
+
13259
+ ## CODE MAP
13260
+
13261
+ {Generated from LSP analysis - shows key symbols and their relationships}
13262
+
13263
+ | Symbol | Type | Location | Refs | Role |
13264
+ |--------|------|----------|------|------|
13265
+ | {MainClass} | Class | \\\`src/index.ts\\\` | {N} | {Central orchestrator} |
13266
+ | {createX} | Function | \\\`src/utils.ts\\\` | {N} | {Factory pattern} |
13267
+ | {Config} | Interface | \\\`src/types.ts\\\` | {N} | {Configuration contract} |
13268
+
13269
+ ### Module Dependencies
13270
+
13271
+ \\\`\\\`\\\`
13272
+ {entry} \u2500\u2500imports\u2500\u2500> {core/}
13273
+ \u2502 \u2502
13274
+ \u2514\u2500\u2500imports\u2500\u2500> {utils/} <\u2500\u2500imports\u2500\u2500 {features/}
13275
+ \\\`\\\`\\\`
13276
+
13277
+ <code-map-note>
13278
+ **Skip CODE MAP if**: LSP unavailable OR project too small (<10 files) OR no clear module boundaries.
13279
+ </code-map-note>
13280
+
13281
+ ## CONVENTIONS
13282
+
13283
+ {ONLY deviations from standard - skip generic advice}
13284
+
13285
+ - **{rule}**: {specific detail}
13286
+
13287
+ ## ANTI-PATTERNS (THIS PROJECT)
13288
+
13289
+ {Things explicitly forbidden HERE}
13290
+
13291
+ - **{pattern}**: {why} \u2192 {alternative}
13292
+
13293
+ ## UNIQUE STYLES
13294
+
13295
+ {Project-specific coding styles}
13296
+
13297
+ - **{style}**: {how different}
13298
+
13299
+ ## COMMANDS
13300
+
13301
+ \\\`\\\`\\\`bash
13302
+ {dev-command}
13303
+ {test-command}
13304
+ {build-command}
13305
+ \\\`\\\`\\\`
13306
+
13307
+ ## NOTES
13308
+
13309
+ {Gotchas, non-obvious info}
13310
+ \`\`\`
13311
+
13312
+ ### Quality Gates
13313
+
13314
+ - [ ] Size: 50-150 lines
13315
+ - [ ] No generic advice ("write clean code")
13316
+ - [ ] No obvious info ("tests/ has tests")
13317
+ - [ ] Every item is project-specific
13318
+
13319
+ **Mark "p3-root" as completed.**
13320
+
13321
+ ---
13322
+
13323
+ ## Phase 4: Generate Subdirectory AGENTS.md
13324
+
13325
+ **Mark "p4-subdirs" as in_progress.**
13326
+
13327
+ For each location in AGENTS_LOCATIONS (except root), launch **parallel document-writer agents**:
13328
+
13329
+ \`\`\`typescript
13330
+ for (const loc of AGENTS_LOCATIONS.filter(l => l.path !== ".")) {
13331
+ background_task({
13332
+ agent: "document-writer",
13333
+ prompt: \\\`
13334
+ Generate AGENTS.md for: \${loc.path}
13335
+
13336
+ CONTEXT:
13337
+ - Complexity reason: \${loc.reason}
13338
+ - Parent AGENTS.md: ./AGENTS.md (already covers project overview)
13339
+
13340
+ CRITICAL RULES:
13341
+ 1. Focus ONLY on this directory's specific context
13342
+ 2. NEVER repeat parent AGENTS.md content
13343
+ 3. Shorter is better - 30-80 lines max
13344
+ 4. Telegraphic style - sacrifice grammar
13345
+
13346
+ REQUIRED SECTIONS:
13347
+ - OVERVIEW (1 line: what this directory does)
13348
+ - STRUCTURE (only if >5 subdirs)
13349
+ - WHERE TO LOOK (directory-specific tasks)
13350
+ - CONVENTIONS (only if DIFFERENT from root)
13351
+ - ANTI-PATTERNS (directory-specific only)
13352
+
13353
+ OUTPUT: Write to \${loc.path}/AGENTS.md
13354
+ \\\`
13355
+ })
13356
+ }
13357
+ \`\`\`
13358
+
13359
+ **Wait for all agents. Mark "p4-subdirs" as completed.**
13360
+
13361
+ ---
13362
+
13363
+ ## Phase 5: Review & Deduplicate
13364
+
13365
+ **Mark "p5-review" as in_progress.**
13366
+
13367
+ ### Validation Checklist
13368
+
13369
+ For EACH generated AGENTS.md:
13370
+
13371
+ | Check | Action if Fail |
13372
+ |-------|----------------|
13373
+ | Contains generic advice | REMOVE the line |
13374
+ | Repeats parent content | REMOVE the line |
13375
+ | Missing required section | ADD it |
13376
+ | Over 150 lines (root) / 80 lines (subdir) | TRIM |
13377
+ | Verbose explanations | REWRITE telegraphic |
13378
+
13379
+ ### Cross-Reference Validation
13380
+
13381
+ \`\`\`
13382
+ For each child AGENTS.md:
13383
+ For each line in child:
13384
+ If similar line exists in parent:
13385
+ REMOVE from child (parent already covers)
13386
+ \`\`\`
13387
+
13388
+ **Mark "p5-review" as completed.**
13389
+
13390
+ ---
13391
+
13392
+ ## Final Report
13393
+
13394
+ \`\`\`
13395
+ === init-deep Complete ===
13396
+
13397
+ Files Generated:
13398
+ \u2713 ./AGENTS.md (root, {N} lines)
13399
+ \u2713 ./src/hooks/AGENTS.md ({N} lines)
13400
+ \u2713 ./src/tools/AGENTS.md ({N} lines)
13401
+
13402
+ Directories Analyzed: {N}
13403
+ AGENTS.md Created: {N}
13404
+ Total Lines: {N}
13405
+
13406
+ Hierarchy:
13407
+ ./AGENTS.md
13408
+ \u251C\u2500\u2500 src/hooks/AGENTS.md
13409
+ \u2514\u2500\u2500 src/tools/AGENTS.md
13410
+ \`\`\`
13411
+
13412
+ ---
13413
+
13414
+ ## Anti-Patterns for THIS Command
13415
+
13416
+ - **Over-documenting**: Not every directory needs AGENTS.md
13417
+ - **Redundancy**: Child must NOT repeat parent
13418
+ - **Generic content**: Remove anything that applies to ALL projects
13419
+ - **Sequential execution**: MUST use parallel agents
13420
+ - **Deep nesting**: Rarely need AGENTS.md at depth 4+
13421
+ - **Verbose style**: "This directory contains..." \u2192 just list it
13422
+ - **Ignoring LSP**: If LSP available, USE IT - semantic analysis > text grep
13423
+ - **LSP without fallback**: Always have explore agent backup if LSP unavailable
13424
+ - **Over-referencing**: Don't trace refs for EVERY symbol - focus on exports only`;
13425
+
13426
+ // src/features/builtin-commands/commands.ts
13427
+ var BUILTIN_COMMAND_DEFINITIONS = {
13428
+ "init-deep": {
13429
+ description: "(builtin) Initialize hierarchical AGENTS.md knowledge base",
13430
+ template: `<command-instruction>
13431
+ ${INIT_DEEP_TEMPLATE}
12849
13432
  </command-instruction>
12850
13433
 
12851
13434
  <user-request>
12852
13435
  $ARGUMENTS
12853
- </user-request>`;
12854
- const formattedDescription = `(${scope}) ${data.description || ""}`;
12855
- const isOpencodeSource = scope === "opencode" || scope === "opencode-project";
12856
- const definition = {
12857
- name: commandName,
12858
- description: formattedDescription,
12859
- template: wrappedTemplate,
12860
- agent: data.agent,
12861
- model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
12862
- subtask: data.subtask,
12863
- argumentHint: data["argument-hint"]
13436
+ </user-request>`,
13437
+ argumentHint: "[--create-new] [--max-depth=N]"
13438
+ }
13439
+ };
13440
+ function loadBuiltinCommands(disabledCommands) {
13441
+ const disabled = new Set(disabledCommands ?? []);
13442
+ const commands = {};
13443
+ for (const [name, definition] of Object.entries(BUILTIN_COMMAND_DEFINITIONS)) {
13444
+ if (!disabled.has(name)) {
13445
+ commands[name] = {
13446
+ name,
13447
+ ...definition
12864
13448
  };
12865
- commands.push({
12866
- name: commandName,
12867
- path: commandPath,
12868
- definition,
12869
- scope
12870
- });
12871
- } catch {
12872
- continue;
12873
13449
  }
12874
13450
  }
12875
13451
  return commands;
12876
13452
  }
12877
- function commandsToRecord(commands) {
12878
- const result = {};
12879
- for (const cmd of commands) {
12880
- result[cmd.name] = cmd.definition;
12881
- }
12882
- return result;
12883
- }
12884
- function loadUserCommands() {
12885
- const userCommandsDir = join40(getClaudeConfigDir(), "commands");
12886
- const commands = loadCommandsFromDir(userCommandsDir, "user");
12887
- return commandsToRecord(commands);
12888
- }
12889
- function loadProjectCommands() {
12890
- const projectCommandsDir = join40(process.cwd(), ".claude", "commands");
12891
- const commands = loadCommandsFromDir(projectCommandsDir, "project");
12892
- return commandsToRecord(commands);
12893
- }
12894
- function loadOpencodeGlobalCommands() {
12895
- const { homedir: homedir10 } = __require("os");
12896
- const opencodeCommandsDir = join40(homedir10(), ".config", "opencode", "command");
12897
- const commands = loadCommandsFromDir(opencodeCommandsDir, "opencode");
12898
- return commandsToRecord(commands);
12899
- }
12900
- function loadOpencodeProjectCommands() {
12901
- const opencodeProjectDir = join40(process.cwd(), ".opencode", "command");
12902
- const commands = loadCommandsFromDir(opencodeProjectDir, "opencode-project");
12903
- return commandsToRecord(commands);
12904
- }
12905
13453
  // src/features/claude-code-agent-loader/loader.ts
12906
13454
  import { existsSync as existsSync31, readdirSync as readdirSync12, readFileSync as readFileSync21 } from "fs";
12907
13455
  import { join as join41, basename as basename2 } from "path";
@@ -13065,13 +13613,13 @@ async function loadMcpConfigs() {
13065
13613
  const servers = {};
13066
13614
  const loadedServers = [];
13067
13615
  const paths = getMcpConfigPaths();
13068
- for (const { path: path8, scope } of paths) {
13069
- const config = await loadMcpConfigFile(path8);
13616
+ for (const { path: path7, scope } of paths) {
13617
+ const config = await loadMcpConfigFile(path7);
13070
13618
  if (!config?.mcpServers)
13071
13619
  continue;
13072
13620
  for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
13073
13621
  if (serverConfig.disabled) {
13074
- log(`Skipping disabled MCP server "${name}"`, { path: path8 });
13622
+ log(`Skipping disabled MCP server "${name}"`, { path: path7 });
13075
13623
  continue;
13076
13624
  }
13077
13625
  try {
@@ -13082,7 +13630,7 @@ async function loadMcpConfigs() {
13082
13630
  loadedServers.splice(existingIndex, 1);
13083
13631
  }
13084
13632
  loadedServers.push({ name, scope, config: transformed });
13085
- log(`Loaded MCP server "${name}" from ${scope}`, { path: path8 });
13633
+ log(`Loaded MCP server "${name}" from ${scope}`, { path: path7 });
13086
13634
  } catch (error) {
13087
13635
  log(`Failed to transform MCP server "${name}"`, error);
13088
13636
  }
@@ -13092,20 +13640,20 @@ async function loadMcpConfigs() {
13092
13640
  }
13093
13641
  // src/features/claude-code-plugin-loader/loader.ts
13094
13642
  import { existsSync as existsSync33, readdirSync as readdirSync13, readFileSync as readFileSync22 } from "fs";
13095
- import { homedir as homedir10 } from "os";
13643
+ import { homedir as homedir9 } from "os";
13096
13644
  import { join as join43, basename as basename3 } from "path";
13097
13645
  var CLAUDE_PLUGIN_ROOT_VAR = "${CLAUDE_PLUGIN_ROOT}";
13098
13646
  function getPluginsBaseDir() {
13099
13647
  if (process.env.CLAUDE_PLUGINS_HOME) {
13100
13648
  return process.env.CLAUDE_PLUGINS_HOME;
13101
13649
  }
13102
- return join43(homedir10(), ".claude", "plugins");
13650
+ return join43(homedir9(), ".claude", "plugins");
13103
13651
  }
13104
13652
  function getInstalledPluginsPath() {
13105
13653
  return join43(getPluginsBaseDir(), "installed_plugins.json");
13106
13654
  }
13107
- function resolvePluginPath(path8, pluginRoot) {
13108
- return path8.replace(CLAUDE_PLUGIN_ROOT_VAR, pluginRoot);
13655
+ function resolvePluginPath(path7, pluginRoot) {
13656
+ return path7.replace(CLAUDE_PLUGIN_ROOT_VAR, pluginRoot);
13109
13657
  }
13110
13658
  function resolvePluginPaths(obj, pluginRoot) {
13111
13659
  if (obj === null || obj === undefined)
@@ -13142,7 +13690,7 @@ function getClaudeSettingsPath() {
13142
13690
  if (process.env.CLAUDE_SETTINGS_PATH) {
13143
13691
  return process.env.CLAUDE_SETTINGS_PATH;
13144
13692
  }
13145
- return join43(homedir10(), ".claude", "settings.json");
13693
+ return join43(homedir9(), ".claude", "settings.json");
13146
13694
  }
13147
13695
  function loadClaudeSettings() {
13148
13696
  const settingsPath = getClaudeSettingsPath();
@@ -13251,7 +13799,7 @@ function discoverInstalledPlugins(options) {
13251
13799
  return { plugins, errors };
13252
13800
  }
13253
13801
  function loadPluginCommands(plugins) {
13254
- const commands = {};
13802
+ const commands2 = {};
13255
13803
  for (const plugin2 of plugins) {
13256
13804
  if (!plugin2.commandsDir || !existsSync33(plugin2.commandsDir))
13257
13805
  continue;
@@ -13273,7 +13821,7 @@ ${body.trim()}
13273
13821
  $ARGUMENTS
13274
13822
  </user-request>`;
13275
13823
  const formattedDescription = `(plugin: ${plugin2.name}) ${data.description || ""}`;
13276
- commands[namespacedName] = {
13824
+ commands2[namespacedName] = {
13277
13825
  name: namespacedName,
13278
13826
  description: formattedDescription,
13279
13827
  template: wrappedTemplate,
@@ -13288,7 +13836,7 @@ $ARGUMENTS
13288
13836
  }
13289
13837
  }
13290
13838
  }
13291
- return commands;
13839
+ return commands2;
13292
13840
  }
13293
13841
  function loadPluginSkillsAsCommands(plugins) {
13294
13842
  const skills = {};
@@ -13436,14 +13984,14 @@ function loadPluginHooksConfigs(plugins) {
13436
13984
  }
13437
13985
  async function loadAllPluginComponents(options) {
13438
13986
  const { plugins, errors } = discoverInstalledPlugins(options);
13439
- const commands = loadPluginCommands(plugins);
13987
+ const commands2 = loadPluginCommands(plugins);
13440
13988
  const skills = loadPluginSkillsAsCommands(plugins);
13441
13989
  const agents = loadPluginAgents(plugins);
13442
13990
  const mcpServers = await loadPluginMcpServers(plugins);
13443
13991
  const hooksConfigs = loadPluginHooksConfigs(plugins);
13444
- log(`Loaded ${plugins.length} plugins with ${Object.keys(commands).length} commands, ${Object.keys(skills).length} skills, ${Object.keys(agents).length} agents, ${Object.keys(mcpServers).length} MCP servers`);
13992
+ log(`Loaded ${plugins.length} plugins with ${Object.keys(commands2).length} commands, ${Object.keys(skills).length} skills, ${Object.keys(agents).length} agents, ${Object.keys(mcpServers).length} MCP servers`);
13445
13993
  return {
13446
- commands,
13994
+ commands: commands2,
13447
13995
  skills,
13448
13996
  agents,
13449
13997
  mcpServers,
@@ -13490,6 +14038,36 @@ var SEVERITY_MAP = {
13490
14038
  var DEFAULT_MAX_REFERENCES = 200;
13491
14039
  var DEFAULT_MAX_SYMBOLS = 200;
13492
14040
  var DEFAULT_MAX_DIAGNOSTICS = 200;
14041
+ var LSP_INSTALL_HINTS = {
14042
+ typescript: "npm install -g typescript-language-server typescript",
14043
+ deno: "Install Deno from https://deno.land",
14044
+ vue: "npm install -g @vue/language-server",
14045
+ eslint: "npm install -g vscode-langservers-extracted",
14046
+ oxlint: "npm install -g oxlint",
14047
+ biome: "npm install -g @biomejs/biome",
14048
+ gopls: "go install golang.org/x/tools/gopls@latest",
14049
+ "ruby-lsp": "gem install ruby-lsp",
14050
+ basedpyright: "pip install basedpyright",
14051
+ pyright: "pip install pyright",
14052
+ ty: "pip install ty",
14053
+ ruff: "pip install ruff",
14054
+ "elixir-ls": "See https://github.com/elixir-lsp/elixir-ls",
14055
+ zls: "See https://github.com/zigtools/zls",
14056
+ csharp: "dotnet tool install -g csharp-ls",
14057
+ fsharp: "dotnet tool install -g fsautocomplete",
14058
+ "sourcekit-lsp": "Included with Xcode or Swift toolchain",
14059
+ rust: "rustup component add rust-analyzer",
14060
+ clangd: "See https://clangd.llvm.org/installation",
14061
+ svelte: "npm install -g svelte-language-server",
14062
+ astro: "npm install -g @astrojs/language-server",
14063
+ "bash-ls": "npm install -g bash-language-server",
14064
+ jdtls: "See https://github.com/eclipse-jdtls/eclipse.jdt.ls",
14065
+ "yaml-ls": "npm install -g yaml-language-server",
14066
+ "lua-ls": "See https://github.com/LuaLS/lua-language-server",
14067
+ php: "npm install -g intelephense",
14068
+ dart: "Included with Dart SDK",
14069
+ "terraform-ls": "See https://github.com/hashicorp/terraform-ls"
14070
+ };
13493
14071
  var BUILTIN_SERVERS = {
13494
14072
  typescript: {
13495
14073
  command: ["typescript-language-server", "--stdio"],
@@ -13749,12 +14327,12 @@ var EXT_TO_LANG = {
13749
14327
  // src/tools/lsp/config.ts
13750
14328
  import { existsSync as existsSync34, readFileSync as readFileSync23 } from "fs";
13751
14329
  import { join as join44 } from "path";
13752
- import { homedir as homedir11 } from "os";
13753
- function loadJsonFile(path8) {
13754
- if (!existsSync34(path8))
14330
+ import { homedir as homedir10 } from "os";
14331
+ function loadJsonFile(path7) {
14332
+ if (!existsSync34(path7))
13755
14333
  return null;
13756
14334
  try {
13757
- return JSON.parse(readFileSync23(path8, "utf-8"));
14335
+ return JSON.parse(readFileSync23(path7, "utf-8"));
13758
14336
  } catch {
13759
14337
  return null;
13760
14338
  }
@@ -13763,8 +14341,8 @@ function getConfigPaths2() {
13763
14341
  const cwd = process.cwd();
13764
14342
  return {
13765
14343
  project: join44(cwd, ".opencode", "oh-my-opencode.json"),
13766
- user: join44(homedir11(), ".config", "opencode", "oh-my-opencode.json"),
13767
- opencode: join44(homedir11(), ".config", "opencode", "opencode.json")
14344
+ user: join44(homedir10(), ".config", "opencode", "oh-my-opencode.json"),
14345
+ opencode: join44(homedir10(), ".config", "opencode", "opencode.json")
13768
14346
  };
13769
14347
  }
13770
14348
  function loadAllConfigs() {
@@ -13836,16 +14414,38 @@ function findServerForExtension(ext) {
13836
14414
  for (const server of servers) {
13837
14415
  if (server.extensions.includes(ext) && isServerInstalled(server.command)) {
13838
14416
  return {
13839
- id: server.id,
13840
- command: server.command,
13841
- extensions: server.extensions,
13842
- priority: server.priority,
13843
- env: server.env,
13844
- initialization: server.initialization
14417
+ status: "found",
14418
+ server: {
14419
+ id: server.id,
14420
+ command: server.command,
14421
+ extensions: server.extensions,
14422
+ priority: server.priority,
14423
+ env: server.env,
14424
+ initialization: server.initialization
14425
+ }
13845
14426
  };
13846
14427
  }
13847
14428
  }
13848
- return null;
14429
+ for (const server of servers) {
14430
+ if (server.extensions.includes(ext)) {
14431
+ const installHint = LSP_INSTALL_HINTS[server.id] || `Install '${server.command[0]}' and ensure it's in your PATH`;
14432
+ return {
14433
+ status: "not_installed",
14434
+ server: {
14435
+ id: server.id,
14436
+ command: server.command,
14437
+ extensions: server.extensions
14438
+ },
14439
+ installHint
14440
+ };
14441
+ }
14442
+ }
14443
+ const availableServers = [...new Set(servers.map((s) => s.id))];
14444
+ return {
14445
+ status: "not_configured",
14446
+ extension: ext,
14447
+ availableServers
14448
+ };
13849
14449
  }
13850
14450
  function getLanguageId(ext) {
13851
14451
  return EXT_TO_LANG[ext] || "plaintext";
@@ -13868,10 +14468,10 @@ function isServerInstalled(command) {
13868
14468
  const additionalPaths = [
13869
14469
  join44(cwd, "node_modules", ".bin", cmd),
13870
14470
  join44(cwd, "node_modules", ".bin", cmd + ext),
13871
- join44(homedir11(), ".config", "opencode", "bin", cmd),
13872
- join44(homedir11(), ".config", "opencode", "bin", cmd + ext),
13873
- join44(homedir11(), ".config", "opencode", "node_modules", ".bin", cmd),
13874
- join44(homedir11(), ".config", "opencode", "node_modules", ".bin", cmd + ext)
14471
+ join44(homedir10(), ".config", "opencode", "bin", cmd),
14472
+ join44(homedir10(), ".config", "opencode", "bin", cmd + ext),
14473
+ join44(homedir10(), ".config", "opencode", "node_modules", ".bin", cmd),
14474
+ join44(homedir10(), ".config", "opencode", "node_modules", ".bin", cmd + ext)
13875
14475
  ];
13876
14476
  for (const p of additionalPaths) {
13877
14477
  if (existsSync34(p)) {
@@ -14486,13 +15086,49 @@ function findWorkspaceRoot(filePath) {
14486
15086
  }
14487
15087
  return __require("path").dirname(resolve6(filePath));
14488
15088
  }
15089
+ function formatServerLookupError(result) {
15090
+ if (result.status === "not_installed") {
15091
+ const { server, installHint } = result;
15092
+ return [
15093
+ `LSP server '${server.id}' is configured but NOT INSTALLED.`,
15094
+ ``,
15095
+ `Command not found: ${server.command[0]}`,
15096
+ ``,
15097
+ `To install:`,
15098
+ ` ${installHint}`,
15099
+ ``,
15100
+ `Supported extensions: ${server.extensions.join(", ")}`,
15101
+ ``,
15102
+ `After installation, the server will be available automatically.`,
15103
+ `Run 'lsp_servers' tool to verify installation status.`
15104
+ ].join(`
15105
+ `);
15106
+ }
15107
+ return [
15108
+ `No LSP server configured for extension: ${result.extension}`,
15109
+ ``,
15110
+ `Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
15111
+ ``,
15112
+ `To add a custom server, configure 'lsp' in oh-my-opencode.json:`,
15113
+ ` {`,
15114
+ ` "lsp": {`,
15115
+ ` "my-server": {`,
15116
+ ` "command": ["my-lsp", "--stdio"],`,
15117
+ ` "extensions": ["${result.extension}"]`,
15118
+ ` }`,
15119
+ ` }`,
15120
+ ` }`
15121
+ ].join(`
15122
+ `);
15123
+ }
14489
15124
  async function withLspClient(filePath, fn) {
14490
15125
  const absPath = resolve6(filePath);
14491
15126
  const ext = extname2(absPath);
14492
- const server = findServerForExtension(ext);
14493
- if (!server) {
14494
- throw new Error(`No LSP server configured for extension: ${ext}`);
15127
+ const result = findServerForExtension(ext);
15128
+ if (result.status !== "found") {
15129
+ throw new Error(formatServerLookupError(result));
14495
15130
  }
15131
+ const server = result.server;
14496
15132
  const root = findWorkspaceRoot(absPath);
14497
15133
  const client = await lspManager.getClient(root, server);
14498
15134
  try {
@@ -15483,10 +16119,10 @@ function mergeDefs(...defs) {
15483
16119
  function cloneDef(schema) {
15484
16120
  return mergeDefs(schema._zod.def);
15485
16121
  }
15486
- function getElementAtPath(obj, path8) {
15487
- if (!path8)
16122
+ function getElementAtPath(obj, path7) {
16123
+ if (!path7)
15488
16124
  return obj;
15489
- return path8.reduce((acc, key) => acc?.[key], obj);
16125
+ return path7.reduce((acc, key) => acc?.[key], obj);
15490
16126
  }
15491
16127
  function promiseAllObject(promisesObj) {
15492
16128
  const keys = Object.keys(promisesObj);
@@ -15845,11 +16481,11 @@ function aborted(x, startIndex = 0) {
15845
16481
  }
15846
16482
  return false;
15847
16483
  }
15848
- function prefixIssues(path8, issues) {
16484
+ function prefixIssues(path7, issues) {
15849
16485
  return issues.map((iss) => {
15850
16486
  var _a;
15851
16487
  (_a = iss).path ?? (_a.path = []);
15852
- iss.path.unshift(path8);
16488
+ iss.path.unshift(path7);
15853
16489
  return iss;
15854
16490
  });
15855
16491
  }
@@ -16017,7 +16653,7 @@ function treeifyError(error, _mapper) {
16017
16653
  return issue2.message;
16018
16654
  };
16019
16655
  const result = { errors: [] };
16020
- const processError = (error2, path8 = []) => {
16656
+ const processError = (error2, path7 = []) => {
16021
16657
  var _a, _b;
16022
16658
  for (const issue2 of error2.issues) {
16023
16659
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -16027,7 +16663,7 @@ function treeifyError(error, _mapper) {
16027
16663
  } else if (issue2.code === "invalid_element") {
16028
16664
  processError({ issues: issue2.issues }, issue2.path);
16029
16665
  } else {
16030
- const fullpath = [...path8, ...issue2.path];
16666
+ const fullpath = [...path7, ...issue2.path];
16031
16667
  if (fullpath.length === 0) {
16032
16668
  result.errors.push(mapper(issue2));
16033
16669
  continue;
@@ -16059,8 +16695,8 @@ function treeifyError(error, _mapper) {
16059
16695
  }
16060
16696
  function toDotPath(_path) {
16061
16697
  const segs = [];
16062
- const path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16063
- for (const seg of path8) {
16698
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16699
+ for (const seg of path7) {
16064
16700
  if (typeof seg === "number")
16065
16701
  segs.push(`[${seg}]`);
16066
16702
  else if (typeof seg === "symbol")
@@ -24838,10 +25474,10 @@ function _property(property, schema, params) {
24838
25474
  ...normalizeParams(params)
24839
25475
  });
24840
25476
  }
24841
- function _mime(types10, params) {
25477
+ function _mime(types11, params) {
24842
25478
  return new $ZodCheckMimeType({
24843
25479
  check: "mime_type",
24844
- mime: types10,
25480
+ mime: types11,
24845
25481
  ...normalizeParams(params)
24846
25482
  });
24847
25483
  }
@@ -26751,7 +27387,7 @@ var ZodFile = /* @__PURE__ */ $constructor("ZodFile", (inst, def) => {
26751
27387
  ZodType.init(inst, def);
26752
27388
  inst.min = (size, params) => inst.check(_minSize(size, params));
26753
27389
  inst.max = (size, params) => inst.check(_maxSize(size, params));
26754
- inst.mime = (types10, params) => inst.check(_mime(Array.isArray(types10) ? types10 : [types10], params));
27390
+ inst.mime = (types11, params) => inst.check(_mime(Array.isArray(types11) ? types11 : [types11], params));
26755
27391
  });
26756
27392
  function file(params) {
26757
27393
  return _file(ZodFile, params);
@@ -27410,7 +28046,7 @@ import { existsSync as existsSync37, statSync as statSync4 } from "fs";
27410
28046
  var {spawn: spawn6 } = globalThis.Bun;
27411
28047
  import { existsSync as existsSync36, mkdirSync as mkdirSync10, chmodSync as chmodSync2, unlinkSync as unlinkSync9 } from "fs";
27412
28048
  import { join as join45 } from "path";
27413
- import { homedir as homedir12 } from "os";
28049
+ import { homedir as homedir11 } from "os";
27414
28050
  import { createRequire as createRequire3 } from "module";
27415
28051
  var REPO2 = "ast-grep/ast-grep";
27416
28052
  var DEFAULT_VERSION = "0.40.0";
@@ -27435,11 +28071,11 @@ var PLATFORM_MAP2 = {
27435
28071
  function getCacheDir3() {
27436
28072
  if (process.platform === "win32") {
27437
28073
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
27438
- const base2 = localAppData || join45(homedir12(), "AppData", "Local");
28074
+ const base2 = localAppData || join45(homedir11(), "AppData", "Local");
27439
28075
  return join45(base2, "oh-my-opencode", "bin");
27440
28076
  }
27441
- const xdgCache2 = process.env.XDG_CACHE_HOME;
27442
- const base = xdgCache2 || join45(homedir12(), ".cache");
28077
+ const xdgCache = process.env.XDG_CACHE_HOME;
28078
+ const base = xdgCache || join45(homedir11(), ".cache");
27443
28079
  return join45(base, "oh-my-opencode", "bin");
27444
28080
  }
27445
28081
  function getBinaryName3() {
@@ -27477,8 +28113,8 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
27477
28113
  if (existsSync36(binaryPath)) {
27478
28114
  return binaryPath;
27479
28115
  }
27480
- const { arch, os: os6 } = platformInfo;
27481
- const assetName = `app-${arch}-${os6}.zip`;
28116
+ const { arch, os: os5 } = platformInfo;
28117
+ const assetName = `app-${arch}-${os5}.zip`;
27482
28118
  const downloadUrl = `https://github.com/${REPO2}/releases/download/${version2}/${assetName}`;
27483
28119
  console.log(`[oh-my-opencode] Downloading ast-grep binary...`);
27484
28120
  try {
@@ -27567,9 +28203,9 @@ function findSgCliPathSync() {
27567
28203
  }
27568
28204
  if (process.platform === "darwin") {
27569
28205
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
27570
- for (const path8 of homebrewPaths) {
27571
- if (existsSync37(path8) && isValidBinary(path8)) {
27572
- return path8;
28206
+ for (const path7 of homebrewPaths) {
28207
+ if (existsSync37(path7) && isValidBinary(path7)) {
28208
+ return path7;
27573
28209
  }
27574
28210
  }
27575
28211
  }
@@ -27587,10 +28223,9 @@ function getSgCliPath() {
27587
28223
  }
27588
28224
  return "sg";
27589
28225
  }
27590
- function setSgCliPath(path8) {
27591
- resolvedCliPath2 = path8;
28226
+ function setSgCliPath(path7) {
28227
+ resolvedCliPath2 = path7;
27592
28228
  }
27593
- var SG_CLI_PATH = getSgCliPath();
27594
28229
  var CLI_LANGUAGES = [
27595
28230
  "bash",
27596
28231
  "c",
@@ -28390,7 +29025,7 @@ function discoverCommandsFromDir(commandsDir, scope) {
28390
29025
  return [];
28391
29026
  }
28392
29027
  const entries = readdirSync15(commandsDir, { withFileTypes: true });
28393
- const commands = [];
29028
+ const commands2 = [];
28394
29029
  for (const entry of entries) {
28395
29030
  if (!isMarkdownFile(entry))
28396
29031
  continue;
@@ -28408,7 +29043,7 @@ function discoverCommandsFromDir(commandsDir, scope) {
28408
29043
  agent: data.agent,
28409
29044
  subtask: Boolean(data.subtask)
28410
29045
  };
28411
- commands.push({
29046
+ commands2.push({
28412
29047
  name: commandName,
28413
29048
  path: commandPath,
28414
29049
  metadata,
@@ -28419,13 +29054,13 @@ function discoverCommandsFromDir(commandsDir, scope) {
28419
29054
  continue;
28420
29055
  }
28421
29056
  }
28422
- return commands;
29057
+ return commands2;
28423
29058
  }
28424
29059
  function discoverCommandsSync() {
28425
- const { homedir: homedir13 } = __require("os");
29060
+ const { homedir: homedir12 } = __require("os");
28426
29061
  const userCommandsDir = join49(getClaudeConfigDir(), "commands");
28427
29062
  const projectCommandsDir = join49(process.cwd(), ".claude", "commands");
28428
- const opencodeGlobalDir = join49(homedir13(), ".config", "opencode", "command");
29063
+ const opencodeGlobalDir = join49(homedir12(), ".config", "opencode", "command");
28429
29064
  const opencodeProjectDir = join49(process.cwd(), ".opencode", "command");
28430
29065
  const userCommands = discoverCommandsFromDir(userCommandsDir, "user");
28431
29066
  const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode");
@@ -28476,18 +29111,18 @@ async function formatLoadedCommand(cmd) {
28476
29111
  return sections.join(`
28477
29112
  `);
28478
29113
  }
28479
- function formatCommandList(commands) {
28480
- if (commands.length === 0) {
29114
+ function formatCommandList(commands2) {
29115
+ if (commands2.length === 0) {
28481
29116
  return "No commands found.";
28482
29117
  }
28483
29118
  const lines = [`# Available Commands
28484
29119
  `];
28485
- for (const cmd of commands) {
29120
+ for (const cmd of commands2) {
28486
29121
  const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : "";
28487
29122
  lines.push(`- **/${cmd.name}${hint}**: ${cmd.metadata.description || "(no description)"} (${cmd.scope})`);
28488
29123
  }
28489
29124
  lines.push(`
28490
- **Total**: ${commands.length} commands`);
29125
+ **Total**: ${commands2.length} commands`);
28491
29126
  return lines.join(`
28492
29127
  `);
28493
29128
  }
@@ -28524,27 +29159,27 @@ ${commandListForDescription}`,
28524
29159
  command: tool.schema.string().describe("The slash command to execute (without the leading slash). E.g., 'commit', 'plan', 'execute'.")
28525
29160
  },
28526
29161
  async execute(args) {
28527
- const commands = discoverCommandsSync();
29162
+ const commands2 = discoverCommandsSync();
28528
29163
  if (!args.command) {
28529
- return formatCommandList(commands) + `
29164
+ return formatCommandList(commands2) + `
28530
29165
 
28531
29166
  Provide a command name to execute.`;
28532
29167
  }
28533
29168
  const cmdName = args.command.replace(/^\//, "");
28534
- const exactMatch = commands.find((cmd) => cmd.name.toLowerCase() === cmdName.toLowerCase());
29169
+ const exactMatch = commands2.find((cmd) => cmd.name.toLowerCase() === cmdName.toLowerCase());
28535
29170
  if (exactMatch) {
28536
29171
  return await formatLoadedCommand(exactMatch);
28537
29172
  }
28538
- const partialMatches = commands.filter((cmd) => cmd.name.toLowerCase().includes(cmdName.toLowerCase()));
29173
+ const partialMatches = commands2.filter((cmd) => cmd.name.toLowerCase().includes(cmdName.toLowerCase()));
28539
29174
  if (partialMatches.length > 0) {
28540
29175
  const matchList = partialMatches.map((cmd) => `/${cmd.name}`).join(", ");
28541
29176
  return `No exact match for "/${cmdName}". Did you mean: ${matchList}?
28542
29177
 
28543
- ` + formatCommandList(commands);
29178
+ ` + formatCommandList(commands2);
28544
29179
  }
28545
29180
  return `Command "/${cmdName}" not found.
28546
29181
 
28547
- ` + formatCommandList(commands) + `
29182
+ ` + formatCommandList(commands2) + `
28548
29183
 
28549
29184
  Try a different command name.`;
28550
29185
  }
@@ -28552,7 +29187,7 @@ Try a different command name.`;
28552
29187
  // src/tools/session-manager/constants.ts
28553
29188
  import { join as join50 } from "path";
28554
29189
  var OPENCODE_STORAGE9 = getOpenCodeStorageDir();
28555
- var MESSAGE_STORAGE8 = join50(OPENCODE_STORAGE9, "message");
29190
+ var MESSAGE_STORAGE4 = join50(OPENCODE_STORAGE9, "message");
28556
29191
  var PART_STORAGE4 = join50(OPENCODE_STORAGE9, "part");
28557
29192
  var TODO_DIR2 = join50(getClaudeConfigDir(), "todos");
28558
29193
  var TRANSCRIPT_DIR2 = join50(getClaudeConfigDir(), "transcripts");
@@ -28628,22 +29263,24 @@ Has Todos: Yes (12 items, 8 completed)
28628
29263
  Has Transcript: Yes (234 entries)`;
28629
29264
 
28630
29265
  // src/tools/session-manager/storage.ts
28631
- import { existsSync as existsSync42, readdirSync as readdirSync16, readFileSync as readFileSync27 } from "fs";
29266
+ import { existsSync as existsSync42, readdirSync as readdirSync16 } from "fs";
29267
+ import { readdir, readFile } from "fs/promises";
28632
29268
  import { join as join51 } from "path";
28633
- function getAllSessions() {
28634
- if (!existsSync42(MESSAGE_STORAGE8))
29269
+ async function getAllSessions() {
29270
+ if (!existsSync42(MESSAGE_STORAGE4))
28635
29271
  return [];
28636
29272
  const sessions = [];
28637
- function scanDirectory(dir) {
29273
+ async function scanDirectory(dir) {
28638
29274
  try {
28639
- for (const entry of readdirSync16(dir, { withFileTypes: true })) {
29275
+ const entries = await readdir(dir, { withFileTypes: true });
29276
+ for (const entry of entries) {
28640
29277
  if (entry.isDirectory()) {
28641
29278
  const sessionPath = join51(dir, entry.name);
28642
- const files = readdirSync16(sessionPath);
29279
+ const files = await readdir(sessionPath);
28643
29280
  if (files.some((f) => f.endsWith(".json"))) {
28644
29281
  sessions.push(entry.name);
28645
29282
  } else {
28646
- scanDirectory(sessionPath);
29283
+ await scanDirectory(sessionPath);
28647
29284
  }
28648
29285
  }
28649
29286
  }
@@ -28651,49 +29288,58 @@ function getAllSessions() {
28651
29288
  return;
28652
29289
  }
28653
29290
  }
28654
- scanDirectory(MESSAGE_STORAGE8);
29291
+ await scanDirectory(MESSAGE_STORAGE4);
28655
29292
  return [...new Set(sessions)];
28656
29293
  }
28657
29294
  function getMessageDir9(sessionID) {
28658
- if (!existsSync42(MESSAGE_STORAGE8))
29295
+ if (!existsSync42(MESSAGE_STORAGE4))
28659
29296
  return "";
28660
- const directPath = join51(MESSAGE_STORAGE8, sessionID);
29297
+ const directPath = join51(MESSAGE_STORAGE4, sessionID);
28661
29298
  if (existsSync42(directPath)) {
28662
29299
  return directPath;
28663
29300
  }
28664
- for (const dir of readdirSync16(MESSAGE_STORAGE8)) {
28665
- const sessionPath = join51(MESSAGE_STORAGE8, dir, sessionID);
28666
- if (existsSync42(sessionPath)) {
28667
- return sessionPath;
29301
+ try {
29302
+ for (const dir of readdirSync16(MESSAGE_STORAGE4)) {
29303
+ const sessionPath = join51(MESSAGE_STORAGE4, dir, sessionID);
29304
+ if (existsSync42(sessionPath)) {
29305
+ return sessionPath;
29306
+ }
28668
29307
  }
29308
+ } catch {
29309
+ return "";
28669
29310
  }
28670
29311
  return "";
28671
29312
  }
28672
29313
  function sessionExists(sessionID) {
28673
29314
  return getMessageDir9(sessionID) !== "";
28674
29315
  }
28675
- function readSessionMessages(sessionID) {
29316
+ async function readSessionMessages(sessionID) {
28676
29317
  const messageDir = getMessageDir9(sessionID);
28677
29318
  if (!messageDir || !existsSync42(messageDir))
28678
29319
  return [];
28679
29320
  const messages = [];
28680
- for (const file2 of readdirSync16(messageDir)) {
28681
- if (!file2.endsWith(".json"))
28682
- continue;
28683
- try {
28684
- const content = readFileSync27(join51(messageDir, file2), "utf-8");
28685
- const meta = JSON.parse(content);
28686
- const parts = readParts2(meta.id);
28687
- messages.push({
28688
- id: meta.id,
28689
- role: meta.role,
28690
- agent: meta.agent,
28691
- time: meta.time,
28692
- parts
28693
- });
28694
- } catch {
28695
- continue;
29321
+ try {
29322
+ const files = await readdir(messageDir);
29323
+ for (const file2 of files) {
29324
+ if (!file2.endsWith(".json"))
29325
+ continue;
29326
+ try {
29327
+ const content = await readFile(join51(messageDir, file2), "utf-8");
29328
+ const meta = JSON.parse(content);
29329
+ const parts = await readParts2(meta.id);
29330
+ messages.push({
29331
+ id: meta.id,
29332
+ role: meta.role,
29333
+ agent: meta.agent,
29334
+ time: meta.time,
29335
+ parts
29336
+ });
29337
+ } catch {
29338
+ continue;
29339
+ }
28696
29340
  }
29341
+ } catch {
29342
+ return [];
28697
29343
  }
28698
29344
  return messages.sort((a, b) => {
28699
29345
  const aTime = a.time?.created ?? 0;
@@ -28703,61 +29349,71 @@ function readSessionMessages(sessionID) {
28703
29349
  return a.id.localeCompare(b.id);
28704
29350
  });
28705
29351
  }
28706
- function readParts2(messageID) {
29352
+ async function readParts2(messageID) {
28707
29353
  const partDir = join51(PART_STORAGE4, messageID);
28708
29354
  if (!existsSync42(partDir))
28709
29355
  return [];
28710
29356
  const parts = [];
28711
- for (const file2 of readdirSync16(partDir)) {
28712
- if (!file2.endsWith(".json"))
28713
- continue;
28714
- try {
28715
- const content = readFileSync27(join51(partDir, file2), "utf-8");
28716
- parts.push(JSON.parse(content));
28717
- } catch {
28718
- continue;
29357
+ try {
29358
+ const files = await readdir(partDir);
29359
+ for (const file2 of files) {
29360
+ if (!file2.endsWith(".json"))
29361
+ continue;
29362
+ try {
29363
+ const content = await readFile(join51(partDir, file2), "utf-8");
29364
+ parts.push(JSON.parse(content));
29365
+ } catch {
29366
+ continue;
29367
+ }
28719
29368
  }
29369
+ } catch {
29370
+ return [];
28720
29371
  }
28721
29372
  return parts.sort((a, b) => a.id.localeCompare(b.id));
28722
29373
  }
28723
- function readSessionTodos(sessionID) {
29374
+ async function readSessionTodos(sessionID) {
28724
29375
  if (!existsSync42(TODO_DIR2))
28725
29376
  return [];
28726
- const todoFiles = readdirSync16(TODO_DIR2).filter((f) => f.includes(sessionID) && f.endsWith(".json"));
28727
- for (const file2 of todoFiles) {
28728
- try {
28729
- const content = readFileSync27(join51(TODO_DIR2, file2), "utf-8");
28730
- const data = JSON.parse(content);
28731
- if (Array.isArray(data)) {
28732
- return data.map((item) => ({
28733
- id: item.id || "",
28734
- content: item.content || "",
28735
- status: item.status || "pending",
28736
- priority: item.priority
28737
- }));
29377
+ try {
29378
+ const allFiles = await readdir(TODO_DIR2);
29379
+ const todoFiles = allFiles.filter((f) => f.includes(sessionID) && f.endsWith(".json"));
29380
+ for (const file2 of todoFiles) {
29381
+ try {
29382
+ const content = await readFile(join51(TODO_DIR2, file2), "utf-8");
29383
+ const data = JSON.parse(content);
29384
+ if (Array.isArray(data)) {
29385
+ return data.map((item) => ({
29386
+ id: item.id || "",
29387
+ content: item.content || "",
29388
+ status: item.status || "pending",
29389
+ priority: item.priority
29390
+ }));
29391
+ }
29392
+ } catch {
29393
+ continue;
28738
29394
  }
28739
- } catch {
28740
- continue;
28741
29395
  }
29396
+ } catch {
29397
+ return [];
28742
29398
  }
28743
29399
  return [];
28744
29400
  }
28745
- function readSessionTranscript(sessionID) {
29401
+ async function readSessionTranscript(sessionID) {
28746
29402
  if (!existsSync42(TRANSCRIPT_DIR2))
28747
29403
  return 0;
28748
29404
  const transcriptFile = join51(TRANSCRIPT_DIR2, `${sessionID}.jsonl`);
28749
29405
  if (!existsSync42(transcriptFile))
28750
29406
  return 0;
28751
29407
  try {
28752
- const content = readFileSync27(transcriptFile, "utf-8");
29408
+ const content = await readFile(transcriptFile, "utf-8");
28753
29409
  return content.trim().split(`
28754
29410
  `).filter(Boolean).length;
28755
29411
  } catch {
28756
29412
  return 0;
28757
29413
  }
28758
29414
  }
28759
- function getSessionInfo(sessionID) {
28760
- const messages = readSessionMessages(sessionID);
29415
+ async function getSessionInfo(sessionID) {
29416
+ const messages = await readSessionMessages(sessionID);
28761
29417
  if (messages.length === 0)
28762
29418
  return null;
28763
29419
  const agentsUsed = new Set;
@@ -28774,8 +29430,8 @@ function getSessionInfo(sessionID) {
28774
29430
  lastMessage = date5;
28775
29431
  }
28776
29432
  }
28777
- const todos = readSessionTodos(sessionID);
28778
- const transcriptEntries = readSessionTranscript(sessionID);
29433
+ const todos = await readSessionTodos(sessionID);
29434
+ const transcriptEntries = await readSessionTranscript(sessionID);
28779
29435
  return {
28780
29436
  id: sessionID,
28781
29437
  message_count: messages.length,
@@ -28790,11 +29446,11 @@ function getSessionInfo(sessionID) {
28790
29446
  }
28791
29447
 
28792
29448
  // src/tools/session-manager/utils.ts
28793
- function formatSessionList(sessionIDs) {
29449
+ async function formatSessionList(sessionIDs) {
28794
29450
  if (sessionIDs.length === 0) {
28795
29451
  return "No sessions found.";
28796
29452
  }
28797
- const infos = sessionIDs.map((id) => getSessionInfo(id)).filter((info) => info !== null);
29453
+ const infos = (await Promise.all(sessionIDs.map((id) => getSessionInfo(id)))).filter((info) => info !== null);
28798
29454
  if (infos.length === 0) {
28799
29455
  return "No valid sessions found.";
28800
29456
  }
@@ -28886,29 +29542,33 @@ function formatSearchResults(results) {
28886
29542
  return lines.join(`
28887
29543
  `);
28888
29544
  }
28889
- function filterSessionsByDate(sessionIDs, fromDate, toDate) {
29545
+ async function filterSessionsByDate(sessionIDs, fromDate, toDate) {
28890
29546
  if (!fromDate && !toDate)
28891
29547
  return sessionIDs;
28892
29548
  const from = fromDate ? new Date(fromDate) : null;
28893
29549
  const to = toDate ? new Date(toDate) : null;
28894
- return sessionIDs.filter((id) => {
28895
- const info = getSessionInfo(id);
29550
+ const results = [];
29551
+ for (const id of sessionIDs) {
29552
+ const info = await getSessionInfo(id);
28896
29553
  if (!info || !info.last_message)
28897
- return false;
29554
+ continue;
28898
29555
  if (from && info.last_message < from)
28899
- return false;
29556
+ continue;
28900
29557
  if (to && info.last_message > to)
28901
- return false;
28902
- return true;
28903
- });
29558
+ continue;
29559
+ results.push(id);
29560
+ }
29561
+ return results;
28904
29562
  }
28905
- function searchInSession(sessionID, query, caseSensitive = false) {
28906
- const messages = readSessionMessages(sessionID);
29563
+ async function searchInSession(sessionID, query, caseSensitive = false, maxResults) {
29564
+ const messages = await readSessionMessages(sessionID);
28907
29565
  const results = [];
28908
29566
  const searchQuery = caseSensitive ? query : query.toLowerCase();
28909
29567
  for (const msg of messages) {
29568
+ if (maxResults && results.length >= maxResults)
29569
+ break;
28910
29570
  let matchCount = 0;
28911
- let excerpts = [];
29571
+ const excerpts = [];
28912
29572
  for (const part of msg.parts) {
28913
29573
  if (part.type === "text" && part.text) {
28914
29574
  const text = caseSensitive ? part.text : part.text.toLowerCase();
@@ -28944,6 +29604,14 @@ function searchInSession(sessionID, query, caseSensitive = false) {
28944
29604
  }
28945
29605
 
28946
29606
  // src/tools/session-manager/tools.ts
29607
+ var SEARCH_TIMEOUT_MS = 60000;
29608
+ var MAX_SESSIONS_TO_SCAN = 50;
29609
+ function withTimeout(promise2, ms, operation) {
29610
+ return Promise.race([
29611
+ promise2,
29612
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`${operation} timed out after ${ms}ms`)), ms))
29613
+ ]);
29614
+ }
28947
29615
  var session_list = tool({
28948
29616
  description: SESSION_LIST_DESCRIPTION,
28949
29617
  args: {
@@ -28953,14 +29621,14 @@ var session_list = tool({
28953
29621
  },
28954
29622
  execute: async (args, _context) => {
28955
29623
  try {
28956
- let sessions = getAllSessions();
29624
+ let sessions = await getAllSessions();
28957
29625
  if (args.from_date || args.to_date) {
28958
- sessions = filterSessionsByDate(sessions, args.from_date, args.to_date);
29626
+ sessions = await filterSessionsByDate(sessions, args.from_date, args.to_date);
28959
29627
  }
28960
29628
  if (args.limit && args.limit > 0) {
28961
29629
  sessions = sessions.slice(0, args.limit);
28962
29630
  }
28963
- return formatSessionList(sessions);
29631
+ return await formatSessionList(sessions);
28964
29632
  } catch (e) {
28965
29633
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
28966
29634
  }
@@ -28979,11 +29647,11 @@ var session_read = tool({
28979
29647
  if (!sessionExists(args.session_id)) {
28980
29648
  return `Session not found: ${args.session_id}`;
28981
29649
  }
28982
- let messages = readSessionMessages(args.session_id);
29650
+ let messages = await readSessionMessages(args.session_id);
28983
29651
  if (args.limit && args.limit > 0) {
28984
29652
  messages = messages.slice(0, args.limit);
28985
29653
  }
28986
- const todos = args.include_todos ? readSessionTodos(args.session_id) : undefined;
29654
+ const todos = args.include_todos ? await readSessionTodos(args.session_id) : undefined;
28987
29655
  return formatSessionMessages(messages, args.include_todos, todos);
28988
29656
  } catch (e) {
28989
29657
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
@@ -29000,10 +29668,25 @@ var session_search = tool({
29000
29668
  },
29001
29669
  execute: async (args, _context) => {
29002
29670
  try {
29003
- const sessions = args.session_id ? [args.session_id] : getAllSessions();
29004
- const allResults = sessions.flatMap((sid) => searchInSession(sid, args.query, args.case_sensitive));
29005
- const limited = args.limit && args.limit > 0 ? allResults.slice(0, args.limit) : allResults.slice(0, 20);
29006
- return formatSearchResults(limited);
29671
+ const resultLimit = args.limit && args.limit > 0 ? args.limit : 20;
29672
+ const searchOperation = async () => {
29673
+ if (args.session_id) {
29674
+ return searchInSession(args.session_id, args.query, args.case_sensitive, resultLimit);
29675
+ }
29676
+ const allSessions = await getAllSessions();
29677
+ const sessionsToScan = allSessions.slice(0, MAX_SESSIONS_TO_SCAN);
29678
+ const allResults = [];
29679
+ for (const sid of sessionsToScan) {
29680
+ if (allResults.length >= resultLimit)
29681
+ break;
29682
+ const remaining = resultLimit - allResults.length;
29683
+ const sessionResults = await searchInSession(sid, args.query, args.case_sensitive, remaining);
29684
+ allResults.push(...sessionResults);
29685
+ }
29686
+ return allResults.slice(0, resultLimit);
29687
+ };
29688
+ const results = await withTimeout(searchOperation(), SEARCH_TIMEOUT_MS, "Search");
29689
+ return formatSearchResults(results);
29007
29690
  } catch (e) {
29008
29691
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
29009
29692
  }
@@ -29016,7 +29699,7 @@ var session_info = tool({
29016
29699
  },
29017
29700
  execute: async (args, _context) => {
29018
29701
  try {
29019
- const info = getSessionInfo(args.session_id);
29702
+ const info = await getSessionInfo(args.session_id);
29020
29703
  if (!info) {
29021
29704
  return `Session not found: ${args.session_id}`;
29022
29705
  }
@@ -29059,12 +29742,12 @@ async function findTmuxPath() {
29059
29742
  return null;
29060
29743
  }
29061
29744
  const stdout = await new Response(proc.stdout).text();
29062
- const path8 = stdout.trim().split(`
29745
+ const path7 = stdout.trim().split(`
29063
29746
  `)[0];
29064
- if (!path8) {
29747
+ if (!path7) {
29065
29748
  return null;
29066
29749
  }
29067
- const verifyProc = spawn10([path8, "-V"], {
29750
+ const verifyProc = spawn10([path7, "-V"], {
29068
29751
  stdout: "pipe",
29069
29752
  stderr: "pipe"
29070
29753
  });
@@ -29072,7 +29755,7 @@ async function findTmuxPath() {
29072
29755
  if (verifyExitCode !== 0) {
29073
29756
  return null;
29074
29757
  }
29075
- return path8;
29758
+ return path7;
29076
29759
  } catch {
29077
29760
  return null;
29078
29761
  }
@@ -29085,9 +29768,9 @@ async function getTmuxPath() {
29085
29768
  return initPromise3;
29086
29769
  }
29087
29770
  initPromise3 = (async () => {
29088
- const path8 = await findTmuxPath();
29089
- tmuxPath = path8;
29090
- return path8;
29771
+ const path7 = await findTmuxPath();
29772
+ tmuxPath = path7;
29773
+ return path7;
29091
29774
  })();
29092
29775
  return initPromise3;
29093
29776
  }
@@ -29224,21 +29907,26 @@ function createBackgroundTask(manager) {
29224
29907
  agent: tool.schema.string().describe("Agent type to use (any registered agent)")
29225
29908
  },
29226
29909
  async execute(args, toolContext) {
29910
+ const ctx = toolContext;
29227
29911
  if (!args.agent || args.agent.trim() === "") {
29228
29912
  return `\u274C Agent parameter is required. Please specify which agent to use (e.g., "explore", "librarian", "build", etc.)`;
29229
29913
  }
29230
29914
  try {
29231
- const messageDir = getMessageDir10(toolContext.sessionID);
29915
+ const messageDir = getMessageDir10(ctx.sessionID);
29232
29916
  const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null;
29233
29917
  const parentModel = prevMessage?.model?.providerID && prevMessage?.model?.modelID ? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID } : undefined;
29234
29918
  const task = await manager.launch({
29235
29919
  description: args.description,
29236
29920
  prompt: args.prompt,
29237
29921
  agent: args.agent.trim(),
29238
- parentSessionID: toolContext.sessionID,
29239
- parentMessageID: toolContext.messageID,
29922
+ parentSessionID: ctx.sessionID,
29923
+ parentMessageID: ctx.messageID,
29240
29924
  parentModel
29241
29925
  });
29926
+ ctx.metadata?.({
29927
+ title: args.description,
29928
+ metadata: { sessionId: task.sessionID }
29929
+ });
29242
29930
  return `Background task launched successfully.
29243
29931
 
29244
29932
  Task ID: ${task.id}
@@ -29494,6 +30182,7 @@ function createCallOmoAgent(ctx, backgroundManager) {
29494
30182
  session_id: tool.schema.string().describe("Existing Task session to continue").optional()
29495
30183
  },
29496
30184
  async execute(args, toolContext) {
30185
+ const toolCtx = toolContext;
29497
30186
  log(`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`);
29498
30187
  if (!ALLOWED_AGENTS.includes(args.subagent_type)) {
29499
30188
  return `Error: Invalid agent type "${args.subagent_type}". Only ${ALLOWED_AGENTS.join(", ")} are allowed.`;
@@ -29502,9 +30191,9 @@ function createCallOmoAgent(ctx, backgroundManager) {
29502
30191
  if (args.session_id) {
29503
30192
  return `Error: session_id is not supported in background mode. Use run_in_background=false to continue an existing session.`;
29504
30193
  }
29505
- return await executeBackground(args, toolContext, backgroundManager);
30194
+ return await executeBackground(args, toolCtx, backgroundManager);
29506
30195
  }
29507
- return await executeSync(args, toolContext, ctx);
30196
+ return await executeSync(args, toolCtx, ctx);
29508
30197
  }
29509
30198
  });
29510
30199
  }
@@ -29517,6 +30206,10 @@ async function executeBackground(args, toolContext, manager) {
29517
30206
  parentSessionID: toolContext.sessionID,
29518
30207
  parentMessageID: toolContext.messageID
29519
30208
  });
30209
+ toolContext.metadata?.({
30210
+ title: args.description,
30211
+ metadata: { sessionId: task.sessionID }
30212
+ });
29520
30213
  return `Background agent task launched successfully.
29521
30214
 
29522
30215
  Task ID: ${task.id}
@@ -29561,6 +30254,10 @@ async function executeSync(args, toolContext, ctx) {
29561
30254
  sessionID = createResult.data.id;
29562
30255
  log(`[call_omo_agent] Created session: ${sessionID}`);
29563
30256
  }
30257
+ toolContext.metadata?.({
30258
+ title: args.description,
30259
+ metadata: { sessionId: sessionID }
30260
+ });
29564
30261
  log(`[call_omo_agent] Sending prompt to session ${sessionID}`);
29565
30262
  log(`[call_omo_agent] Prompt text:`, args.prompt.substring(0, 100));
29566
30263
  try {
@@ -30006,6 +30703,7 @@ class BackgroundManager {
30006
30703
  }
30007
30704
  const message = `[BACKGROUND TASK COMPLETED] Task "${task.description}" finished in ${duration3}. Use background_output with task_id="${task.id}" to get results.`;
30008
30705
  log("[background-agent] Sending notification to parent session:", { parentSessionID: task.parentSessionID });
30706
+ const taskId = task.id;
30009
30707
  setTimeout(async () => {
30010
30708
  try {
30011
30709
  const messageDir = getMessageDir11(task.parentSessionID);
@@ -30021,10 +30719,13 @@ class BackgroundManager {
30021
30719
  },
30022
30720
  query: { directory: this.directory }
30023
30721
  });
30024
- this.clearNotificationsForTask(task.id);
30722
+ this.clearNotificationsForTask(taskId);
30025
30723
  log("[background-agent] Successfully sent prompt to parent session:", { parentSessionID: task.parentSessionID });
30026
30724
  } catch (error45) {
30027
30725
  log("[background-agent] prompt failed:", String(error45));
30726
+ } finally {
30727
+ this.tasks.delete(taskId);
30728
+ log("[background-agent] Removed completed task from memory:", taskId);
30028
30729
  }
30029
30730
  }, 200);
30030
30731
  }
@@ -30212,6 +30913,9 @@ var HookNameSchema = exports_external.enum([
30212
30913
  "empty-message-sanitizer",
30213
30914
  "thinking-block-validator"
30214
30915
  ]);
30916
+ var BuiltinCommandNameSchema = exports_external.enum([
30917
+ "init-deep"
30918
+ ]);
30215
30919
  var AgentOverrideConfigSchema = exports_external.object({
30216
30920
  model: exports_external.string().optional(),
30217
30921
  temperature: exports_external.number().min(0).max(2).optional(),
@@ -30253,6 +30957,9 @@ var SisyphusAgentConfigSchema = exports_external.object({
30253
30957
  planner_enabled: exports_external.boolean().optional(),
30254
30958
  replace_plan: exports_external.boolean().optional()
30255
30959
  });
30960
+ var CommentCheckerConfigSchema = exports_external.object({
30961
+ custom_prompt: exports_external.string().optional()
30962
+ });
30256
30963
  var DynamicContextPruningConfigSchema = exports_external.object({
30257
30964
  enabled: exports_external.boolean().default(false),
30258
30965
  notification: exports_external.enum(["off", "minimal", "detailed"]).default("detailed"),
@@ -30291,17 +30998,19 @@ var ExperimentalConfigSchema = exports_external.object({
30291
30998
  preemptive_compaction_threshold: exports_external.number().min(0.5).max(0.95).optional(),
30292
30999
  truncate_all_tool_outputs: exports_external.boolean().default(true),
30293
31000
  dynamic_context_pruning: DynamicContextPruningConfigSchema.optional(),
30294
- dcp_on_compaction_failure: exports_external.boolean().optional()
31001
+ dcp_for_compaction: exports_external.boolean().optional()
30295
31002
  });
30296
31003
  var OhMyOpenCodeConfigSchema = exports_external.object({
30297
31004
  $schema: exports_external.string().optional(),
30298
31005
  disabled_mcps: exports_external.array(McpNameSchema).optional(),
30299
31006
  disabled_agents: exports_external.array(BuiltinAgentNameSchema).optional(),
30300
31007
  disabled_hooks: exports_external.array(HookNameSchema).optional(),
31008
+ disabled_commands: exports_external.array(BuiltinCommandNameSchema).optional(),
30301
31009
  agents: AgentOverridesSchema.optional(),
30302
31010
  claude_code: ClaudeCodeConfigSchema.optional(),
30303
31011
  google_auth: exports_external.boolean().optional(),
30304
31012
  sisyphus_agent: SisyphusAgentConfigSchema.optional(),
31013
+ comment_checker: CommentCheckerConfigSchema.optional(),
30305
31014
  experimental: ExperimentalConfigSchema.optional(),
30306
31015
  auto_update: exports_external.boolean().optional()
30307
31016
  });
@@ -30378,7 +31087,7 @@ var PLAN_PERMISSION = {
30378
31087
 
30379
31088
  // src/index.ts
30380
31089
  import * as fs7 from "fs";
30381
- import * as path8 from "path";
31090
+ import * as path7 from "path";
30382
31091
  var AGENT_NAME_MAP = {
30383
31092
  omo: "Sisyphus",
30384
31093
  OmO: "Sisyphus",
@@ -30477,14 +31186,20 @@ function mergeConfigs(base, override) {
30477
31186
  ...override.disabled_hooks ?? []
30478
31187
  ])
30479
31188
  ],
31189
+ disabled_commands: [
31190
+ ...new Set([
31191
+ ...base.disabled_commands ?? [],
31192
+ ...override.disabled_commands ?? []
31193
+ ])
31194
+ ],
30480
31195
  claude_code: deepMerge(base.claude_code, override.claude_code)
30481
31196
  };
30482
31197
  }
30483
31198
  function loadPluginConfig(directory, ctx) {
30484
- const userBasePath = path8.join(getUserConfigDir(), "opencode", "oh-my-opencode");
31199
+ const userBasePath = path7.join(getUserConfigDir(), "opencode", "oh-my-opencode");
30485
31200
  const userDetected = detectConfigFile(userBasePath);
30486
31201
  const userConfigPath = userDetected.format !== "none" ? userDetected.path : userBasePath + ".json";
30487
- const projectBasePath = path8.join(directory, ".opencode", "oh-my-opencode");
31202
+ const projectBasePath = path7.join(directory, ".opencode", "oh-my-opencode");
30488
31203
  const projectDetected = detectConfigFile(projectBasePath);
30489
31204
  const projectConfigPath = projectDetected.format !== "none" ? projectDetected.path : projectBasePath + ".json";
30490
31205
  let config3 = loadConfigFromPath2(userConfigPath, ctx) ?? {};
@@ -30520,7 +31235,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
30520
31235
  const contextWindowMonitor = isHookEnabled("context-window-monitor") ? createContextWindowMonitorHook(ctx) : null;
30521
31236
  const sessionRecovery = isHookEnabled("session-recovery") ? createSessionRecoveryHook(ctx, { experimental: pluginConfig.experimental }) : null;
30522
31237
  const sessionNotification = isHookEnabled("session-notification") ? createSessionNotification(ctx) : null;
30523
- const commentChecker = isHookEnabled("comment-checker") ? createCommentCheckerHooks() : null;
31238
+ const commentChecker = isHookEnabled("comment-checker") ? createCommentCheckerHooks(pluginConfig.comment_checker) : null;
30524
31239
  const toolOutputTruncator = isHookEnabled("tool-output-truncator") ? createToolOutputTruncatorHook(ctx, { experimental: pluginConfig.experimental }) : null;
30525
31240
  const directoryAgentsInjector = isHookEnabled("directory-agents-injector") ? createDirectoryAgentsInjectorHook(ctx) : null;
30526
31241
  const directoryReadmeInjector = isHookEnabled("directory-readme-injector") ? createDirectoryReadmeInjectorHook(ctx) : null;
@@ -30700,12 +31415,14 @@ var OhMyOpenCodePlugin = async (ctx) => {
30700
31415
  ...mcpResult.servers,
30701
31416
  ...pluginComponents.mcpServers
30702
31417
  };
31418
+ const builtinCommands = loadBuiltinCommands(pluginConfig.disabled_commands);
30703
31419
  const userCommands = pluginConfig.claude_code?.commands ?? true ? loadUserCommands() : {};
30704
31420
  const opencodeGlobalCommands = loadOpencodeGlobalCommands();
30705
31421
  const systemCommands = config3.command ?? {};
30706
31422
  const projectCommands = pluginConfig.claude_code?.commands ?? true ? loadProjectCommands() : {};
30707
31423
  const opencodeProjectCommands = loadOpencodeProjectCommands();
30708
31424
  config3.command = {
31425
+ ...builtinCommands,
30709
31426
  ...userCommands,
30710
31427
  ...opencodeGlobalCommands,
30711
31428
  ...systemCommands,