oh-my-opencode 2.6.2 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 MIN_INJECTION_INTERVAL_MS = 1e4;
4403
4386
  function getMessageDir(sessionID) {
4404
4387
  if (!existsSync6(MESSAGE_STORAGE))
4405
4388
  return null;
@@ -4433,39 +4416,216 @@ 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 getOrCreateState(sessionID) {
4426
+ let state2 = sessions.get(sessionID);
4427
+ if (!state2) {
4428
+ state2 = { version: 0, mode: "idle" };
4429
+ sessions.set(sessionID, state2);
4430
+ }
4431
+ return state2;
4432
+ }
4433
+ function clearTimer(state2) {
4434
+ if (state2.timer) {
4435
+ clearTimeout(state2.timer);
4436
+ state2.timer = undefined;
4437
+ }
4438
+ }
4439
+ function invalidate(sessionID, reason) {
4440
+ const state2 = sessions.get(sessionID);
4441
+ if (!state2)
4442
+ return;
4443
+ if (state2.mode === "recovering")
4444
+ return;
4445
+ state2.version++;
4446
+ clearTimer(state2);
4447
+ if (state2.mode !== "idle" && state2.mode !== "errorBypass") {
4448
+ log(`[${HOOK_NAME}] Invalidated`, { sessionID, reason, prevMode: state2.mode, newVersion: state2.version });
4449
+ state2.mode = "idle";
4450
+ }
4451
+ }
4452
+ function isMainSession(sessionID) {
4453
+ const mainSessionID2 = getMainSessionID();
4454
+ return !mainSessionID2 || sessionID === mainSessionID2;
4455
+ }
4446
4456
  const markRecovering = (sessionID) => {
4447
- recoveringSessions.add(sessionID);
4457
+ const state2 = getOrCreateState(sessionID);
4458
+ invalidate(sessionID, "entering recovery mode");
4459
+ state2.mode = "recovering";
4460
+ log(`[${HOOK_NAME}] Session marked as recovering`, { sessionID });
4448
4461
  };
4449
4462
  const markRecoveryComplete = (sessionID) => {
4450
- recoveringSessions.delete(sessionID);
4463
+ const state2 = sessions.get(sessionID);
4464
+ if (state2 && state2.mode === "recovering") {
4465
+ state2.mode = "idle";
4466
+ log(`[${HOOK_NAME}] Session recovery complete`, { sessionID });
4467
+ }
4451
4468
  };
4469
+ async function showCountdownToast(seconds, incompleteCount) {
4470
+ await ctx.client.tui.showToast({
4471
+ body: {
4472
+ title: "Todo Continuation",
4473
+ message: `Resuming in ${seconds}s... (${incompleteCount} tasks remaining)`,
4474
+ variant: "warning",
4475
+ duration: TOAST_DURATION_MS
4476
+ }
4477
+ }).catch(() => {});
4478
+ }
4479
+ async function executeInjection(sessionID, capturedVersion) {
4480
+ const state2 = sessions.get(sessionID);
4481
+ if (!state2)
4482
+ return;
4483
+ if (state2.version !== capturedVersion) {
4484
+ log(`[${HOOK_NAME}] Injection aborted: version mismatch`, {
4485
+ sessionID,
4486
+ capturedVersion,
4487
+ currentVersion: state2.version
4488
+ });
4489
+ return;
4490
+ }
4491
+ if (state2.mode !== "countingDown") {
4492
+ log(`[${HOOK_NAME}] Injection aborted: mode changed`, {
4493
+ sessionID,
4494
+ mode: state2.mode
4495
+ });
4496
+ return;
4497
+ }
4498
+ if (state2.lastAttemptedAt) {
4499
+ const elapsed = Date.now() - state2.lastAttemptedAt;
4500
+ if (elapsed < MIN_INJECTION_INTERVAL_MS) {
4501
+ log(`[${HOOK_NAME}] Injection throttled: too soon since last injection`, {
4502
+ sessionID,
4503
+ elapsedMs: elapsed,
4504
+ minIntervalMs: MIN_INJECTION_INTERVAL_MS
4505
+ });
4506
+ state2.mode = "idle";
4507
+ return;
4508
+ }
4509
+ }
4510
+ state2.mode = "injecting";
4511
+ let todos = [];
4512
+ try {
4513
+ const response = await ctx.client.session.todo({ path: { id: sessionID } });
4514
+ todos = response.data ?? response;
4515
+ } catch (err) {
4516
+ log(`[${HOOK_NAME}] Failed to fetch todos for injection`, { sessionID, error: String(err) });
4517
+ state2.mode = "idle";
4518
+ return;
4519
+ }
4520
+ if (state2.version !== capturedVersion) {
4521
+ log(`[${HOOK_NAME}] Injection aborted after todo fetch: version mismatch`, { sessionID });
4522
+ state2.mode = "idle";
4523
+ return;
4524
+ }
4525
+ const incompleteCount = getIncompleteCount(todos);
4526
+ if (incompleteCount === 0) {
4527
+ log(`[${HOOK_NAME}] No incomplete todos at injection time`, { sessionID, total: todos.length });
4528
+ state2.mode = "idle";
4529
+ return;
4530
+ }
4531
+ const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((t) => t.status === "running") : false;
4532
+ if (hasRunningBgTasks) {
4533
+ log(`[${HOOK_NAME}] Skipped: background tasks still running`, { sessionID });
4534
+ state2.mode = "idle";
4535
+ return;
4536
+ }
4537
+ const messageDir = getMessageDir(sessionID);
4538
+ const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null;
4539
+ const agentHasWritePermission = !prevMessage?.tools || prevMessage.tools.write !== false && prevMessage.tools.edit !== false;
4540
+ if (!agentHasWritePermission) {
4541
+ log(`[${HOOK_NAME}] Skipped: agent lacks write permission`, {
4542
+ sessionID,
4543
+ agent: prevMessage?.agent,
4544
+ tools: prevMessage?.tools
4545
+ });
4546
+ state2.mode = "idle";
4547
+ return;
4548
+ }
4549
+ const agentName = prevMessage?.agent?.toLowerCase() ?? "";
4550
+ const isPlanModeAgent = agentName === "plan" || agentName === "planner-sisyphus";
4551
+ if (isPlanModeAgent) {
4552
+ log(`[${HOOK_NAME}] Skipped: plan mode agent detected`, {
4553
+ sessionID,
4554
+ agent: prevMessage?.agent
4555
+ });
4556
+ state2.mode = "idle";
4557
+ return;
4558
+ }
4559
+ const prompt = `${CONTINUATION_PROMPT}
4560
+
4561
+ [Status: ${todos.length - incompleteCount}/${todos.length} completed, ${incompleteCount} remaining]`;
4562
+ if (state2.version !== capturedVersion) {
4563
+ log(`[${HOOK_NAME}] Injection aborted: version changed before API call`, { sessionID });
4564
+ state2.mode = "idle";
4565
+ return;
4566
+ }
4567
+ state2.lastAttemptedAt = Date.now();
4568
+ try {
4569
+ log(`[${HOOK_NAME}] Injecting continuation prompt`, {
4570
+ sessionID,
4571
+ agent: prevMessage?.agent,
4572
+ incompleteCount
4573
+ });
4574
+ await ctx.client.session.prompt({
4575
+ path: { id: sessionID },
4576
+ body: {
4577
+ agent: prevMessage?.agent,
4578
+ parts: [{ type: "text", text: prompt }]
4579
+ },
4580
+ query: { directory: ctx.directory }
4581
+ });
4582
+ log(`[${HOOK_NAME}] Continuation prompt injected successfully`, { sessionID });
4583
+ } catch (err) {
4584
+ log(`[${HOOK_NAME}] Prompt injection failed`, { sessionID, error: String(err) });
4585
+ }
4586
+ state2.mode = "idle";
4587
+ }
4588
+ function startCountdown(sessionID, incompleteCount) {
4589
+ const state2 = getOrCreateState(sessionID);
4590
+ invalidate(sessionID, "starting new countdown");
4591
+ state2.version++;
4592
+ state2.mode = "countingDown";
4593
+ const capturedVersion = state2.version;
4594
+ log(`[${HOOK_NAME}] Starting countdown`, {
4595
+ sessionID,
4596
+ seconds: COUNTDOWN_SECONDS,
4597
+ version: capturedVersion,
4598
+ incompleteCount
4599
+ });
4600
+ showCountdownToast(COUNTDOWN_SECONDS, incompleteCount);
4601
+ let secondsRemaining = COUNTDOWN_SECONDS;
4602
+ const toastInterval = setInterval(() => {
4603
+ if (state2.version !== capturedVersion) {
4604
+ clearInterval(toastInterval);
4605
+ return;
4606
+ }
4607
+ secondsRemaining--;
4608
+ if (secondsRemaining > 0) {
4609
+ showCountdownToast(secondsRemaining, incompleteCount);
4610
+ }
4611
+ }, 1000);
4612
+ state2.timer = setTimeout(() => {
4613
+ clearInterval(toastInterval);
4614
+ clearTimer(state2);
4615
+ executeInjection(sessionID, capturedVersion);
4616
+ }, COUNTDOWN_SECONDS * 1000);
4617
+ }
4452
4618
  const handler = async ({ event }) => {
4453
4619
  const props = event.properties;
4454
4620
  if (event.type === "session.error") {
4455
4621
  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
- }
4622
+ if (!sessionID)
4623
+ return;
4624
+ const isInterrupt = detectInterrupt(props?.error);
4625
+ const state2 = getOrCreateState(sessionID);
4626
+ invalidate(sessionID, isInterrupt ? "user interrupt" : "session error");
4627
+ state2.mode = "errorBypass";
4628
+ log(`[${HOOK_NAME}] session.error received`, { sessionID, isInterrupt, error: props?.error });
4469
4629
  return;
4470
4630
  }
4471
4631
  if (event.type === "session.idle") {
@@ -4473,40 +4633,27 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
4473
4633
  if (!sessionID)
4474
4634
  return;
4475
4635
  log(`[${HOOK_NAME}] session.idle received`, { sessionID });
4476
- const mainSessionID2 = getMainSessionID();
4477
- if (mainSessionID2 && sessionID !== mainSessionID2) {
4478
- log(`[${HOOK_NAME}] Skipped: not main session`, { sessionID, mainSessionID: mainSessionID2 });
4636
+ if (!isMainSession(sessionID)) {
4637
+ log(`[${HOOK_NAME}] Skipped: not main session`, { sessionID });
4479
4638
  return;
4480
4639
  }
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)) {
4640
+ const state2 = getOrCreateState(sessionID);
4641
+ if (state2.mode === "recovering") {
4488
4642
  log(`[${HOOK_NAME}] Skipped: session in recovery mode`, { sessionID });
4489
4643
  return;
4490
4644
  }
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 });
4645
+ if (state2.mode === "errorBypass") {
4646
+ log(`[${HOOK_NAME}] Skipped: error bypass (awaiting user message to resume)`, { sessionID });
4496
4647
  return;
4497
4648
  }
4498
- if (remindedSessions.has(sessionID)) {
4499
- log(`[${HOOK_NAME}] Skipped: already reminded this session`, { sessionID });
4649
+ if (state2.mode === "countingDown" || state2.mode === "injecting") {
4650
+ log(`[${HOOK_NAME}] Skipped: already ${state2.mode}`, { sessionID });
4500
4651
  return;
4501
4652
  }
4502
4653
  let todos = [];
4503
4654
  try {
4504
- log(`[${HOOK_NAME}] Fetching todos for session`, { sessionID });
4505
- const response = await ctx.client.session.todo({
4506
- path: { id: sessionID }
4507
- });
4655
+ const response = await ctx.client.session.todo({ path: { id: sessionID } });
4508
4656
  todos = response.data ?? response;
4509
- log(`[${HOOK_NAME}] Todo API response`, { sessionID, todosCount: todos?.length ?? 0 });
4510
4657
  } catch (err) {
4511
4658
  log(`[${HOOK_NAME}] Todo API error`, { sessionID, error: String(err) });
4512
4659
  return;
@@ -4515,188 +4662,77 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
4515
4662
  log(`[${HOOK_NAME}] No todos found`, { sessionID });
4516
4663
  return;
4517
4664
  }
4518
- const incomplete = todos.filter((t) => t.status !== "completed" && t.status !== "cancelled");
4519
- if (incomplete.length === 0) {
4665
+ const incompleteCount = getIncompleteCount(todos);
4666
+ if (incompleteCount === 0) {
4520
4667
  log(`[${HOOK_NAME}] All todos completed`, { sessionID, total: todos.length });
4521
4668
  return;
4522
4669
  }
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 });
4670
+ const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((t) => t.status === "running") : false;
4671
+ if (hasRunningBgTasks) {
4672
+ log(`[${HOOK_NAME}] Skipped: background tasks still running`, { sessionID });
4673
+ return;
4674
+ }
4675
+ log(`[${HOOK_NAME}] Found incomplete todos`, {
4676
+ sessionID,
4677
+ incomplete: incompleteCount,
4678
+ total: todos.length
4679
+ });
4680
+ startCountdown(sessionID, incompleteCount);
4681
+ return;
4616
4682
  }
4617
4683
  if (event.type === "message.updated") {
4618
4684
  const info = props?.info;
4619
4685
  const sessionID = info?.sessionID;
4620
4686
  const role = info?.role;
4621
4687
  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
- }
4688
+ if (!sessionID)
4689
+ return;
4690
+ if (role === "user") {
4691
+ const state2 = sessions.get(sessionID);
4692
+ if (state2?.mode === "errorBypass") {
4693
+ state2.mode = "idle";
4694
+ log(`[${HOOK_NAME}] User message cleared errorBypass mode`, { sessionID });
4683
4695
  }
4696
+ invalidate(sessionID, "user message received");
4697
+ return;
4684
4698
  }
4699
+ if (role === "assistant" && !finish) {
4700
+ invalidate(sessionID, "assistant is working (streaming)");
4701
+ return;
4702
+ }
4703
+ if (role === "assistant" && finish) {
4704
+ log(`[${HOOK_NAME}] Assistant turn finished`, { sessionID, finish });
4705
+ return;
4706
+ }
4707
+ return;
4708
+ }
4709
+ if (event.type === "message.part.updated") {
4710
+ const info = props?.info;
4711
+ const sessionID = info?.sessionID;
4712
+ const role = info?.role;
4713
+ if (sessionID && role === "assistant") {
4714
+ invalidate(sessionID, "assistant streaming");
4715
+ }
4716
+ return;
4717
+ }
4718
+ if (event.type === "tool.execute.before" || event.type === "tool.execute.after") {
4719
+ const sessionID = props?.sessionID;
4720
+ if (sessionID) {
4721
+ invalidate(sessionID, `tool execution (${event.type})`);
4722
+ }
4723
+ return;
4685
4724
  }
4686
4725
  if (event.type === "session.deleted") {
4687
4726
  const sessionInfo = props?.info;
4688
4727
  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);
4728
+ const state2 = sessions.get(sessionInfo.id);
4729
+ if (state2) {
4730
+ clearTimer(state2);
4698
4731
  }
4732
+ sessions.delete(sessionInfo.id);
4733
+ log(`[${HOOK_NAME}] Session deleted, state cleaned up`, { sessionID: sessionInfo.id });
4699
4734
  }
4735
+ return;
4700
4736
  }
4701
4737
  };
4702
4738
  return {
@@ -5154,28 +5190,7 @@ import { join as join10 } from "path";
5154
5190
 
5155
5191
  // src/hooks/session-recovery/constants.ts
5156
5192
  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");
5193
+ var OPENCODE_STORAGE2 = getOpenCodeStorageDir();
5179
5194
  var MESSAGE_STORAGE2 = join9(OPENCODE_STORAGE2, "message");
5180
5195
  var PART_STORAGE2 = join9(OPENCODE_STORAGE2, "part");
5181
5196
  var THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"]);
@@ -5297,7 +5312,16 @@ function findEmptyMessages(sessionID) {
5297
5312
  }
5298
5313
  function findEmptyMessageByIndex(sessionID, targetIndex) {
5299
5314
  const messages = readMessages(sessionID);
5300
- const indicesToTry = [targetIndex, targetIndex - 1, targetIndex - 2];
5315
+ const indicesToTry = [
5316
+ targetIndex,
5317
+ targetIndex - 1,
5318
+ targetIndex + 1,
5319
+ targetIndex - 2,
5320
+ targetIndex + 2,
5321
+ targetIndex - 3,
5322
+ targetIndex - 4,
5323
+ targetIndex - 5
5324
+ ];
5301
5325
  for (const idx of indicesToTry) {
5302
5326
  if (idx < 0 || idx >= messages.length)
5303
5327
  continue;
@@ -5723,8 +5747,8 @@ var PLATFORM_MAP = {
5723
5747
  "win32-x64": { os: "windows", arch: "amd64", ext: "zip" }
5724
5748
  };
5725
5749
  function getCacheDir() {
5726
- const xdgCache2 = process.env.XDG_CACHE_HOME;
5727
- const base = xdgCache2 || join11(homedir5(), ".cache");
5750
+ const xdgCache = process.env.XDG_CACHE_HOME;
5751
+ const base = xdgCache || join11(homedir5(), ".cache");
5728
5752
  return join11(base, "oh-my-opencode", "bin");
5729
5753
  }
5730
5754
  function getBinaryName() {
@@ -5785,8 +5809,8 @@ async function downloadCommentChecker() {
5785
5809
  return binaryPath;
5786
5810
  }
5787
5811
  const version = getPackageVersion();
5788
- const { os: os5, arch, ext } = platformInfo;
5789
- const assetName = `comment-checker_v${version}_${os5}_${arch}.${ext}`;
5812
+ const { os: os4, arch, ext } = platformInfo;
5813
+ const assetName = `comment-checker_v${version}_${os4}_${arch}.${ext}`;
5790
5814
  const downloadUrl = `https://github.com/${REPO}/releases/download/v${version}/${assetName}`;
5791
5815
  debugLog(`Downloading from: ${downloadUrl}`);
5792
5816
  console.log(`[oh-my-opencode] Downloading comment-checker binary...`);
@@ -5898,15 +5922,15 @@ async function getCommentCheckerPath() {
5898
5922
  function startBackgroundInit() {
5899
5923
  if (!initPromise) {
5900
5924
  initPromise = getCommentCheckerPath();
5901
- initPromise.then((path5) => {
5902
- debugLog2("background init complete:", path5 || "no binary");
5925
+ initPromise.then((path4) => {
5926
+ debugLog2("background init complete:", path4 || "no binary");
5903
5927
  }).catch((err) => {
5904
5928
  debugLog2("background init error:", err);
5905
5929
  });
5906
5930
  }
5907
5931
  }
5908
5932
  var COMMENT_CHECKER_CLI_PATH = findCommentCheckerPathSync();
5909
- async function runCommentChecker(input, cliPath) {
5933
+ async function runCommentChecker(input, cliPath, customPrompt) {
5910
5934
  const binaryPath = cliPath ?? resolvedCliPath ?? COMMENT_CHECKER_CLI_PATH;
5911
5935
  if (!binaryPath) {
5912
5936
  debugLog2("comment-checker binary not found");
@@ -5919,7 +5943,11 @@ async function runCommentChecker(input, cliPath) {
5919
5943
  const jsonInput = JSON.stringify(input);
5920
5944
  debugLog2("running comment-checker with input:", jsonInput.substring(0, 200));
5921
5945
  try {
5922
- const proc = spawn4([binaryPath], {
5946
+ const args = [binaryPath];
5947
+ if (customPrompt) {
5948
+ args.push("--prompt", customPrompt);
5949
+ }
5950
+ const proc = spawn4(args, {
5923
5951
  stdin: "pipe",
5924
5952
  stdout: "pipe",
5925
5953
  stderr: "pipe"
@@ -5961,6 +5989,7 @@ function debugLog3(...args) {
5961
5989
  var pendingCalls = new Map;
5962
5990
  var PENDING_CALL_TTL = 60000;
5963
5991
  var cliPathPromise = null;
5992
+ var cleanupIntervalStarted = false;
5964
5993
  function cleanupOldPendingCalls() {
5965
5994
  const now = Date.now();
5966
5995
  for (const [callID, call] of pendingCalls) {
@@ -5969,13 +5998,16 @@ function cleanupOldPendingCalls() {
5969
5998
  }
5970
5999
  }
5971
6000
  }
5972
- setInterval(cleanupOldPendingCalls, 1e4);
5973
- function createCommentCheckerHooks() {
5974
- debugLog3("createCommentCheckerHooks called");
6001
+ function createCommentCheckerHooks(config) {
6002
+ debugLog3("createCommentCheckerHooks called", { config });
6003
+ if (!cleanupIntervalStarted) {
6004
+ cleanupIntervalStarted = true;
6005
+ setInterval(cleanupOldPendingCalls, 1e4);
6006
+ }
5975
6007
  startBackgroundInit();
5976
6008
  cliPathPromise = getCommentCheckerPath();
5977
- cliPathPromise.then((path5) => {
5978
- debugLog3("CLI path resolved:", path5 || "disabled (no binary)");
6009
+ cliPathPromise.then((path4) => {
6010
+ debugLog3("CLI path resolved:", path4 || "disabled (no binary)");
5979
6011
  }).catch((err) => {
5980
6012
  debugLog3("CLI path resolution error:", err);
5981
6013
  });
@@ -6031,14 +6063,14 @@ function createCommentCheckerHooks() {
6031
6063
  return;
6032
6064
  }
6033
6065
  debugLog3("using CLI:", cliPath);
6034
- await processWithCli(input, pendingCall, output, cliPath);
6066
+ await processWithCli(input, pendingCall, output, cliPath, config?.custom_prompt);
6035
6067
  } catch (err) {
6036
6068
  debugLog3("tool.execute.after failed:", err);
6037
6069
  }
6038
6070
  }
6039
6071
  };
6040
6072
  }
6041
- async function processWithCli(input, pendingCall, output, cliPath) {
6073
+ async function processWithCli(input, pendingCall, output, cliPath, customPrompt) {
6042
6074
  debugLog3("using CLI mode with path:", cliPath);
6043
6075
  const hookInput = {
6044
6076
  session_id: pendingCall.sessionID,
@@ -6054,7 +6086,7 @@ async function processWithCli(input, pendingCall, output, cliPath) {
6054
6086
  edits: pendingCall.edits
6055
6087
  }
6056
6088
  };
6057
- const result = await runCommentChecker(hookInput, cliPath);
6089
+ const result = await runCommentChecker(hookInput, cliPath, customPrompt);
6058
6090
  if (result.hasComments && result.message) {
6059
6091
  debugLog3("CLI detected comments, appending message");
6060
6092
  output.output += `
@@ -6113,7 +6145,7 @@ import { join as join15 } from "path";
6113
6145
 
6114
6146
  // src/hooks/directory-agents-injector/constants.ts
6115
6147
  import { join as join14 } from "path";
6116
- var OPENCODE_STORAGE3 = join14(xdgData ?? "", "opencode", "storage");
6148
+ var OPENCODE_STORAGE3 = getOpenCodeStorageDir();
6117
6149
  var AGENTS_INJECTOR_STORAGE = join14(OPENCODE_STORAGE3, "directory-agents");
6118
6150
  var AGENTS_FILENAME = "AGENTS.md";
6119
6151
 
@@ -6162,12 +6194,12 @@ function createDirectoryAgentsInjectorHook(ctx) {
6162
6194
  }
6163
6195
  return sessionCaches.get(sessionID);
6164
6196
  }
6165
- function resolveFilePath2(path5) {
6166
- if (!path5)
6197
+ function resolveFilePath2(path4) {
6198
+ if (!path4)
6167
6199
  return null;
6168
- if (path5.startsWith("/"))
6169
- return path5;
6170
- return resolve2(ctx.directory, path5);
6200
+ if (path4.startsWith("/"))
6201
+ return path4;
6202
+ return resolve2(ctx.directory, path4);
6171
6203
  }
6172
6204
  function findAgentsMdUp(startDir) {
6173
6205
  const found = [];
@@ -6285,7 +6317,7 @@ import { join as join18 } from "path";
6285
6317
 
6286
6318
  // src/hooks/directory-readme-injector/constants.ts
6287
6319
  import { join as join17 } from "path";
6288
- var OPENCODE_STORAGE4 = join17(xdgData ?? "", "opencode", "storage");
6320
+ var OPENCODE_STORAGE4 = getOpenCodeStorageDir();
6289
6321
  var README_INJECTOR_STORAGE = join17(OPENCODE_STORAGE4, "directory-readme");
6290
6322
  var README_FILENAME = "README.md";
6291
6323
 
@@ -6334,12 +6366,12 @@ function createDirectoryReadmeInjectorHook(ctx) {
6334
6366
  }
6335
6367
  return sessionCaches.get(sessionID);
6336
6368
  }
6337
- function resolveFilePath2(path5) {
6338
- if (!path5)
6369
+ function resolveFilePath2(path4) {
6370
+ if (!path4)
6339
6371
  return null;
6340
- if (path5.startsWith("/"))
6341
- return path5;
6342
- return resolve3(ctx.directory, path5);
6372
+ if (path4.startsWith("/"))
6373
+ return path4;
6374
+ return resolve3(ctx.directory, path4);
6343
6375
  }
6344
6376
  function findReadmeMdUp(startDir) {
6345
6377
  const found = [];
@@ -6478,7 +6510,8 @@ var TOKEN_LIMIT_KEYWORDS = [
6478
6510
  "token limit",
6479
6511
  "context length",
6480
6512
  "too many tokens",
6481
- "non-empty content"
6513
+ "non-empty content",
6514
+ "invalid_request_error"
6482
6515
  ];
6483
6516
  var MESSAGE_INDEX_PATTERN = /messages\.(\d+)/;
6484
6517
  function extractTokensFromMessage(message) {
@@ -6566,9 +6599,9 @@ function parseAnthropicTokenLimitError(err) {
6566
6599
  if (typeof responseBody === "string") {
6567
6600
  try {
6568
6601
  const jsonPatterns = [
6569
- /data:\s*(\{[\s\S]*?\})\s*$/m,
6570
- /(\{"type"\s*:\s*"error"[\s\S]*?\})/,
6571
- /(\{[\s\S]*?"error"[\s\S]*?\})/
6602
+ /data:\s*(\{[\s\S]*\})\s*$/m,
6603
+ /(\{"type"\s*:\s*"error"[\s\S]*\})/,
6604
+ /(\{[\s\S]*"error"[\s\S]*\})/
6572
6605
  ];
6573
6606
  for (const pattern of jsonPatterns) {
6574
6607
  const dataMatch = responseBody.match(pattern);
@@ -6655,7 +6688,6 @@ function estimateTokens2(text) {
6655
6688
  }
6656
6689
 
6657
6690
  // src/hooks/anthropic-auto-compact/pruning-deduplication.ts
6658
- var MESSAGE_STORAGE3 = join20(process.env.HOME || process.env.USERPROFILE || "", ".config", "opencode", "sessions");
6659
6691
  function createToolSignature(toolName, input) {
6660
6692
  const sortedInput = sortObject(input);
6661
6693
  return `${toolName}::${JSON.stringify(sortedInput)}`;
@@ -6675,13 +6707,13 @@ function sortObject(obj) {
6675
6707
  return sorted;
6676
6708
  }
6677
6709
  function getMessageDir3(sessionID) {
6678
- if (!existsSync15(MESSAGE_STORAGE3))
6710
+ if (!existsSync15(MESSAGE_STORAGE))
6679
6711
  return null;
6680
- const directPath = join20(MESSAGE_STORAGE3, sessionID);
6712
+ const directPath = join20(MESSAGE_STORAGE, sessionID);
6681
6713
  if (existsSync15(directPath))
6682
6714
  return directPath;
6683
- for (const dir of readdirSync4(MESSAGE_STORAGE3)) {
6684
- const sessionPath = join20(MESSAGE_STORAGE3, dir, sessionID);
6715
+ for (const dir of readdirSync4(MESSAGE_STORAGE)) {
6716
+ const sessionPath = join20(MESSAGE_STORAGE, dir, sessionID);
6685
6717
  if (existsSync15(sessionPath))
6686
6718
  return sessionPath;
6687
6719
  }
@@ -6793,15 +6825,14 @@ function findToolOutput(messages, callID) {
6793
6825
  // src/hooks/anthropic-auto-compact/pruning-supersede.ts
6794
6826
  import { existsSync as existsSync16, readdirSync as readdirSync5, readFileSync as readFileSync10 } from "fs";
6795
6827
  import { join as join21 } from "path";
6796
- var MESSAGE_STORAGE4 = join21(process.env.HOME || process.env.USERPROFILE || "", ".config", "opencode", "sessions");
6797
6828
  function getMessageDir4(sessionID) {
6798
- if (!existsSync16(MESSAGE_STORAGE4))
6829
+ if (!existsSync16(MESSAGE_STORAGE))
6799
6830
  return null;
6800
- const directPath = join21(MESSAGE_STORAGE4, sessionID);
6831
+ const directPath = join21(MESSAGE_STORAGE, sessionID);
6801
6832
  if (existsSync16(directPath))
6802
6833
  return directPath;
6803
- for (const dir of readdirSync5(MESSAGE_STORAGE4)) {
6804
- const sessionPath = join21(MESSAGE_STORAGE4, dir, sessionID);
6834
+ for (const dir of readdirSync5(MESSAGE_STORAGE)) {
6835
+ const sessionPath = join21(MESSAGE_STORAGE, dir, sessionID);
6805
6836
  if (existsSync16(sessionPath))
6806
6837
  return sessionPath;
6807
6838
  }
@@ -6956,15 +6987,14 @@ function findToolInput(messages, callID) {
6956
6987
  // src/hooks/anthropic-auto-compact/pruning-purge-errors.ts
6957
6988
  import { existsSync as existsSync17, readdirSync as readdirSync6, readFileSync as readFileSync11 } from "fs";
6958
6989
  import { join as join22 } from "path";
6959
- var MESSAGE_STORAGE5 = join22(process.env.HOME || process.env.USERPROFILE || "", ".config", "opencode", "sessions");
6960
6990
  function getMessageDir5(sessionID) {
6961
- if (!existsSync17(MESSAGE_STORAGE5))
6991
+ if (!existsSync17(MESSAGE_STORAGE))
6962
6992
  return null;
6963
- const directPath = join22(MESSAGE_STORAGE5, sessionID);
6993
+ const directPath = join22(MESSAGE_STORAGE, sessionID);
6964
6994
  if (existsSync17(directPath))
6965
6995
  return directPath;
6966
- for (const dir of readdirSync6(MESSAGE_STORAGE5)) {
6967
- const sessionPath = join22(MESSAGE_STORAGE5, dir, sessionID);
6996
+ for (const dir of readdirSync6(MESSAGE_STORAGE)) {
6997
+ const sessionPath = join22(MESSAGE_STORAGE, dir, sessionID);
6968
6998
  if (existsSync17(sessionPath))
6969
6999
  return sessionPath;
6970
7000
  }
@@ -7062,15 +7092,14 @@ function executePurgeErrors(sessionID, state2, config, protectedTools) {
7062
7092
  // src/hooks/anthropic-auto-compact/pruning-storage.ts
7063
7093
  import { existsSync as existsSync18, readdirSync as readdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
7064
7094
  import { join as join23 } from "path";
7065
- var MESSAGE_STORAGE6 = join23(process.env.HOME || process.env.USERPROFILE || "", ".config", "opencode", "sessions");
7066
7095
  function getMessageDir6(sessionID) {
7067
- if (!existsSync18(MESSAGE_STORAGE6))
7096
+ if (!existsSync18(MESSAGE_STORAGE))
7068
7097
  return null;
7069
- const directPath = join23(MESSAGE_STORAGE6, sessionID);
7098
+ const directPath = join23(MESSAGE_STORAGE, sessionID);
7070
7099
  if (existsSync18(directPath))
7071
7100
  return directPath;
7072
- for (const dir of readdirSync7(MESSAGE_STORAGE6)) {
7073
- const sessionPath = join23(MESSAGE_STORAGE6, dir, sessionID);
7101
+ for (const dir of readdirSync7(MESSAGE_STORAGE)) {
7102
+ const sessionPath = join23(MESSAGE_STORAGE, dir, sessionID);
7074
7103
  if (existsSync18(sessionPath))
7075
7104
  return sessionPath;
7076
7105
  }
@@ -7212,27 +7241,20 @@ async function executeDynamicContextPruning(sessionID, config, client) {
7212
7241
 
7213
7242
  // src/hooks/anthropic-auto-compact/storage.ts
7214
7243
  import { existsSync as existsSync19, readdirSync as readdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
7215
- import { homedir as homedir6 } from "os";
7216
7244
  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");
7245
+ var OPENCODE_STORAGE5 = getOpenCodeStorageDir();
7246
+ var MESSAGE_STORAGE3 = join24(OPENCODE_STORAGE5, "message");
7225
7247
  var PART_STORAGE3 = join24(OPENCODE_STORAGE5, "part");
7226
7248
  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
7249
  function getMessageDir7(sessionID) {
7228
- if (!existsSync19(MESSAGE_STORAGE7))
7250
+ if (!existsSync19(MESSAGE_STORAGE3))
7229
7251
  return "";
7230
- const directPath = join24(MESSAGE_STORAGE7, sessionID);
7252
+ const directPath = join24(MESSAGE_STORAGE3, sessionID);
7231
7253
  if (existsSync19(directPath)) {
7232
7254
  return directPath;
7233
7255
  }
7234
- for (const dir of readdirSync8(MESSAGE_STORAGE7)) {
7235
- const sessionPath = join24(MESSAGE_STORAGE7, dir, sessionID);
7256
+ for (const dir of readdirSync8(MESSAGE_STORAGE3)) {
7257
+ const sessionPath = join24(MESSAGE_STORAGE3, dir, sessionID);
7236
7258
  if (existsSync19(sessionPath)) {
7237
7259
  return sessionPath;
7238
7260
  }
@@ -7360,6 +7382,7 @@ function truncateUntilTargetTokens(sessionID, currentTokens, maxTokens, targetRa
7360
7382
  }
7361
7383
 
7362
7384
  // src/hooks/anthropic-auto-compact/executor.ts
7385
+ var PLACEHOLDER_TEXT = "[user interrupted]";
7363
7386
  function getOrCreateRetryState(autoCompactState, sessionID) {
7364
7387
  let state2 = autoCompactState.retryStateBySession.get(sessionID);
7365
7388
  if (!state2) {
@@ -7392,6 +7415,32 @@ function getOrCreateDcpState(autoCompactState, sessionID) {
7392
7415
  }
7393
7416
  return state2;
7394
7417
  }
7418
+ function sanitizeEmptyMessagesBeforeSummarize(sessionID) {
7419
+ const emptyMessageIds = findEmptyMessages(sessionID);
7420
+ if (emptyMessageIds.length === 0) {
7421
+ return 0;
7422
+ }
7423
+ let fixedCount = 0;
7424
+ for (const messageID of emptyMessageIds) {
7425
+ const replaced = replaceEmptyTextParts(messageID, PLACEHOLDER_TEXT);
7426
+ if (replaced) {
7427
+ fixedCount++;
7428
+ } else {
7429
+ const injected = injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT);
7430
+ if (injected) {
7431
+ fixedCount++;
7432
+ }
7433
+ }
7434
+ }
7435
+ if (fixedCount > 0) {
7436
+ log("[auto-compact] pre-summarize sanitization fixed empty messages", {
7437
+ sessionID,
7438
+ fixedCount,
7439
+ totalEmpty: emptyMessageIds.length
7440
+ });
7441
+ }
7442
+ return fixedCount;
7443
+ }
7395
7444
  async function getLastMessagePair(sessionID, client, directory) {
7396
7445
  try {
7397
7446
  const resp = await client.session.messages({
@@ -7547,6 +7596,77 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
7547
7596
  try {
7548
7597
  const errorData = autoCompactState.errorDataBySession.get(sessionID);
7549
7598
  const truncateState = getOrCreateTruncateState(autoCompactState, sessionID);
7599
+ const dcpState = getOrCreateDcpState(autoCompactState, sessionID);
7600
+ if (experimental?.dcp_for_compaction && !dcpState.attempted && errorData?.currentTokens && errorData?.maxTokens && errorData.currentTokens > errorData.maxTokens) {
7601
+ dcpState.attempted = true;
7602
+ log("[auto-compact] DCP triggered FIRST on token limit error", {
7603
+ sessionID,
7604
+ currentTokens: errorData.currentTokens,
7605
+ maxTokens: errorData.maxTokens
7606
+ });
7607
+ const dcpConfig = experimental.dynamic_context_pruning ?? {
7608
+ enabled: true,
7609
+ notification: "detailed",
7610
+ protected_tools: ["task", "todowrite", "todoread", "lsp_rename", "lsp_code_action_resolve"]
7611
+ };
7612
+ try {
7613
+ const pruningResult = await executeDynamicContextPruning(sessionID, dcpConfig, client);
7614
+ if (pruningResult.itemsPruned > 0) {
7615
+ dcpState.itemsPruned = pruningResult.itemsPruned;
7616
+ log("[auto-compact] DCP successful, proceeding to compaction", {
7617
+ itemsPruned: pruningResult.itemsPruned,
7618
+ tokensSaved: pruningResult.totalTokensSaved
7619
+ });
7620
+ await client.tui.showToast({
7621
+ body: {
7622
+ title: "Dynamic Context Pruning",
7623
+ message: `Pruned ${pruningResult.itemsPruned} items (~${Math.round(pruningResult.totalTokensSaved / 1000)}k tokens). Running compaction...`,
7624
+ variant: "success",
7625
+ duration: 3000
7626
+ }
7627
+ }).catch(() => {});
7628
+ const providerID = msg.providerID;
7629
+ const modelID = msg.modelID;
7630
+ if (providerID && modelID) {
7631
+ try {
7632
+ sanitizeEmptyMessagesBeforeSummarize(sessionID);
7633
+ await client.tui.showToast({
7634
+ body: {
7635
+ title: "Auto Compact",
7636
+ message: "Summarizing session after DCP...",
7637
+ variant: "warning",
7638
+ duration: 3000
7639
+ }
7640
+ }).catch(() => {});
7641
+ await client.session.summarize({
7642
+ path: { id: sessionID },
7643
+ body: { providerID, modelID },
7644
+ query: { directory }
7645
+ });
7646
+ clearSessionState(autoCompactState, sessionID);
7647
+ setTimeout(async () => {
7648
+ try {
7649
+ await client.session.prompt_async({
7650
+ path: { sessionID },
7651
+ body: { parts: [{ type: "text", text: "Continue" }] },
7652
+ query: { directory }
7653
+ });
7654
+ } catch {}
7655
+ }, 500);
7656
+ return;
7657
+ } catch (summarizeError) {
7658
+ log("[auto-compact] summarize after DCP failed, continuing recovery", {
7659
+ error: String(summarizeError)
7660
+ });
7661
+ }
7662
+ }
7663
+ } else {
7664
+ log("[auto-compact] DCP did not prune any items", { sessionID });
7665
+ }
7666
+ } catch (error) {
7667
+ log("[auto-compact] DCP failed", { error: String(error) });
7668
+ }
7669
+ }
7550
7670
  if (experimental?.aggressive_truncation && errorData?.currentTokens && errorData?.maxTokens && errorData.currentTokens > errorData.maxTokens && truncateState.truncateAttempt < TRUNCATE_CONFIG.maxTruncateAttempts) {
7551
7671
  log("[auto-compact] aggressive truncation triggered (experimental)", {
7552
7672
  currentTokens: errorData.currentTokens,
@@ -7673,6 +7793,7 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
7673
7793
  const modelID = msg.modelID;
7674
7794
  if (providerID && modelID) {
7675
7795
  try {
7796
+ sanitizeEmptyMessagesBeforeSummarize(sessionID);
7676
7797
  await client.tui.showToast({
7677
7798
  body: {
7678
7799
  title: "Auto Compact",
@@ -7715,45 +7836,6 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
7715
7836
  }).catch(() => {});
7716
7837
  }
7717
7838
  }
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
7839
  const fallbackState = getOrCreateFallbackState(autoCompactState, sessionID);
7758
7840
  if (fallbackState.revertAttempt < FALLBACK_CONFIG.maxRevertAttempts) {
7759
7841
  const pair = await getLastMessagePair(sessionID, client, directory);
@@ -8506,21 +8588,21 @@ async function loadClaudeHooksConfig(customSettingsPath) {
8506
8588
 
8507
8589
  // src/hooks/claude-code-hooks/config-loader.ts
8508
8590
  import { existsSync as existsSync22 } from "fs";
8509
- import { homedir as homedir7 } from "os";
8591
+ import { homedir as homedir6 } from "os";
8510
8592
  import { join as join27 } from "path";
8511
- var USER_CONFIG_PATH = join27(homedir7(), ".config", "opencode", "opencode-cc-plugin.json");
8593
+ var USER_CONFIG_PATH = join27(homedir6(), ".config", "opencode", "opencode-cc-plugin.json");
8512
8594
  function getProjectConfigPath() {
8513
8595
  return join27(process.cwd(), ".opencode", "opencode-cc-plugin.json");
8514
8596
  }
8515
- async function loadConfigFromPath(path5) {
8516
- if (!existsSync22(path5)) {
8597
+ async function loadConfigFromPath(path4) {
8598
+ if (!existsSync22(path4)) {
8517
8599
  return null;
8518
8600
  }
8519
8601
  try {
8520
- const content = await Bun.file(path5).text();
8602
+ const content = await Bun.file(path4).text();
8521
8603
  return JSON.parse(content);
8522
8604
  } catch (error) {
8523
- log("Failed to load config", { path: path5, error });
8605
+ log("Failed to load config", { path: path4, error });
8524
8606
  return null;
8525
8607
  }
8526
8608
  }
@@ -8708,10 +8790,10 @@ function ensureTranscriptDir() {
8708
8790
  }
8709
8791
  function appendTranscriptEntry(sessionId, entry) {
8710
8792
  ensureTranscriptDir();
8711
- const path5 = getTranscriptPath(sessionId);
8793
+ const path4 = getTranscriptPath(sessionId);
8712
8794
  const line = JSON.stringify(entry) + `
8713
8795
  `;
8714
- appendFileSync5(path5, line);
8796
+ appendFileSync5(path4, line);
8715
8797
  }
8716
8798
  function recordToolUse(sessionId, toolName, toolInput) {
8717
8799
  appendTranscriptEntry(sessionId, {
@@ -8818,11 +8900,11 @@ async function buildTranscriptFromSession(client, sessionId, directory, currentT
8818
8900
  }
8819
8901
  }
8820
8902
  }
8821
- function deleteTempTranscript(path5) {
8822
- if (!path5)
8903
+ function deleteTempTranscript(path4) {
8904
+ if (!path4)
8823
8905
  return;
8824
8906
  try {
8825
- unlinkSync5(path5);
8907
+ unlinkSync5(path4);
8826
8908
  } catch {}
8827
8909
  }
8828
8910
 
@@ -9449,7 +9531,7 @@ ${result.message}`;
9449
9531
  }
9450
9532
  // src/hooks/rules-injector/index.ts
9451
9533
  import { readFileSync as readFileSync15 } from "fs";
9452
- import { homedir as homedir8 } from "os";
9534
+ import { homedir as homedir7 } from "os";
9453
9535
  import { relative as relative3, resolve as resolve4 } from "path";
9454
9536
 
9455
9537
  // src/hooks/rules-injector/finder.ts
@@ -9463,7 +9545,7 @@ import { dirname as dirname4, join as join31, relative } from "path";
9463
9545
 
9464
9546
  // src/hooks/rules-injector/constants.ts
9465
9547
  import { join as join30 } from "path";
9466
- var OPENCODE_STORAGE6 = join30(xdgData ?? "", "opencode", "storage");
9548
+ var OPENCODE_STORAGE6 = getOpenCodeStorageDir();
9467
9549
  var RULES_INJECTOR_STORAGE = join30(OPENCODE_STORAGE6, "rules-injector");
9468
9550
  var PROJECT_MARKERS = [
9469
9551
  ".git",
@@ -9804,12 +9886,12 @@ function createRulesInjectorHook(ctx) {
9804
9886
  }
9805
9887
  return sessionCaches.get(sessionID);
9806
9888
  }
9807
- function resolveFilePath2(path5) {
9808
- if (!path5)
9889
+ function resolveFilePath2(path4) {
9890
+ if (!path4)
9809
9891
  return null;
9810
- if (path5.startsWith("/"))
9811
- return path5;
9812
- return resolve4(ctx.directory, path5);
9892
+ if (path4.startsWith("/"))
9893
+ return path4;
9894
+ return resolve4(ctx.directory, path4);
9813
9895
  }
9814
9896
  async function processFilePathForInjection(filePath, sessionID, output) {
9815
9897
  const resolved = resolveFilePath2(filePath);
@@ -9817,7 +9899,7 @@ function createRulesInjectorHook(ctx) {
9817
9899
  return;
9818
9900
  const projectRoot = findProjectRoot(resolved);
9819
9901
  const cache2 = getSessionCache(sessionID);
9820
- const home = homedir8();
9902
+ const home = homedir7();
9821
9903
  const ruleFileCandidates = findRuleFiles(projectRoot, home, resolved);
9822
9904
  const toInject = [];
9823
9905
  for (const candidate of ruleFileCandidates) {
@@ -9932,33 +10014,33 @@ function createBackgroundNotificationHook(manager) {
9932
10014
  }
9933
10015
  // src/hooks/auto-update-checker/checker.ts
9934
10016
  import * as fs5 from "fs";
9935
- import * as path6 from "path";
10017
+ import * as path5 from "path";
9936
10018
  import { fileURLToPath } from "url";
9937
10019
 
9938
10020
  // src/hooks/auto-update-checker/constants.ts
9939
- import * as path5 from "path";
9940
- import * as os5 from "os";
10021
+ import * as path4 from "path";
10022
+ import * as os4 from "os";
9941
10023
  var PACKAGE_NAME = "oh-my-opencode";
9942
10024
  var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
9943
10025
  var NPM_FETCH_TIMEOUT = 5000;
9944
10026
  function getCacheDir2() {
9945
10027
  if (process.platform === "win32") {
9946
- return path5.join(process.env.LOCALAPPDATA ?? os5.homedir(), "opencode");
10028
+ return path4.join(process.env.LOCALAPPDATA ?? os4.homedir(), "opencode");
9947
10029
  }
9948
- return path5.join(os5.homedir(), ".cache", "opencode");
10030
+ return path4.join(os4.homedir(), ".cache", "opencode");
9949
10031
  }
9950
10032
  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");
10033
+ var VERSION_FILE = path4.join(CACHE_DIR, "version");
10034
+ var INSTALLED_PACKAGE_JSON = path4.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
9953
10035
  function getUserConfigDir2() {
9954
10036
  if (process.platform === "win32") {
9955
- return process.env.APPDATA ?? path5.join(os5.homedir(), "AppData", "Roaming");
10037
+ return process.env.APPDATA ?? path4.join(os4.homedir(), "AppData", "Roaming");
9956
10038
  }
9957
- return process.env.XDG_CONFIG_HOME ?? path5.join(os5.homedir(), ".config");
10039
+ return process.env.XDG_CONFIG_HOME ?? path4.join(os4.homedir(), ".config");
9958
10040
  }
9959
10041
  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");
10042
+ var USER_OPENCODE_CONFIG = path4.join(USER_CONFIG_DIR, "opencode", "opencode.json");
10043
+ var USER_OPENCODE_CONFIG_JSONC = path4.join(USER_CONFIG_DIR, "opencode", "opencode.jsonc");
9962
10044
 
9963
10045
  // src/hooks/auto-update-checker/checker.ts
9964
10046
  function stripJsonComments(json) {
@@ -9966,8 +10048,8 @@ function stripJsonComments(json) {
9966
10048
  }
9967
10049
  function getConfigPaths(directory) {
9968
10050
  return [
9969
- path6.join(directory, ".opencode", "opencode.json"),
9970
- path6.join(directory, ".opencode", "opencode.jsonc"),
10051
+ path5.join(directory, ".opencode", "opencode.json"),
10052
+ path5.join(directory, ".opencode", "opencode.jsonc"),
9971
10053
  USER_OPENCODE_CONFIG,
9972
10054
  USER_OPENCODE_CONFIG_JSONC
9973
10055
  ];
@@ -9998,9 +10080,9 @@ function getLocalDevPath(directory) {
9998
10080
  function findPackageJsonUp(startPath) {
9999
10081
  try {
10000
10082
  const stat = fs5.statSync(startPath);
10001
- let dir = stat.isDirectory() ? startPath : path6.dirname(startPath);
10083
+ let dir = stat.isDirectory() ? startPath : path5.dirname(startPath);
10002
10084
  for (let i = 0;i < 10; i++) {
10003
- const pkgPath = path6.join(dir, "package.json");
10085
+ const pkgPath = path5.join(dir, "package.json");
10004
10086
  if (fs5.existsSync(pkgPath)) {
10005
10087
  try {
10006
10088
  const content = fs5.readFileSync(pkgPath, "utf-8");
@@ -10009,7 +10091,7 @@ function findPackageJsonUp(startPath) {
10009
10091
  return pkgPath;
10010
10092
  } catch {}
10011
10093
  }
10012
- const parent = path6.dirname(dir);
10094
+ const parent = path5.dirname(dir);
10013
10095
  if (parent === dir)
10014
10096
  break;
10015
10097
  dir = parent;
@@ -10066,7 +10148,7 @@ function getCachedVersion() {
10066
10148
  }
10067
10149
  } catch {}
10068
10150
  try {
10069
- const currentDir = path6.dirname(fileURLToPath(import.meta.url));
10151
+ const currentDir = path5.dirname(fileURLToPath(import.meta.url));
10070
10152
  const pkgPath = findPackageJsonUp(currentDir);
10071
10153
  if (pkgPath) {
10072
10154
  const content = fs5.readFileSync(pkgPath, "utf-8");
@@ -10142,12 +10224,12 @@ async function getLatestVersion() {
10142
10224
 
10143
10225
  // src/hooks/auto-update-checker/cache.ts
10144
10226
  import * as fs6 from "fs";
10145
- import * as path7 from "path";
10227
+ import * as path6 from "path";
10146
10228
  function stripTrailingCommas(json) {
10147
10229
  return json.replace(/,(\s*[}\]])/g, "$1");
10148
10230
  }
10149
10231
  function removeFromBunLock(packageName) {
10150
- const lockPath = path7.join(CACHE_DIR, "bun.lock");
10232
+ const lockPath = path6.join(CACHE_DIR, "bun.lock");
10151
10233
  if (!fs6.existsSync(lockPath))
10152
10234
  return false;
10153
10235
  try {
@@ -10173,8 +10255,8 @@ function removeFromBunLock(packageName) {
10173
10255
  }
10174
10256
  function invalidatePackage(packageName = PACKAGE_NAME) {
10175
10257
  try {
10176
- const pkgDir = path7.join(CACHE_DIR, "node_modules", packageName);
10177
- const pkgJsonPath = path7.join(CACHE_DIR, "package.json");
10258
+ const pkgDir = path6.join(CACHE_DIR, "node_modules", packageName);
10259
+ const pkgJsonPath = path6.join(CACHE_DIR, "package.json");
10178
10260
  let packageRemoved = false;
10179
10261
  let dependencyRemoved = false;
10180
10262
  let lockRemoved = false;
@@ -10372,7 +10454,7 @@ import { join as join37 } from "path";
10372
10454
 
10373
10455
  // src/hooks/agent-usage-reminder/constants.ts
10374
10456
  import { join as join36 } from "path";
10375
- var OPENCODE_STORAGE7 = join36(xdgData ?? "", "opencode", "storage");
10457
+ var OPENCODE_STORAGE7 = getOpenCodeStorageDir();
10376
10458
  var AGENT_USAGE_REMINDER_STORAGE = join36(OPENCODE_STORAGE7, "agent-usage-reminder");
10377
10459
  var TARGET_TOOLS = new Set([
10378
10460
  "grep",
@@ -10756,7 +10838,7 @@ import { join as join39 } from "path";
10756
10838
 
10757
10839
  // src/hooks/interactive-bash-session/constants.ts
10758
10840
  import { join as join38 } from "path";
10759
- var OPENCODE_STORAGE8 = join38(xdgData ?? "", "opencode", "storage");
10841
+ var OPENCODE_STORAGE8 = getOpenCodeStorageDir();
10760
10842
  var INTERACTIVE_BASH_SESSION_STORAGE = join38(OPENCODE_STORAGE8, "interactive-bash-session");
10761
10843
  var OMO_SESSION_PREFIX = "omo-";
10762
10844
  function buildSessionReminderMessage(sessions) {
@@ -10979,7 +11061,7 @@ function createInteractiveBashSessionHook(_ctx) {
10979
11061
  };
10980
11062
  }
10981
11063
  // src/hooks/empty-message-sanitizer/index.ts
10982
- var PLACEHOLDER_TEXT = "[user interrupted]";
11064
+ var PLACEHOLDER_TEXT2 = "[user interrupted]";
10983
11065
  function hasTextContent(part) {
10984
11066
  if (part.type === "text") {
10985
11067
  const text = part.text;
@@ -11008,7 +11090,7 @@ function createEmptyMessageSanitizerHook() {
11008
11090
  if (part.type === "text") {
11009
11091
  const textPart = part;
11010
11092
  if (!textPart.text || !textPart.text.trim()) {
11011
- textPart.text = PLACEHOLDER_TEXT;
11093
+ textPart.text = PLACEHOLDER_TEXT2;
11012
11094
  textPart.synthetic = true;
11013
11095
  injected = true;
11014
11096
  break;
@@ -11022,7 +11104,7 @@ function createEmptyMessageSanitizerHook() {
11022
11104
  messageID: message.info.id,
11023
11105
  sessionID: message.info.sessionID ?? "",
11024
11106
  type: "text",
11025
- text: PLACEHOLDER_TEXT,
11107
+ text: PLACEHOLDER_TEXT2,
11026
11108
  synthetic: true
11027
11109
  };
11028
11110
  if (insertIndex === -1) {
@@ -11036,7 +11118,7 @@ function createEmptyMessageSanitizerHook() {
11036
11118
  if (part.type === "text") {
11037
11119
  const textPart = part;
11038
11120
  if (textPart.text !== undefined && textPart.text.trim() === "") {
11039
- textPart.text = PLACEHOLDER_TEXT;
11121
+ textPart.text = PLACEHOLDER_TEXT2;
11040
11122
  textPart.synthetic = true;
11041
11123
  }
11042
11124
  }
@@ -11055,12 +11137,12 @@ function isExtendedThinkingModel(modelID) {
11055
11137
  }
11056
11138
  return lower.includes("claude-sonnet-4") || lower.includes("claude-opus-4") || lower.includes("claude-3");
11057
11139
  }
11058
- function hasToolParts(parts) {
11140
+ function hasContentParts(parts) {
11059
11141
  if (!parts || parts.length === 0)
11060
11142
  return false;
11061
11143
  return parts.some((part) => {
11062
11144
  const type = part.type;
11063
- return type === "tool" || type === "tool_use";
11145
+ return type === "tool" || type === "tool_use" || type === "text";
11064
11146
  });
11065
11147
  }
11066
11148
  function startsWithThinkingBlock(parts) {
@@ -11119,7 +11201,7 @@ function createThinkingBlockValidatorHook() {
11119
11201
  const msg = messages[i];
11120
11202
  if (msg.info.role !== "assistant")
11121
11203
  continue;
11122
- if (hasToolParts(msg.parts) && !startsWithThinkingBlock(msg.parts)) {
11204
+ if (hasContentParts(msg.parts) && !startsWithThinkingBlock(msg.parts)) {
11123
11205
  const previousThinking = findPreviousThinkingContent(messages, i);
11124
11206
  const thinkingContent = previousThinking || "[Continuing from previous reasoning]";
11125
11207
  prependThinkingBlock(msg, thinkingContent);
@@ -11392,10 +11474,66 @@ function startCallbackServer(timeoutMs = 5 * 60 * 1000) {
11392
11474
  };
11393
11475
  }
11394
11476
  // src/auth/antigravity/token.ts
11477
+ class AntigravityTokenRefreshError extends Error {
11478
+ code;
11479
+ description;
11480
+ status;
11481
+ statusText;
11482
+ responseBody;
11483
+ constructor(options) {
11484
+ super(options.message);
11485
+ this.name = "AntigravityTokenRefreshError";
11486
+ this.code = options.code;
11487
+ this.description = options.description;
11488
+ this.status = options.status;
11489
+ this.statusText = options.statusText;
11490
+ this.responseBody = options.responseBody;
11491
+ }
11492
+ get isInvalidGrant() {
11493
+ return this.code === "invalid_grant";
11494
+ }
11495
+ get isNetworkError() {
11496
+ return this.status === 0;
11497
+ }
11498
+ }
11499
+ function parseOAuthErrorPayload(text) {
11500
+ if (!text) {
11501
+ return {};
11502
+ }
11503
+ try {
11504
+ const payload = JSON.parse(text);
11505
+ let code;
11506
+ if (typeof payload.error === "string") {
11507
+ code = payload.error;
11508
+ } else if (payload.error && typeof payload.error === "object") {
11509
+ code = payload.error.status ?? payload.error.code;
11510
+ }
11511
+ return {
11512
+ code,
11513
+ description: payload.error_description
11514
+ };
11515
+ } catch {
11516
+ return { description: text };
11517
+ }
11518
+ }
11395
11519
  function isTokenExpired(tokens) {
11396
11520
  const expirationTime = tokens.timestamp + tokens.expires_in * 1000;
11397
11521
  return Date.now() >= expirationTime - ANTIGRAVITY_TOKEN_REFRESH_BUFFER_MS;
11398
11522
  }
11523
+ var MAX_REFRESH_RETRIES = 3;
11524
+ var INITIAL_RETRY_DELAY_MS = 1000;
11525
+ function calculateRetryDelay(attempt) {
11526
+ return Math.min(INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt), 1e4);
11527
+ }
11528
+ function isRetryableError(status) {
11529
+ if (status === 0)
11530
+ return true;
11531
+ if (status === 429)
11532
+ return true;
11533
+ if (status >= 500 && status < 600)
11534
+ return true;
11535
+ return false;
11536
+ }
11399
11537
  async function refreshAccessToken(refreshToken, clientId = ANTIGRAVITY_CLIENT_ID, clientSecret = ANTIGRAVITY_CLIENT_SECRET) {
11400
11538
  const params = new URLSearchParams({
11401
11539
  grant_type: "refresh_token",
@@ -11403,24 +11541,67 @@ async function refreshAccessToken(refreshToken, clientId = ANTIGRAVITY_CLIENT_ID
11403
11541
  client_id: clientId,
11404
11542
  client_secret: clientSecret
11405
11543
  });
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}`);
11544
+ let lastError;
11545
+ for (let attempt = 0;attempt <= MAX_REFRESH_RETRIES; attempt++) {
11546
+ try {
11547
+ const response = await fetch(GOOGLE_TOKEN_URL, {
11548
+ method: "POST",
11549
+ headers: {
11550
+ "Content-Type": "application/x-www-form-urlencoded"
11551
+ },
11552
+ body: params
11553
+ });
11554
+ if (response.ok) {
11555
+ const data = await response.json();
11556
+ return {
11557
+ access_token: data.access_token,
11558
+ refresh_token: data.refresh_token || refreshToken,
11559
+ expires_in: data.expires_in,
11560
+ token_type: data.token_type
11561
+ };
11562
+ }
11563
+ const responseBody = await response.text().catch(() => {
11564
+ return;
11565
+ });
11566
+ const parsed = parseOAuthErrorPayload(responseBody);
11567
+ lastError = new AntigravityTokenRefreshError({
11568
+ message: parsed.description || `Token refresh failed: ${response.status} ${response.statusText}`,
11569
+ code: parsed.code,
11570
+ description: parsed.description,
11571
+ status: response.status,
11572
+ statusText: response.statusText,
11573
+ responseBody
11574
+ });
11575
+ if (parsed.code === "invalid_grant") {
11576
+ throw lastError;
11577
+ }
11578
+ if (!isRetryableError(response.status)) {
11579
+ throw lastError;
11580
+ }
11581
+ if (attempt < MAX_REFRESH_RETRIES) {
11582
+ const delay = calculateRetryDelay(attempt);
11583
+ await new Promise((resolve5) => setTimeout(resolve5, delay));
11584
+ }
11585
+ } catch (error) {
11586
+ if (error instanceof AntigravityTokenRefreshError) {
11587
+ throw error;
11588
+ }
11589
+ lastError = new AntigravityTokenRefreshError({
11590
+ message: error instanceof Error ? error.message : "Network error during token refresh",
11591
+ status: 0,
11592
+ statusText: "Network Error"
11593
+ });
11594
+ if (attempt < MAX_REFRESH_RETRIES) {
11595
+ const delay = calculateRetryDelay(attempt);
11596
+ await new Promise((resolve5) => setTimeout(resolve5, delay));
11597
+ }
11598
+ }
11416
11599
  }
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
- };
11600
+ throw lastError || new AntigravityTokenRefreshError({
11601
+ message: "Token refresh failed after all retries",
11602
+ status: 0,
11603
+ statusText: "Max Retries Exceeded"
11604
+ });
11424
11605
  }
11425
11606
  function parseStoredToken(stored) {
11426
11607
  const parts = stored.split("|");
@@ -11645,6 +11826,10 @@ function clearProjectContextCache(accessToken) {
11645
11826
  projectContextCache.clear();
11646
11827
  }
11647
11828
  }
11829
+ function invalidateProjectContextByRefreshToken(_refreshToken) {
11830
+ projectContextCache.clear();
11831
+ debugLog4(`[invalidateProjectContextByRefreshToken] Cleared all project context cache due to refresh token invalidation`);
11832
+ }
11648
11833
  // src/auth/antigravity/request.ts
11649
11834
  function buildRequestHeaders(accessToken) {
11650
11835
  return {
@@ -12359,7 +12544,7 @@ function debugLog7(message) {
12359
12544
  console.log(`[antigravity-fetch] ${message}`);
12360
12545
  }
12361
12546
  }
12362
- function isRetryableError(status) {
12547
+ function isRetryableError2(status) {
12363
12548
  if (status === 0)
12364
12549
  return true;
12365
12550
  if (status === 429)
@@ -12377,11 +12562,11 @@ var GCP_PERMISSION_ERROR_PATTERNS = [
12377
12562
  function isGcpPermissionError(text) {
12378
12563
  return GCP_PERMISSION_ERROR_PATTERNS.some((pattern) => text.includes(pattern));
12379
12564
  }
12380
- function calculateRetryDelay(attempt) {
12565
+ function calculateRetryDelay2(attempt) {
12381
12566
  return Math.min(200 * Math.pow(2, attempt), 2000);
12382
12567
  }
12383
12568
  async function isRetryableResponse(response) {
12384
- if (isRetryableError(response.status))
12569
+ if (isRetryableError2(response.status))
12385
12570
  return true;
12386
12571
  if (response.status === 403) {
12387
12572
  try {
@@ -12460,7 +12645,7 @@ async function attemptFetch(options) {
12460
12645
  const text = await response.clone().text();
12461
12646
  if (isGcpPermissionError(text)) {
12462
12647
  if (attempt < maxPermissionRetries) {
12463
- const delay = calculateRetryDelay(attempt);
12648
+ const delay = calculateRetryDelay2(attempt);
12464
12649
  debugLog7(`[RETRY] GCP permission error, retry ${attempt + 1}/${maxPermissionRetries} after ${delay}ms`);
12465
12650
  await new Promise((resolve5) => setTimeout(resolve5, delay));
12466
12651
  continue;
@@ -12581,6 +12766,14 @@ function createAntigravityFetch(getAuth, client, providerId, clientId, clientSec
12581
12766
  });
12582
12767
  debugLog7("Token refreshed successfully");
12583
12768
  } catch (error) {
12769
+ if (error instanceof AntigravityTokenRefreshError) {
12770
+ if (error.isInvalidGrant) {
12771
+ debugLog7(`[REFRESH] Token revoked (invalid_grant), clearing caches`);
12772
+ invalidateProjectContextByRefreshToken(refreshParts.refreshToken);
12773
+ clearProjectContextCache();
12774
+ }
12775
+ throw new Error(`Antigravity: Token refresh failed: ${error.description || error.message}${error.code ? ` (${error.code})` : ""}`);
12776
+ }
12584
12777
  throw new Error(`Antigravity: Token refresh failed: ${error instanceof Error ? error.message : "Unknown error"}`);
12585
12778
  }
12586
12779
  }
@@ -12662,10 +12855,29 @@ function createAntigravityFetch(getAuth, client, providerId, clientId, clientSec
12662
12855
  debugLog7("[401] Token refreshed, retrying request...");
12663
12856
  return executeWithEndpoints();
12664
12857
  } catch (refreshError) {
12858
+ if (refreshError instanceof AntigravityTokenRefreshError) {
12859
+ if (refreshError.isInvalidGrant) {
12860
+ debugLog7(`[401] Token revoked (invalid_grant), clearing caches`);
12861
+ invalidateProjectContextByRefreshToken(refreshParts.refreshToken);
12862
+ clearProjectContextCache();
12863
+ }
12864
+ debugLog7(`[401] Token refresh failed: ${refreshError.description || refreshError.message}`);
12865
+ return new Response(JSON.stringify({
12866
+ error: {
12867
+ message: refreshError.description || refreshError.message,
12868
+ type: refreshError.isInvalidGrant ? "token_revoked" : "unauthorized",
12869
+ code: refreshError.code || "token_refresh_failed"
12870
+ }
12871
+ }), {
12872
+ status: 401,
12873
+ statusText: "Unauthorized",
12874
+ headers: { "Content-Type": "application/json" }
12875
+ });
12876
+ }
12665
12877
  debugLog7(`[401] Token refresh failed: ${refreshError instanceof Error ? refreshError.message : "Unknown error"}`);
12666
12878
  return new Response(JSON.stringify({
12667
12879
  error: {
12668
- message: `Token refresh failed: ${refreshError instanceof Error ? refreshError.message : "Unknown error"}`,
12880
+ message: refreshError instanceof Error ? refreshError.message : "Unknown error",
12669
12881
  type: "unauthorized",
12670
12882
  code: "token_refresh_failed"
12671
12883
  }
@@ -12892,8 +13104,8 @@ function loadProjectCommands() {
12892
13104
  return commandsToRecord(commands);
12893
13105
  }
12894
13106
  function loadOpencodeGlobalCommands() {
12895
- const { homedir: homedir10 } = __require("os");
12896
- const opencodeCommandsDir = join40(homedir10(), ".config", "opencode", "command");
13107
+ const { homedir: homedir9 } = __require("os");
13108
+ const opencodeCommandsDir = join40(homedir9(), ".config", "opencode", "command");
12897
13109
  const commands = loadCommandsFromDir(opencodeCommandsDir, "opencode");
12898
13110
  return commandsToRecord(commands);
12899
13111
  }
@@ -12902,6 +13114,334 @@ function loadOpencodeProjectCommands() {
12902
13114
  const commands = loadCommandsFromDir(opencodeProjectDir, "opencode-project");
12903
13115
  return commandsToRecord(commands);
12904
13116
  }
13117
+ // src/features/builtin-commands/templates/init-deep.ts
13118
+ var INIT_DEEP_TEMPLATE = `# Initialize Deep Knowledge Base
13119
+
13120
+ Generate comprehensive AGENTS.md files across project hierarchy. Combines root-level project knowledge (gen-knowledge) with complexity-based subdirectory documentation (gen-knowledge-deep).
13121
+
13122
+ ## Usage
13123
+
13124
+ \`\`\`
13125
+ /init-deep # Analyze and generate hierarchical AGENTS.md
13126
+ /init-deep --create-new # Force create from scratch (ignore existing)
13127
+ /init-deep --max-depth=2 # Limit to N directory levels (default: 3)
13128
+ \`\`\`
13129
+
13130
+ ---
13131
+
13132
+ ## Core Principles
13133
+
13134
+ - **Telegraphic Style**: Sacrifice grammar for concision ("Project uses React" \u2192 "React 18")
13135
+ - **Predict-then-Compare**: Predict standard \u2192 find actual \u2192 document ONLY deviations
13136
+ - **Hierarchy Aware**: Parent covers general, children cover specific
13137
+ - **No Redundancy**: Child AGENTS.md NEVER repeats parent content
13138
+
13139
+ ---
13140
+
13141
+ ## Process
13142
+
13143
+ <critical>
13144
+ **MANDATORY: TodoWrite for ALL phases. Mark in_progress \u2192 completed in real-time.**
13145
+ </critical>
13146
+
13147
+ ### Phase 0: Initialize
13148
+
13149
+ \`\`\`
13150
+ TodoWrite([
13151
+ { id: "p1-analysis", content: "Parallel project structure & complexity analysis", status: "pending", priority: "high" },
13152
+ { id: "p2-scoring", content: "Score directories, determine AGENTS.md locations", status: "pending", priority: "high" },
13153
+ { id: "p3-root", content: "Generate root AGENTS.md with Predict-then-Compare", status: "pending", priority: "high" },
13154
+ { id: "p4-subdirs", content: "Generate subdirectory AGENTS.md files in parallel", status: "pending", priority: "high" },
13155
+ { id: "p5-review", content: "Review, deduplicate, validate all files", status: "pending", priority: "medium" }
13156
+ ])
13157
+ \`\`\`
13158
+
13159
+ ---
13160
+
13161
+ ## Phase 1: Parallel Project Analysis
13162
+
13163
+ **Mark "p1-analysis" as in_progress.**
13164
+
13165
+ Launch **ALL tasks simultaneously**:
13166
+
13167
+ <parallel-tasks>
13168
+
13169
+ ### Structural Analysis (bash - run in parallel)
13170
+ \`\`\`bash
13171
+ # Task A: Directory depth analysis
13172
+ 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
13173
+
13174
+ # Task B: File count per directory
13175
+ find . -type f -not -path '*/\\.*' -not -path '*/node_modules/*' -not -path '*/venv/*' -not -path '*/__pycache__/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -30
13176
+
13177
+ # Task C: Code concentration
13178
+ 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
13179
+
13180
+ # Task D: Existing knowledge files
13181
+ find . -type f \\( -name "AGENTS.md" -o -name "CLAUDE.md" \\) -not -path '*/node_modules/*' 2>/dev/null
13182
+ \`\`\`
13183
+
13184
+ ### Context Gathering (Explore agents - background_task in parallel)
13185
+
13186
+ \`\`\`
13187
+ background_task(agent="explore", prompt="Project structure: PREDICT standard {lang} patterns \u2192 FIND package.json/pyproject.toml/go.mod \u2192 REPORT deviations only")
13188
+
13189
+ background_task(agent="explore", prompt="Entry points: PREDICT typical (main.py, index.ts) \u2192 FIND actual \u2192 REPORT non-standard organization")
13190
+
13191
+ background_task(agent="explore", prompt="Conventions: FIND .cursor/rules, .cursorrules, eslintrc, pyproject.toml \u2192 REPORT project-specific rules DIFFERENT from defaults")
13192
+
13193
+ background_task(agent="explore", prompt="Anti-patterns: FIND comments with 'DO NOT', 'NEVER', 'ALWAYS', 'LEGACY', 'DEPRECATED' \u2192 REPORT forbidden patterns")
13194
+
13195
+ background_task(agent="explore", prompt="Build/CI: FIND .github/workflows, Makefile, justfile \u2192 REPORT non-standard build/deploy patterns")
13196
+
13197
+ background_task(agent="explore", prompt="Test patterns: FIND pytest.ini, jest.config, test structure \u2192 REPORT unique testing conventions")
13198
+ \`\`\`
13199
+
13200
+ </parallel-tasks>
13201
+
13202
+ **Collect all results. Mark "p1-analysis" as completed.**
13203
+
13204
+ ---
13205
+
13206
+ ## Phase 2: Complexity Scoring & Location Decision
13207
+
13208
+ **Mark "p2-scoring" as in_progress.**
13209
+
13210
+ ### Scoring Matrix
13211
+
13212
+ | Factor | Weight | Threshold |
13213
+ |--------|--------|-----------|
13214
+ | File count | 3x | >20 files = high |
13215
+ | Subdirectory count | 2x | >5 subdirs = high |
13216
+ | Code file ratio | 2x | >70% code = high |
13217
+ | Unique patterns | 1x | Has own config |
13218
+ | Module boundary | 2x | Has __init__.py/index.ts |
13219
+
13220
+ ### Decision Rules
13221
+
13222
+ | Score | Action |
13223
+ |-------|--------|
13224
+ | **Root (.)** | ALWAYS create AGENTS.md |
13225
+ | **High (>15)** | Create dedicated AGENTS.md |
13226
+ | **Medium (8-15)** | Create if distinct domain |
13227
+ | **Low (<8)** | Skip, parent sufficient |
13228
+
13229
+ ### Output Format
13230
+
13231
+ \`\`\`
13232
+ AGENTS_LOCATIONS = [
13233
+ { path: ".", type: "root" },
13234
+ { path: "src/api", score: 18, reason: "high complexity, 45 files" },
13235
+ { path: "src/hooks", score: 12, reason: "distinct domain, unique patterns" },
13236
+ ]
13237
+ \`\`\`
13238
+
13239
+ **Mark "p2-scoring" as completed.**
13240
+
13241
+ ---
13242
+
13243
+ ## Phase 3: Generate Root AGENTS.md
13244
+
13245
+ **Mark "p3-root" as in_progress.**
13246
+
13247
+ Root AGENTS.md gets **full treatment** with Predict-then-Compare synthesis.
13248
+
13249
+ ### Required Sections
13250
+
13251
+ \`\`\`markdown
13252
+ # PROJECT KNOWLEDGE BASE
13253
+
13254
+ **Generated:** {TIMESTAMP}
13255
+ **Commit:** {SHORT_SHA}
13256
+ **Branch:** {BRANCH}
13257
+
13258
+ ## OVERVIEW
13259
+
13260
+ {1-2 sentences: what project does, core tech stack}
13261
+
13262
+ ## STRUCTURE
13263
+
13264
+ \\\`\\\`\\\`
13265
+ {project-root}/
13266
+ \u251C\u2500\u2500 {dir}/ # {non-obvious purpose only}
13267
+ \u2514\u2500\u2500 {entry} # entry point
13268
+ \\\`\\\`\\\`
13269
+
13270
+ ## WHERE TO LOOK
13271
+
13272
+ | Task | Location | Notes |
13273
+ |------|----------|-------|
13274
+ | Add feature X | \\\`src/x/\\\` | {pattern hint} |
13275
+
13276
+ ## CONVENTIONS
13277
+
13278
+ {ONLY deviations from standard - skip generic advice}
13279
+
13280
+ - **{rule}**: {specific detail}
13281
+
13282
+ ## ANTI-PATTERNS (THIS PROJECT)
13283
+
13284
+ {Things explicitly forbidden HERE}
13285
+
13286
+ - **{pattern}**: {why} \u2192 {alternative}
13287
+
13288
+ ## UNIQUE STYLES
13289
+
13290
+ {Project-specific coding styles}
13291
+
13292
+ - **{style}**: {how different}
13293
+
13294
+ ## COMMANDS
13295
+
13296
+ \\\`\\\`\\\`bash
13297
+ {dev-command}
13298
+ {test-command}
13299
+ {build-command}
13300
+ \\\`\\\`\\\`
13301
+
13302
+ ## NOTES
13303
+
13304
+ {Gotchas, non-obvious info}
13305
+ \`\`\`
13306
+
13307
+ ### Quality Gates
13308
+
13309
+ - [ ] Size: 50-150 lines
13310
+ - [ ] No generic advice ("write clean code")
13311
+ - [ ] No obvious info ("tests/ has tests")
13312
+ - [ ] Every item is project-specific
13313
+
13314
+ **Mark "p3-root" as completed.**
13315
+
13316
+ ---
13317
+
13318
+ ## Phase 4: Generate Subdirectory AGENTS.md
13319
+
13320
+ **Mark "p4-subdirs" as in_progress.**
13321
+
13322
+ For each location in AGENTS_LOCATIONS (except root), launch **parallel document-writer agents**:
13323
+
13324
+ \`\`\`typescript
13325
+ for (const loc of AGENTS_LOCATIONS.filter(l => l.path !== ".")) {
13326
+ background_task({
13327
+ agent: "document-writer",
13328
+ prompt: \\\`
13329
+ Generate AGENTS.md for: \${loc.path}
13330
+
13331
+ CONTEXT:
13332
+ - Complexity reason: \${loc.reason}
13333
+ - Parent AGENTS.md: ./AGENTS.md (already covers project overview)
13334
+
13335
+ CRITICAL RULES:
13336
+ 1. Focus ONLY on this directory's specific context
13337
+ 2. NEVER repeat parent AGENTS.md content
13338
+ 3. Shorter is better - 30-80 lines max
13339
+ 4. Telegraphic style - sacrifice grammar
13340
+
13341
+ REQUIRED SECTIONS:
13342
+ - OVERVIEW (1 line: what this directory does)
13343
+ - STRUCTURE (only if >5 subdirs)
13344
+ - WHERE TO LOOK (directory-specific tasks)
13345
+ - CONVENTIONS (only if DIFFERENT from root)
13346
+ - ANTI-PATTERNS (directory-specific only)
13347
+
13348
+ OUTPUT: Write to \${loc.path}/AGENTS.md
13349
+ \\\`
13350
+ })
13351
+ }
13352
+ \`\`\`
13353
+
13354
+ **Wait for all agents. Mark "p4-subdirs" as completed.**
13355
+
13356
+ ---
13357
+
13358
+ ## Phase 5: Review & Deduplicate
13359
+
13360
+ **Mark "p5-review" as in_progress.**
13361
+
13362
+ ### Validation Checklist
13363
+
13364
+ For EACH generated AGENTS.md:
13365
+
13366
+ | Check | Action if Fail |
13367
+ |-------|----------------|
13368
+ | Contains generic advice | REMOVE the line |
13369
+ | Repeats parent content | REMOVE the line |
13370
+ | Missing required section | ADD it |
13371
+ | Over 150 lines (root) / 80 lines (subdir) | TRIM |
13372
+ | Verbose explanations | REWRITE telegraphic |
13373
+
13374
+ ### Cross-Reference Validation
13375
+
13376
+ \`\`\`
13377
+ For each child AGENTS.md:
13378
+ For each line in child:
13379
+ If similar line exists in parent:
13380
+ REMOVE from child (parent already covers)
13381
+ \`\`\`
13382
+
13383
+ **Mark "p5-review" as completed.**
13384
+
13385
+ ---
13386
+
13387
+ ## Final Report
13388
+
13389
+ \`\`\`
13390
+ === init-deep Complete ===
13391
+
13392
+ Files Generated:
13393
+ \u2713 ./AGENTS.md (root, {N} lines)
13394
+ \u2713 ./src/hooks/AGENTS.md ({N} lines)
13395
+ \u2713 ./src/tools/AGENTS.md ({N} lines)
13396
+
13397
+ Directories Analyzed: {N}
13398
+ AGENTS.md Created: {N}
13399
+ Total Lines: {N}
13400
+
13401
+ Hierarchy:
13402
+ ./AGENTS.md
13403
+ \u251C\u2500\u2500 src/hooks/AGENTS.md
13404
+ \u2514\u2500\u2500 src/tools/AGENTS.md
13405
+ \`\`\`
13406
+
13407
+ ---
13408
+
13409
+ ## Anti-Patterns for THIS Command
13410
+
13411
+ - **Over-documenting**: Not every directory needs AGENTS.md
13412
+ - **Redundancy**: Child must NOT repeat parent
13413
+ - **Generic content**: Remove anything that applies to ALL projects
13414
+ - **Sequential execution**: MUST use parallel agents
13415
+ - **Deep nesting**: Rarely need AGENTS.md at depth 4+
13416
+ - **Verbose style**: "This directory contains..." \u2192 just list it`;
13417
+
13418
+ // src/features/builtin-commands/commands.ts
13419
+ var BUILTIN_COMMAND_DEFINITIONS = {
13420
+ "init-deep": {
13421
+ description: "(builtin) Initialize hierarchical AGENTS.md knowledge base",
13422
+ template: `<command-instruction>
13423
+ ${INIT_DEEP_TEMPLATE}
13424
+ </command-instruction>
13425
+
13426
+ <user-request>
13427
+ $ARGUMENTS
13428
+ </user-request>`,
13429
+ argumentHint: "[--create-new] [--max-depth=N]"
13430
+ }
13431
+ };
13432
+ function loadBuiltinCommands(disabledCommands) {
13433
+ const disabled = new Set(disabledCommands ?? []);
13434
+ const commands = {};
13435
+ for (const [name, definition] of Object.entries(BUILTIN_COMMAND_DEFINITIONS)) {
13436
+ if (!disabled.has(name)) {
13437
+ commands[name] = {
13438
+ name,
13439
+ ...definition
13440
+ };
13441
+ }
13442
+ }
13443
+ return commands;
13444
+ }
12905
13445
  // src/features/claude-code-agent-loader/loader.ts
12906
13446
  import { existsSync as existsSync31, readdirSync as readdirSync12, readFileSync as readFileSync21 } from "fs";
12907
13447
  import { join as join41, basename as basename2 } from "path";
@@ -13065,13 +13605,13 @@ async function loadMcpConfigs() {
13065
13605
  const servers = {};
13066
13606
  const loadedServers = [];
13067
13607
  const paths = getMcpConfigPaths();
13068
- for (const { path: path8, scope } of paths) {
13069
- const config = await loadMcpConfigFile(path8);
13608
+ for (const { path: path7, scope } of paths) {
13609
+ const config = await loadMcpConfigFile(path7);
13070
13610
  if (!config?.mcpServers)
13071
13611
  continue;
13072
13612
  for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
13073
13613
  if (serverConfig.disabled) {
13074
- log(`Skipping disabled MCP server "${name}"`, { path: path8 });
13614
+ log(`Skipping disabled MCP server "${name}"`, { path: path7 });
13075
13615
  continue;
13076
13616
  }
13077
13617
  try {
@@ -13082,7 +13622,7 @@ async function loadMcpConfigs() {
13082
13622
  loadedServers.splice(existingIndex, 1);
13083
13623
  }
13084
13624
  loadedServers.push({ name, scope, config: transformed });
13085
- log(`Loaded MCP server "${name}" from ${scope}`, { path: path8 });
13625
+ log(`Loaded MCP server "${name}" from ${scope}`, { path: path7 });
13086
13626
  } catch (error) {
13087
13627
  log(`Failed to transform MCP server "${name}"`, error);
13088
13628
  }
@@ -13092,20 +13632,20 @@ async function loadMcpConfigs() {
13092
13632
  }
13093
13633
  // src/features/claude-code-plugin-loader/loader.ts
13094
13634
  import { existsSync as existsSync33, readdirSync as readdirSync13, readFileSync as readFileSync22 } from "fs";
13095
- import { homedir as homedir10 } from "os";
13635
+ import { homedir as homedir9 } from "os";
13096
13636
  import { join as join43, basename as basename3 } from "path";
13097
13637
  var CLAUDE_PLUGIN_ROOT_VAR = "${CLAUDE_PLUGIN_ROOT}";
13098
13638
  function getPluginsBaseDir() {
13099
13639
  if (process.env.CLAUDE_PLUGINS_HOME) {
13100
13640
  return process.env.CLAUDE_PLUGINS_HOME;
13101
13641
  }
13102
- return join43(homedir10(), ".claude", "plugins");
13642
+ return join43(homedir9(), ".claude", "plugins");
13103
13643
  }
13104
13644
  function getInstalledPluginsPath() {
13105
13645
  return join43(getPluginsBaseDir(), "installed_plugins.json");
13106
13646
  }
13107
- function resolvePluginPath(path8, pluginRoot) {
13108
- return path8.replace(CLAUDE_PLUGIN_ROOT_VAR, pluginRoot);
13647
+ function resolvePluginPath(path7, pluginRoot) {
13648
+ return path7.replace(CLAUDE_PLUGIN_ROOT_VAR, pluginRoot);
13109
13649
  }
13110
13650
  function resolvePluginPaths(obj, pluginRoot) {
13111
13651
  if (obj === null || obj === undefined)
@@ -13142,7 +13682,7 @@ function getClaudeSettingsPath() {
13142
13682
  if (process.env.CLAUDE_SETTINGS_PATH) {
13143
13683
  return process.env.CLAUDE_SETTINGS_PATH;
13144
13684
  }
13145
- return join43(homedir10(), ".claude", "settings.json");
13685
+ return join43(homedir9(), ".claude", "settings.json");
13146
13686
  }
13147
13687
  function loadClaudeSettings() {
13148
13688
  const settingsPath = getClaudeSettingsPath();
@@ -13251,7 +13791,7 @@ function discoverInstalledPlugins(options) {
13251
13791
  return { plugins, errors };
13252
13792
  }
13253
13793
  function loadPluginCommands(plugins) {
13254
- const commands = {};
13794
+ const commands2 = {};
13255
13795
  for (const plugin2 of plugins) {
13256
13796
  if (!plugin2.commandsDir || !existsSync33(plugin2.commandsDir))
13257
13797
  continue;
@@ -13273,7 +13813,7 @@ ${body.trim()}
13273
13813
  $ARGUMENTS
13274
13814
  </user-request>`;
13275
13815
  const formattedDescription = `(plugin: ${plugin2.name}) ${data.description || ""}`;
13276
- commands[namespacedName] = {
13816
+ commands2[namespacedName] = {
13277
13817
  name: namespacedName,
13278
13818
  description: formattedDescription,
13279
13819
  template: wrappedTemplate,
@@ -13288,7 +13828,7 @@ $ARGUMENTS
13288
13828
  }
13289
13829
  }
13290
13830
  }
13291
- return commands;
13831
+ return commands2;
13292
13832
  }
13293
13833
  function loadPluginSkillsAsCommands(plugins) {
13294
13834
  const skills = {};
@@ -13436,14 +13976,14 @@ function loadPluginHooksConfigs(plugins) {
13436
13976
  }
13437
13977
  async function loadAllPluginComponents(options) {
13438
13978
  const { plugins, errors } = discoverInstalledPlugins(options);
13439
- const commands = loadPluginCommands(plugins);
13979
+ const commands2 = loadPluginCommands(plugins);
13440
13980
  const skills = loadPluginSkillsAsCommands(plugins);
13441
13981
  const agents = loadPluginAgents(plugins);
13442
13982
  const mcpServers = await loadPluginMcpServers(plugins);
13443
13983
  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`);
13984
+ 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
13985
  return {
13446
- commands,
13986
+ commands: commands2,
13447
13987
  skills,
13448
13988
  agents,
13449
13989
  mcpServers,
@@ -13490,6 +14030,36 @@ var SEVERITY_MAP = {
13490
14030
  var DEFAULT_MAX_REFERENCES = 200;
13491
14031
  var DEFAULT_MAX_SYMBOLS = 200;
13492
14032
  var DEFAULT_MAX_DIAGNOSTICS = 200;
14033
+ var LSP_INSTALL_HINTS = {
14034
+ typescript: "npm install -g typescript-language-server typescript",
14035
+ deno: "Install Deno from https://deno.land",
14036
+ vue: "npm install -g @vue/language-server",
14037
+ eslint: "npm install -g vscode-langservers-extracted",
14038
+ oxlint: "npm install -g oxlint",
14039
+ biome: "npm install -g @biomejs/biome",
14040
+ gopls: "go install golang.org/x/tools/gopls@latest",
14041
+ "ruby-lsp": "gem install ruby-lsp",
14042
+ basedpyright: "pip install basedpyright",
14043
+ pyright: "pip install pyright",
14044
+ ty: "pip install ty",
14045
+ ruff: "pip install ruff",
14046
+ "elixir-ls": "See https://github.com/elixir-lsp/elixir-ls",
14047
+ zls: "See https://github.com/zigtools/zls",
14048
+ csharp: "dotnet tool install -g csharp-ls",
14049
+ fsharp: "dotnet tool install -g fsautocomplete",
14050
+ "sourcekit-lsp": "Included with Xcode or Swift toolchain",
14051
+ rust: "rustup component add rust-analyzer",
14052
+ clangd: "See https://clangd.llvm.org/installation",
14053
+ svelte: "npm install -g svelte-language-server",
14054
+ astro: "npm install -g @astrojs/language-server",
14055
+ "bash-ls": "npm install -g bash-language-server",
14056
+ jdtls: "See https://github.com/eclipse-jdtls/eclipse.jdt.ls",
14057
+ "yaml-ls": "npm install -g yaml-language-server",
14058
+ "lua-ls": "See https://github.com/LuaLS/lua-language-server",
14059
+ php: "npm install -g intelephense",
14060
+ dart: "Included with Dart SDK",
14061
+ "terraform-ls": "See https://github.com/hashicorp/terraform-ls"
14062
+ };
13493
14063
  var BUILTIN_SERVERS = {
13494
14064
  typescript: {
13495
14065
  command: ["typescript-language-server", "--stdio"],
@@ -13749,12 +14319,12 @@ var EXT_TO_LANG = {
13749
14319
  // src/tools/lsp/config.ts
13750
14320
  import { existsSync as existsSync34, readFileSync as readFileSync23 } from "fs";
13751
14321
  import { join as join44 } from "path";
13752
- import { homedir as homedir11 } from "os";
13753
- function loadJsonFile(path8) {
13754
- if (!existsSync34(path8))
14322
+ import { homedir as homedir10 } from "os";
14323
+ function loadJsonFile(path7) {
14324
+ if (!existsSync34(path7))
13755
14325
  return null;
13756
14326
  try {
13757
- return JSON.parse(readFileSync23(path8, "utf-8"));
14327
+ return JSON.parse(readFileSync23(path7, "utf-8"));
13758
14328
  } catch {
13759
14329
  return null;
13760
14330
  }
@@ -13763,8 +14333,8 @@ function getConfigPaths2() {
13763
14333
  const cwd = process.cwd();
13764
14334
  return {
13765
14335
  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")
14336
+ user: join44(homedir10(), ".config", "opencode", "oh-my-opencode.json"),
14337
+ opencode: join44(homedir10(), ".config", "opencode", "opencode.json")
13768
14338
  };
13769
14339
  }
13770
14340
  function loadAllConfigs() {
@@ -13836,16 +14406,38 @@ function findServerForExtension(ext) {
13836
14406
  for (const server of servers) {
13837
14407
  if (server.extensions.includes(ext) && isServerInstalled(server.command)) {
13838
14408
  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
14409
+ status: "found",
14410
+ server: {
14411
+ id: server.id,
14412
+ command: server.command,
14413
+ extensions: server.extensions,
14414
+ priority: server.priority,
14415
+ env: server.env,
14416
+ initialization: server.initialization
14417
+ }
13845
14418
  };
13846
14419
  }
13847
14420
  }
13848
- return null;
14421
+ for (const server of servers) {
14422
+ if (server.extensions.includes(ext)) {
14423
+ const installHint = LSP_INSTALL_HINTS[server.id] || `Install '${server.command[0]}' and ensure it's in your PATH`;
14424
+ return {
14425
+ status: "not_installed",
14426
+ server: {
14427
+ id: server.id,
14428
+ command: server.command,
14429
+ extensions: server.extensions
14430
+ },
14431
+ installHint
14432
+ };
14433
+ }
14434
+ }
14435
+ const availableServers = [...new Set(servers.map((s) => s.id))];
14436
+ return {
14437
+ status: "not_configured",
14438
+ extension: ext,
14439
+ availableServers
14440
+ };
13849
14441
  }
13850
14442
  function getLanguageId(ext) {
13851
14443
  return EXT_TO_LANG[ext] || "plaintext";
@@ -13868,10 +14460,10 @@ function isServerInstalled(command) {
13868
14460
  const additionalPaths = [
13869
14461
  join44(cwd, "node_modules", ".bin", cmd),
13870
14462
  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)
14463
+ join44(homedir10(), ".config", "opencode", "bin", cmd),
14464
+ join44(homedir10(), ".config", "opencode", "bin", cmd + ext),
14465
+ join44(homedir10(), ".config", "opencode", "node_modules", ".bin", cmd),
14466
+ join44(homedir10(), ".config", "opencode", "node_modules", ".bin", cmd + ext)
13875
14467
  ];
13876
14468
  for (const p of additionalPaths) {
13877
14469
  if (existsSync34(p)) {
@@ -14486,13 +15078,49 @@ function findWorkspaceRoot(filePath) {
14486
15078
  }
14487
15079
  return __require("path").dirname(resolve6(filePath));
14488
15080
  }
15081
+ function formatServerLookupError(result) {
15082
+ if (result.status === "not_installed") {
15083
+ const { server, installHint } = result;
15084
+ return [
15085
+ `LSP server '${server.id}' is configured but NOT INSTALLED.`,
15086
+ ``,
15087
+ `Command not found: ${server.command[0]}`,
15088
+ ``,
15089
+ `To install:`,
15090
+ ` ${installHint}`,
15091
+ ``,
15092
+ `Supported extensions: ${server.extensions.join(", ")}`,
15093
+ ``,
15094
+ `After installation, the server will be available automatically.`,
15095
+ `Run 'lsp_servers' tool to verify installation status.`
15096
+ ].join(`
15097
+ `);
15098
+ }
15099
+ return [
15100
+ `No LSP server configured for extension: ${result.extension}`,
15101
+ ``,
15102
+ `Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
15103
+ ``,
15104
+ `To add a custom server, configure 'lsp' in oh-my-opencode.json:`,
15105
+ ` {`,
15106
+ ` "lsp": {`,
15107
+ ` "my-server": {`,
15108
+ ` "command": ["my-lsp", "--stdio"],`,
15109
+ ` "extensions": ["${result.extension}"]`,
15110
+ ` }`,
15111
+ ` }`,
15112
+ ` }`
15113
+ ].join(`
15114
+ `);
15115
+ }
14489
15116
  async function withLspClient(filePath, fn) {
14490
15117
  const absPath = resolve6(filePath);
14491
15118
  const ext = extname2(absPath);
14492
- const server = findServerForExtension(ext);
14493
- if (!server) {
14494
- throw new Error(`No LSP server configured for extension: ${ext}`);
15119
+ const result = findServerForExtension(ext);
15120
+ if (result.status !== "found") {
15121
+ throw new Error(formatServerLookupError(result));
14495
15122
  }
15123
+ const server = result.server;
14496
15124
  const root = findWorkspaceRoot(absPath);
14497
15125
  const client = await lspManager.getClient(root, server);
14498
15126
  try {
@@ -15483,10 +16111,10 @@ function mergeDefs(...defs) {
15483
16111
  function cloneDef(schema) {
15484
16112
  return mergeDefs(schema._zod.def);
15485
16113
  }
15486
- function getElementAtPath(obj, path8) {
15487
- if (!path8)
16114
+ function getElementAtPath(obj, path7) {
16115
+ if (!path7)
15488
16116
  return obj;
15489
- return path8.reduce((acc, key) => acc?.[key], obj);
16117
+ return path7.reduce((acc, key) => acc?.[key], obj);
15490
16118
  }
15491
16119
  function promiseAllObject(promisesObj) {
15492
16120
  const keys = Object.keys(promisesObj);
@@ -15845,11 +16473,11 @@ function aborted(x, startIndex = 0) {
15845
16473
  }
15846
16474
  return false;
15847
16475
  }
15848
- function prefixIssues(path8, issues) {
16476
+ function prefixIssues(path7, issues) {
15849
16477
  return issues.map((iss) => {
15850
16478
  var _a;
15851
16479
  (_a = iss).path ?? (_a.path = []);
15852
- iss.path.unshift(path8);
16480
+ iss.path.unshift(path7);
15853
16481
  return iss;
15854
16482
  });
15855
16483
  }
@@ -16017,7 +16645,7 @@ function treeifyError(error, _mapper) {
16017
16645
  return issue2.message;
16018
16646
  };
16019
16647
  const result = { errors: [] };
16020
- const processError = (error2, path8 = []) => {
16648
+ const processError = (error2, path7 = []) => {
16021
16649
  var _a, _b;
16022
16650
  for (const issue2 of error2.issues) {
16023
16651
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -16027,7 +16655,7 @@ function treeifyError(error, _mapper) {
16027
16655
  } else if (issue2.code === "invalid_element") {
16028
16656
  processError({ issues: issue2.issues }, issue2.path);
16029
16657
  } else {
16030
- const fullpath = [...path8, ...issue2.path];
16658
+ const fullpath = [...path7, ...issue2.path];
16031
16659
  if (fullpath.length === 0) {
16032
16660
  result.errors.push(mapper(issue2));
16033
16661
  continue;
@@ -16059,8 +16687,8 @@ function treeifyError(error, _mapper) {
16059
16687
  }
16060
16688
  function toDotPath(_path) {
16061
16689
  const segs = [];
16062
- const path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16063
- for (const seg of path8) {
16690
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16691
+ for (const seg of path7) {
16064
16692
  if (typeof seg === "number")
16065
16693
  segs.push(`[${seg}]`);
16066
16694
  else if (typeof seg === "symbol")
@@ -24838,10 +25466,10 @@ function _property(property, schema, params) {
24838
25466
  ...normalizeParams(params)
24839
25467
  });
24840
25468
  }
24841
- function _mime(types10, params) {
25469
+ function _mime(types11, params) {
24842
25470
  return new $ZodCheckMimeType({
24843
25471
  check: "mime_type",
24844
- mime: types10,
25472
+ mime: types11,
24845
25473
  ...normalizeParams(params)
24846
25474
  });
24847
25475
  }
@@ -26751,7 +27379,7 @@ var ZodFile = /* @__PURE__ */ $constructor("ZodFile", (inst, def) => {
26751
27379
  ZodType.init(inst, def);
26752
27380
  inst.min = (size, params) => inst.check(_minSize(size, params));
26753
27381
  inst.max = (size, params) => inst.check(_maxSize(size, params));
26754
- inst.mime = (types10, params) => inst.check(_mime(Array.isArray(types10) ? types10 : [types10], params));
27382
+ inst.mime = (types11, params) => inst.check(_mime(Array.isArray(types11) ? types11 : [types11], params));
26755
27383
  });
26756
27384
  function file(params) {
26757
27385
  return _file(ZodFile, params);
@@ -27410,7 +28038,7 @@ import { existsSync as existsSync37, statSync as statSync4 } from "fs";
27410
28038
  var {spawn: spawn6 } = globalThis.Bun;
27411
28039
  import { existsSync as existsSync36, mkdirSync as mkdirSync10, chmodSync as chmodSync2, unlinkSync as unlinkSync9 } from "fs";
27412
28040
  import { join as join45 } from "path";
27413
- import { homedir as homedir12 } from "os";
28041
+ import { homedir as homedir11 } from "os";
27414
28042
  import { createRequire as createRequire3 } from "module";
27415
28043
  var REPO2 = "ast-grep/ast-grep";
27416
28044
  var DEFAULT_VERSION = "0.40.0";
@@ -27435,11 +28063,11 @@ var PLATFORM_MAP2 = {
27435
28063
  function getCacheDir3() {
27436
28064
  if (process.platform === "win32") {
27437
28065
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
27438
- const base2 = localAppData || join45(homedir12(), "AppData", "Local");
28066
+ const base2 = localAppData || join45(homedir11(), "AppData", "Local");
27439
28067
  return join45(base2, "oh-my-opencode", "bin");
27440
28068
  }
27441
- const xdgCache2 = process.env.XDG_CACHE_HOME;
27442
- const base = xdgCache2 || join45(homedir12(), ".cache");
28069
+ const xdgCache = process.env.XDG_CACHE_HOME;
28070
+ const base = xdgCache || join45(homedir11(), ".cache");
27443
28071
  return join45(base, "oh-my-opencode", "bin");
27444
28072
  }
27445
28073
  function getBinaryName3() {
@@ -27477,8 +28105,8 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
27477
28105
  if (existsSync36(binaryPath)) {
27478
28106
  return binaryPath;
27479
28107
  }
27480
- const { arch, os: os6 } = platformInfo;
27481
- const assetName = `app-${arch}-${os6}.zip`;
28108
+ const { arch, os: os5 } = platformInfo;
28109
+ const assetName = `app-${arch}-${os5}.zip`;
27482
28110
  const downloadUrl = `https://github.com/${REPO2}/releases/download/${version2}/${assetName}`;
27483
28111
  console.log(`[oh-my-opencode] Downloading ast-grep binary...`);
27484
28112
  try {
@@ -27567,9 +28195,9 @@ function findSgCliPathSync() {
27567
28195
  }
27568
28196
  if (process.platform === "darwin") {
27569
28197
  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;
28198
+ for (const path7 of homebrewPaths) {
28199
+ if (existsSync37(path7) && isValidBinary(path7)) {
28200
+ return path7;
27573
28201
  }
27574
28202
  }
27575
28203
  }
@@ -27587,10 +28215,9 @@ function getSgCliPath() {
27587
28215
  }
27588
28216
  return "sg";
27589
28217
  }
27590
- function setSgCliPath(path8) {
27591
- resolvedCliPath2 = path8;
28218
+ function setSgCliPath(path7) {
28219
+ resolvedCliPath2 = path7;
27592
28220
  }
27593
- var SG_CLI_PATH = getSgCliPath();
27594
28221
  var CLI_LANGUAGES = [
27595
28222
  "bash",
27596
28223
  "c",
@@ -28390,7 +29017,7 @@ function discoverCommandsFromDir(commandsDir, scope) {
28390
29017
  return [];
28391
29018
  }
28392
29019
  const entries = readdirSync15(commandsDir, { withFileTypes: true });
28393
- const commands = [];
29020
+ const commands2 = [];
28394
29021
  for (const entry of entries) {
28395
29022
  if (!isMarkdownFile(entry))
28396
29023
  continue;
@@ -28408,7 +29035,7 @@ function discoverCommandsFromDir(commandsDir, scope) {
28408
29035
  agent: data.agent,
28409
29036
  subtask: Boolean(data.subtask)
28410
29037
  };
28411
- commands.push({
29038
+ commands2.push({
28412
29039
  name: commandName,
28413
29040
  path: commandPath,
28414
29041
  metadata,
@@ -28419,13 +29046,13 @@ function discoverCommandsFromDir(commandsDir, scope) {
28419
29046
  continue;
28420
29047
  }
28421
29048
  }
28422
- return commands;
29049
+ return commands2;
28423
29050
  }
28424
29051
  function discoverCommandsSync() {
28425
- const { homedir: homedir13 } = __require("os");
29052
+ const { homedir: homedir12 } = __require("os");
28426
29053
  const userCommandsDir = join49(getClaudeConfigDir(), "commands");
28427
29054
  const projectCommandsDir = join49(process.cwd(), ".claude", "commands");
28428
- const opencodeGlobalDir = join49(homedir13(), ".config", "opencode", "command");
29055
+ const opencodeGlobalDir = join49(homedir12(), ".config", "opencode", "command");
28429
29056
  const opencodeProjectDir = join49(process.cwd(), ".opencode", "command");
28430
29057
  const userCommands = discoverCommandsFromDir(userCommandsDir, "user");
28431
29058
  const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode");
@@ -28476,18 +29103,18 @@ async function formatLoadedCommand(cmd) {
28476
29103
  return sections.join(`
28477
29104
  `);
28478
29105
  }
28479
- function formatCommandList(commands) {
28480
- if (commands.length === 0) {
29106
+ function formatCommandList(commands2) {
29107
+ if (commands2.length === 0) {
28481
29108
  return "No commands found.";
28482
29109
  }
28483
29110
  const lines = [`# Available Commands
28484
29111
  `];
28485
- for (const cmd of commands) {
29112
+ for (const cmd of commands2) {
28486
29113
  const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : "";
28487
29114
  lines.push(`- **/${cmd.name}${hint}**: ${cmd.metadata.description || "(no description)"} (${cmd.scope})`);
28488
29115
  }
28489
29116
  lines.push(`
28490
- **Total**: ${commands.length} commands`);
29117
+ **Total**: ${commands2.length} commands`);
28491
29118
  return lines.join(`
28492
29119
  `);
28493
29120
  }
@@ -28524,27 +29151,27 @@ ${commandListForDescription}`,
28524
29151
  command: tool.schema.string().describe("The slash command to execute (without the leading slash). E.g., 'commit', 'plan', 'execute'.")
28525
29152
  },
28526
29153
  async execute(args) {
28527
- const commands = discoverCommandsSync();
29154
+ const commands2 = discoverCommandsSync();
28528
29155
  if (!args.command) {
28529
- return formatCommandList(commands) + `
29156
+ return formatCommandList(commands2) + `
28530
29157
 
28531
29158
  Provide a command name to execute.`;
28532
29159
  }
28533
29160
  const cmdName = args.command.replace(/^\//, "");
28534
- const exactMatch = commands.find((cmd) => cmd.name.toLowerCase() === cmdName.toLowerCase());
29161
+ const exactMatch = commands2.find((cmd) => cmd.name.toLowerCase() === cmdName.toLowerCase());
28535
29162
  if (exactMatch) {
28536
29163
  return await formatLoadedCommand(exactMatch);
28537
29164
  }
28538
- const partialMatches = commands.filter((cmd) => cmd.name.toLowerCase().includes(cmdName.toLowerCase()));
29165
+ const partialMatches = commands2.filter((cmd) => cmd.name.toLowerCase().includes(cmdName.toLowerCase()));
28539
29166
  if (partialMatches.length > 0) {
28540
29167
  const matchList = partialMatches.map((cmd) => `/${cmd.name}`).join(", ");
28541
29168
  return `No exact match for "/${cmdName}". Did you mean: ${matchList}?
28542
29169
 
28543
- ` + formatCommandList(commands);
29170
+ ` + formatCommandList(commands2);
28544
29171
  }
28545
29172
  return `Command "/${cmdName}" not found.
28546
29173
 
28547
- ` + formatCommandList(commands) + `
29174
+ ` + formatCommandList(commands2) + `
28548
29175
 
28549
29176
  Try a different command name.`;
28550
29177
  }
@@ -28552,7 +29179,7 @@ Try a different command name.`;
28552
29179
  // src/tools/session-manager/constants.ts
28553
29180
  import { join as join50 } from "path";
28554
29181
  var OPENCODE_STORAGE9 = getOpenCodeStorageDir();
28555
- var MESSAGE_STORAGE8 = join50(OPENCODE_STORAGE9, "message");
29182
+ var MESSAGE_STORAGE4 = join50(OPENCODE_STORAGE9, "message");
28556
29183
  var PART_STORAGE4 = join50(OPENCODE_STORAGE9, "part");
28557
29184
  var TODO_DIR2 = join50(getClaudeConfigDir(), "todos");
28558
29185
  var TRANSCRIPT_DIR2 = join50(getClaudeConfigDir(), "transcripts");
@@ -28628,22 +29255,24 @@ Has Todos: Yes (12 items, 8 completed)
28628
29255
  Has Transcript: Yes (234 entries)`;
28629
29256
 
28630
29257
  // src/tools/session-manager/storage.ts
28631
- import { existsSync as existsSync42, readdirSync as readdirSync16, readFileSync as readFileSync27 } from "fs";
29258
+ import { existsSync as existsSync42, readdirSync as readdirSync16 } from "fs";
29259
+ import { readdir, readFile } from "fs/promises";
28632
29260
  import { join as join51 } from "path";
28633
- function getAllSessions() {
28634
- if (!existsSync42(MESSAGE_STORAGE8))
29261
+ async function getAllSessions() {
29262
+ if (!existsSync42(MESSAGE_STORAGE4))
28635
29263
  return [];
28636
29264
  const sessions = [];
28637
- function scanDirectory(dir) {
29265
+ async function scanDirectory(dir) {
28638
29266
  try {
28639
- for (const entry of readdirSync16(dir, { withFileTypes: true })) {
29267
+ const entries = await readdir(dir, { withFileTypes: true });
29268
+ for (const entry of entries) {
28640
29269
  if (entry.isDirectory()) {
28641
29270
  const sessionPath = join51(dir, entry.name);
28642
- const files = readdirSync16(sessionPath);
29271
+ const files = await readdir(sessionPath);
28643
29272
  if (files.some((f) => f.endsWith(".json"))) {
28644
29273
  sessions.push(entry.name);
28645
29274
  } else {
28646
- scanDirectory(sessionPath);
29275
+ await scanDirectory(sessionPath);
28647
29276
  }
28648
29277
  }
28649
29278
  }
@@ -28651,49 +29280,58 @@ function getAllSessions() {
28651
29280
  return;
28652
29281
  }
28653
29282
  }
28654
- scanDirectory(MESSAGE_STORAGE8);
29283
+ await scanDirectory(MESSAGE_STORAGE4);
28655
29284
  return [...new Set(sessions)];
28656
29285
  }
28657
29286
  function getMessageDir9(sessionID) {
28658
- if (!existsSync42(MESSAGE_STORAGE8))
29287
+ if (!existsSync42(MESSAGE_STORAGE4))
28659
29288
  return "";
28660
- const directPath = join51(MESSAGE_STORAGE8, sessionID);
29289
+ const directPath = join51(MESSAGE_STORAGE4, sessionID);
28661
29290
  if (existsSync42(directPath)) {
28662
29291
  return directPath;
28663
29292
  }
28664
- for (const dir of readdirSync16(MESSAGE_STORAGE8)) {
28665
- const sessionPath = join51(MESSAGE_STORAGE8, dir, sessionID);
28666
- if (existsSync42(sessionPath)) {
28667
- return sessionPath;
29293
+ try {
29294
+ for (const dir of readdirSync16(MESSAGE_STORAGE4)) {
29295
+ const sessionPath = join51(MESSAGE_STORAGE4, dir, sessionID);
29296
+ if (existsSync42(sessionPath)) {
29297
+ return sessionPath;
29298
+ }
28668
29299
  }
29300
+ } catch {
29301
+ return "";
28669
29302
  }
28670
29303
  return "";
28671
29304
  }
28672
29305
  function sessionExists(sessionID) {
28673
29306
  return getMessageDir9(sessionID) !== "";
28674
29307
  }
28675
- function readSessionMessages(sessionID) {
29308
+ async function readSessionMessages(sessionID) {
28676
29309
  const messageDir = getMessageDir9(sessionID);
28677
29310
  if (!messageDir || !existsSync42(messageDir))
28678
29311
  return [];
28679
29312
  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;
29313
+ try {
29314
+ const files = await readdir(messageDir);
29315
+ for (const file2 of files) {
29316
+ if (!file2.endsWith(".json"))
29317
+ continue;
29318
+ try {
29319
+ const content = await readFile(join51(messageDir, file2), "utf-8");
29320
+ const meta = JSON.parse(content);
29321
+ const parts = await readParts2(meta.id);
29322
+ messages.push({
29323
+ id: meta.id,
29324
+ role: meta.role,
29325
+ agent: meta.agent,
29326
+ time: meta.time,
29327
+ parts
29328
+ });
29329
+ } catch {
29330
+ continue;
29331
+ }
28696
29332
  }
29333
+ } catch {
29334
+ return [];
28697
29335
  }
28698
29336
  return messages.sort((a, b) => {
28699
29337
  const aTime = a.time?.created ?? 0;
@@ -28703,61 +29341,71 @@ function readSessionMessages(sessionID) {
28703
29341
  return a.id.localeCompare(b.id);
28704
29342
  });
28705
29343
  }
28706
- function readParts2(messageID) {
29344
+ async function readParts2(messageID) {
28707
29345
  const partDir = join51(PART_STORAGE4, messageID);
28708
29346
  if (!existsSync42(partDir))
28709
29347
  return [];
28710
29348
  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;
29349
+ try {
29350
+ const files = await readdir(partDir);
29351
+ for (const file2 of files) {
29352
+ if (!file2.endsWith(".json"))
29353
+ continue;
29354
+ try {
29355
+ const content = await readFile(join51(partDir, file2), "utf-8");
29356
+ parts.push(JSON.parse(content));
29357
+ } catch {
29358
+ continue;
29359
+ }
28719
29360
  }
29361
+ } catch {
29362
+ return [];
28720
29363
  }
28721
29364
  return parts.sort((a, b) => a.id.localeCompare(b.id));
28722
29365
  }
28723
- function readSessionTodos(sessionID) {
29366
+ async function readSessionTodos(sessionID) {
28724
29367
  if (!existsSync42(TODO_DIR2))
28725
29368
  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
- }));
29369
+ try {
29370
+ const allFiles = await readdir(TODO_DIR2);
29371
+ const todoFiles = allFiles.filter((f) => f.includes(sessionID) && f.endsWith(".json"));
29372
+ for (const file2 of todoFiles) {
29373
+ try {
29374
+ const content = await readFile(join51(TODO_DIR2, file2), "utf-8");
29375
+ const data = JSON.parse(content);
29376
+ if (Array.isArray(data)) {
29377
+ return data.map((item) => ({
29378
+ id: item.id || "",
29379
+ content: item.content || "",
29380
+ status: item.status || "pending",
29381
+ priority: item.priority
29382
+ }));
29383
+ }
29384
+ } catch {
29385
+ continue;
28738
29386
  }
28739
- } catch {
28740
- continue;
28741
29387
  }
29388
+ } catch {
29389
+ return [];
28742
29390
  }
28743
29391
  return [];
28744
29392
  }
28745
- function readSessionTranscript(sessionID) {
29393
+ async function readSessionTranscript(sessionID) {
28746
29394
  if (!existsSync42(TRANSCRIPT_DIR2))
28747
29395
  return 0;
28748
29396
  const transcriptFile = join51(TRANSCRIPT_DIR2, `${sessionID}.jsonl`);
28749
29397
  if (!existsSync42(transcriptFile))
28750
29398
  return 0;
28751
29399
  try {
28752
- const content = readFileSync27(transcriptFile, "utf-8");
29400
+ const content = await readFile(transcriptFile, "utf-8");
28753
29401
  return content.trim().split(`
28754
29402
  `).filter(Boolean).length;
28755
29403
  } catch {
28756
29404
  return 0;
28757
29405
  }
28758
29406
  }
28759
- function getSessionInfo(sessionID) {
28760
- const messages = readSessionMessages(sessionID);
29407
+ async function getSessionInfo(sessionID) {
29408
+ const messages = await readSessionMessages(sessionID);
28761
29409
  if (messages.length === 0)
28762
29410
  return null;
28763
29411
  const agentsUsed = new Set;
@@ -28774,8 +29422,8 @@ function getSessionInfo(sessionID) {
28774
29422
  lastMessage = date5;
28775
29423
  }
28776
29424
  }
28777
- const todos = readSessionTodos(sessionID);
28778
- const transcriptEntries = readSessionTranscript(sessionID);
29425
+ const todos = await readSessionTodos(sessionID);
29426
+ const transcriptEntries = await readSessionTranscript(sessionID);
28779
29427
  return {
28780
29428
  id: sessionID,
28781
29429
  message_count: messages.length,
@@ -28790,11 +29438,11 @@ function getSessionInfo(sessionID) {
28790
29438
  }
28791
29439
 
28792
29440
  // src/tools/session-manager/utils.ts
28793
- function formatSessionList(sessionIDs) {
29441
+ async function formatSessionList(sessionIDs) {
28794
29442
  if (sessionIDs.length === 0) {
28795
29443
  return "No sessions found.";
28796
29444
  }
28797
- const infos = sessionIDs.map((id) => getSessionInfo(id)).filter((info) => info !== null);
29445
+ const infos = (await Promise.all(sessionIDs.map((id) => getSessionInfo(id)))).filter((info) => info !== null);
28798
29446
  if (infos.length === 0) {
28799
29447
  return "No valid sessions found.";
28800
29448
  }
@@ -28886,29 +29534,33 @@ function formatSearchResults(results) {
28886
29534
  return lines.join(`
28887
29535
  `);
28888
29536
  }
28889
- function filterSessionsByDate(sessionIDs, fromDate, toDate) {
29537
+ async function filterSessionsByDate(sessionIDs, fromDate, toDate) {
28890
29538
  if (!fromDate && !toDate)
28891
29539
  return sessionIDs;
28892
29540
  const from = fromDate ? new Date(fromDate) : null;
28893
29541
  const to = toDate ? new Date(toDate) : null;
28894
- return sessionIDs.filter((id) => {
28895
- const info = getSessionInfo(id);
29542
+ const results = [];
29543
+ for (const id of sessionIDs) {
29544
+ const info = await getSessionInfo(id);
28896
29545
  if (!info || !info.last_message)
28897
- return false;
29546
+ continue;
28898
29547
  if (from && info.last_message < from)
28899
- return false;
29548
+ continue;
28900
29549
  if (to && info.last_message > to)
28901
- return false;
28902
- return true;
28903
- });
29550
+ continue;
29551
+ results.push(id);
29552
+ }
29553
+ return results;
28904
29554
  }
28905
- function searchInSession(sessionID, query, caseSensitive = false) {
28906
- const messages = readSessionMessages(sessionID);
29555
+ async function searchInSession(sessionID, query, caseSensitive = false, maxResults) {
29556
+ const messages = await readSessionMessages(sessionID);
28907
29557
  const results = [];
28908
29558
  const searchQuery = caseSensitive ? query : query.toLowerCase();
28909
29559
  for (const msg of messages) {
29560
+ if (maxResults && results.length >= maxResults)
29561
+ break;
28910
29562
  let matchCount = 0;
28911
- let excerpts = [];
29563
+ const excerpts = [];
28912
29564
  for (const part of msg.parts) {
28913
29565
  if (part.type === "text" && part.text) {
28914
29566
  const text = caseSensitive ? part.text : part.text.toLowerCase();
@@ -28944,6 +29596,14 @@ function searchInSession(sessionID, query, caseSensitive = false) {
28944
29596
  }
28945
29597
 
28946
29598
  // src/tools/session-manager/tools.ts
29599
+ var SEARCH_TIMEOUT_MS = 60000;
29600
+ var MAX_SESSIONS_TO_SCAN = 50;
29601
+ function withTimeout(promise2, ms, operation) {
29602
+ return Promise.race([
29603
+ promise2,
29604
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`${operation} timed out after ${ms}ms`)), ms))
29605
+ ]);
29606
+ }
28947
29607
  var session_list = tool({
28948
29608
  description: SESSION_LIST_DESCRIPTION,
28949
29609
  args: {
@@ -28953,14 +29613,14 @@ var session_list = tool({
28953
29613
  },
28954
29614
  execute: async (args, _context) => {
28955
29615
  try {
28956
- let sessions = getAllSessions();
29616
+ let sessions = await getAllSessions();
28957
29617
  if (args.from_date || args.to_date) {
28958
- sessions = filterSessionsByDate(sessions, args.from_date, args.to_date);
29618
+ sessions = await filterSessionsByDate(sessions, args.from_date, args.to_date);
28959
29619
  }
28960
29620
  if (args.limit && args.limit > 0) {
28961
29621
  sessions = sessions.slice(0, args.limit);
28962
29622
  }
28963
- return formatSessionList(sessions);
29623
+ return await formatSessionList(sessions);
28964
29624
  } catch (e) {
28965
29625
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
28966
29626
  }
@@ -28979,11 +29639,11 @@ var session_read = tool({
28979
29639
  if (!sessionExists(args.session_id)) {
28980
29640
  return `Session not found: ${args.session_id}`;
28981
29641
  }
28982
- let messages = readSessionMessages(args.session_id);
29642
+ let messages = await readSessionMessages(args.session_id);
28983
29643
  if (args.limit && args.limit > 0) {
28984
29644
  messages = messages.slice(0, args.limit);
28985
29645
  }
28986
- const todos = args.include_todos ? readSessionTodos(args.session_id) : undefined;
29646
+ const todos = args.include_todos ? await readSessionTodos(args.session_id) : undefined;
28987
29647
  return formatSessionMessages(messages, args.include_todos, todos);
28988
29648
  } catch (e) {
28989
29649
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
@@ -29000,10 +29660,25 @@ var session_search = tool({
29000
29660
  },
29001
29661
  execute: async (args, _context) => {
29002
29662
  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);
29663
+ const resultLimit = args.limit && args.limit > 0 ? args.limit : 20;
29664
+ const searchOperation = async () => {
29665
+ if (args.session_id) {
29666
+ return searchInSession(args.session_id, args.query, args.case_sensitive, resultLimit);
29667
+ }
29668
+ const allSessions = await getAllSessions();
29669
+ const sessionsToScan = allSessions.slice(0, MAX_SESSIONS_TO_SCAN);
29670
+ const allResults = [];
29671
+ for (const sid of sessionsToScan) {
29672
+ if (allResults.length >= resultLimit)
29673
+ break;
29674
+ const remaining = resultLimit - allResults.length;
29675
+ const sessionResults = await searchInSession(sid, args.query, args.case_sensitive, remaining);
29676
+ allResults.push(...sessionResults);
29677
+ }
29678
+ return allResults.slice(0, resultLimit);
29679
+ };
29680
+ const results = await withTimeout(searchOperation(), SEARCH_TIMEOUT_MS, "Search");
29681
+ return formatSearchResults(results);
29007
29682
  } catch (e) {
29008
29683
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
29009
29684
  }
@@ -29016,7 +29691,7 @@ var session_info = tool({
29016
29691
  },
29017
29692
  execute: async (args, _context) => {
29018
29693
  try {
29019
- const info = getSessionInfo(args.session_id);
29694
+ const info = await getSessionInfo(args.session_id);
29020
29695
  if (!info) {
29021
29696
  return `Session not found: ${args.session_id}`;
29022
29697
  }
@@ -29059,12 +29734,12 @@ async function findTmuxPath() {
29059
29734
  return null;
29060
29735
  }
29061
29736
  const stdout = await new Response(proc.stdout).text();
29062
- const path8 = stdout.trim().split(`
29737
+ const path7 = stdout.trim().split(`
29063
29738
  `)[0];
29064
- if (!path8) {
29739
+ if (!path7) {
29065
29740
  return null;
29066
29741
  }
29067
- const verifyProc = spawn10([path8, "-V"], {
29742
+ const verifyProc = spawn10([path7, "-V"], {
29068
29743
  stdout: "pipe",
29069
29744
  stderr: "pipe"
29070
29745
  });
@@ -29072,7 +29747,7 @@ async function findTmuxPath() {
29072
29747
  if (verifyExitCode !== 0) {
29073
29748
  return null;
29074
29749
  }
29075
- return path8;
29750
+ return path7;
29076
29751
  } catch {
29077
29752
  return null;
29078
29753
  }
@@ -29085,9 +29760,9 @@ async function getTmuxPath() {
29085
29760
  return initPromise3;
29086
29761
  }
29087
29762
  initPromise3 = (async () => {
29088
- const path8 = await findTmuxPath();
29089
- tmuxPath = path8;
29090
- return path8;
29763
+ const path7 = await findTmuxPath();
29764
+ tmuxPath = path7;
29765
+ return path7;
29091
29766
  })();
29092
29767
  return initPromise3;
29093
29768
  }
@@ -29224,21 +29899,26 @@ function createBackgroundTask(manager) {
29224
29899
  agent: tool.schema.string().describe("Agent type to use (any registered agent)")
29225
29900
  },
29226
29901
  async execute(args, toolContext) {
29902
+ const ctx = toolContext;
29227
29903
  if (!args.agent || args.agent.trim() === "") {
29228
29904
  return `\u274C Agent parameter is required. Please specify which agent to use (e.g., "explore", "librarian", "build", etc.)`;
29229
29905
  }
29230
29906
  try {
29231
- const messageDir = getMessageDir10(toolContext.sessionID);
29907
+ const messageDir = getMessageDir10(ctx.sessionID);
29232
29908
  const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null;
29233
29909
  const parentModel = prevMessage?.model?.providerID && prevMessage?.model?.modelID ? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID } : undefined;
29234
29910
  const task = await manager.launch({
29235
29911
  description: args.description,
29236
29912
  prompt: args.prompt,
29237
29913
  agent: args.agent.trim(),
29238
- parentSessionID: toolContext.sessionID,
29239
- parentMessageID: toolContext.messageID,
29914
+ parentSessionID: ctx.sessionID,
29915
+ parentMessageID: ctx.messageID,
29240
29916
  parentModel
29241
29917
  });
29918
+ ctx.metadata?.({
29919
+ title: args.description,
29920
+ metadata: { sessionId: task.sessionID }
29921
+ });
29242
29922
  return `Background task launched successfully.
29243
29923
 
29244
29924
  Task ID: ${task.id}
@@ -29494,6 +30174,7 @@ function createCallOmoAgent(ctx, backgroundManager) {
29494
30174
  session_id: tool.schema.string().describe("Existing Task session to continue").optional()
29495
30175
  },
29496
30176
  async execute(args, toolContext) {
30177
+ const toolCtx = toolContext;
29497
30178
  log(`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`);
29498
30179
  if (!ALLOWED_AGENTS.includes(args.subagent_type)) {
29499
30180
  return `Error: Invalid agent type "${args.subagent_type}". Only ${ALLOWED_AGENTS.join(", ")} are allowed.`;
@@ -29502,9 +30183,9 @@ function createCallOmoAgent(ctx, backgroundManager) {
29502
30183
  if (args.session_id) {
29503
30184
  return `Error: session_id is not supported in background mode. Use run_in_background=false to continue an existing session.`;
29504
30185
  }
29505
- return await executeBackground(args, toolContext, backgroundManager);
30186
+ return await executeBackground(args, toolCtx, backgroundManager);
29506
30187
  }
29507
- return await executeSync(args, toolContext, ctx);
30188
+ return await executeSync(args, toolCtx, ctx);
29508
30189
  }
29509
30190
  });
29510
30191
  }
@@ -29517,6 +30198,10 @@ async function executeBackground(args, toolContext, manager) {
29517
30198
  parentSessionID: toolContext.sessionID,
29518
30199
  parentMessageID: toolContext.messageID
29519
30200
  });
30201
+ toolContext.metadata?.({
30202
+ title: args.description,
30203
+ metadata: { sessionId: task.sessionID }
30204
+ });
29520
30205
  return `Background agent task launched successfully.
29521
30206
 
29522
30207
  Task ID: ${task.id}
@@ -29561,6 +30246,10 @@ async function executeSync(args, toolContext, ctx) {
29561
30246
  sessionID = createResult.data.id;
29562
30247
  log(`[call_omo_agent] Created session: ${sessionID}`);
29563
30248
  }
30249
+ toolContext.metadata?.({
30250
+ title: args.description,
30251
+ metadata: { sessionId: sessionID }
30252
+ });
29564
30253
  log(`[call_omo_agent] Sending prompt to session ${sessionID}`);
29565
30254
  log(`[call_omo_agent] Prompt text:`, args.prompt.substring(0, 100));
29566
30255
  try {
@@ -30006,6 +30695,7 @@ class BackgroundManager {
30006
30695
  }
30007
30696
  const message = `[BACKGROUND TASK COMPLETED] Task "${task.description}" finished in ${duration3}. Use background_output with task_id="${task.id}" to get results.`;
30008
30697
  log("[background-agent] Sending notification to parent session:", { parentSessionID: task.parentSessionID });
30698
+ const taskId = task.id;
30009
30699
  setTimeout(async () => {
30010
30700
  try {
30011
30701
  const messageDir = getMessageDir11(task.parentSessionID);
@@ -30021,10 +30711,13 @@ class BackgroundManager {
30021
30711
  },
30022
30712
  query: { directory: this.directory }
30023
30713
  });
30024
- this.clearNotificationsForTask(task.id);
30714
+ this.clearNotificationsForTask(taskId);
30025
30715
  log("[background-agent] Successfully sent prompt to parent session:", { parentSessionID: task.parentSessionID });
30026
30716
  } catch (error45) {
30027
30717
  log("[background-agent] prompt failed:", String(error45));
30718
+ } finally {
30719
+ this.tasks.delete(taskId);
30720
+ log("[background-agent] Removed completed task from memory:", taskId);
30028
30721
  }
30029
30722
  }, 200);
30030
30723
  }
@@ -30212,6 +30905,9 @@ var HookNameSchema = exports_external.enum([
30212
30905
  "empty-message-sanitizer",
30213
30906
  "thinking-block-validator"
30214
30907
  ]);
30908
+ var BuiltinCommandNameSchema = exports_external.enum([
30909
+ "init-deep"
30910
+ ]);
30215
30911
  var AgentOverrideConfigSchema = exports_external.object({
30216
30912
  model: exports_external.string().optional(),
30217
30913
  temperature: exports_external.number().min(0).max(2).optional(),
@@ -30253,6 +30949,9 @@ var SisyphusAgentConfigSchema = exports_external.object({
30253
30949
  planner_enabled: exports_external.boolean().optional(),
30254
30950
  replace_plan: exports_external.boolean().optional()
30255
30951
  });
30952
+ var CommentCheckerConfigSchema = exports_external.object({
30953
+ custom_prompt: exports_external.string().optional()
30954
+ });
30256
30955
  var DynamicContextPruningConfigSchema = exports_external.object({
30257
30956
  enabled: exports_external.boolean().default(false),
30258
30957
  notification: exports_external.enum(["off", "minimal", "detailed"]).default("detailed"),
@@ -30291,17 +30990,19 @@ var ExperimentalConfigSchema = exports_external.object({
30291
30990
  preemptive_compaction_threshold: exports_external.number().min(0.5).max(0.95).optional(),
30292
30991
  truncate_all_tool_outputs: exports_external.boolean().default(true),
30293
30992
  dynamic_context_pruning: DynamicContextPruningConfigSchema.optional(),
30294
- dcp_on_compaction_failure: exports_external.boolean().optional()
30993
+ dcp_for_compaction: exports_external.boolean().optional()
30295
30994
  });
30296
30995
  var OhMyOpenCodeConfigSchema = exports_external.object({
30297
30996
  $schema: exports_external.string().optional(),
30298
30997
  disabled_mcps: exports_external.array(McpNameSchema).optional(),
30299
30998
  disabled_agents: exports_external.array(BuiltinAgentNameSchema).optional(),
30300
30999
  disabled_hooks: exports_external.array(HookNameSchema).optional(),
31000
+ disabled_commands: exports_external.array(BuiltinCommandNameSchema).optional(),
30301
31001
  agents: AgentOverridesSchema.optional(),
30302
31002
  claude_code: ClaudeCodeConfigSchema.optional(),
30303
31003
  google_auth: exports_external.boolean().optional(),
30304
31004
  sisyphus_agent: SisyphusAgentConfigSchema.optional(),
31005
+ comment_checker: CommentCheckerConfigSchema.optional(),
30305
31006
  experimental: ExperimentalConfigSchema.optional(),
30306
31007
  auto_update: exports_external.boolean().optional()
30307
31008
  });
@@ -30378,7 +31079,7 @@ var PLAN_PERMISSION = {
30378
31079
 
30379
31080
  // src/index.ts
30380
31081
  import * as fs7 from "fs";
30381
- import * as path8 from "path";
31082
+ import * as path7 from "path";
30382
31083
  var AGENT_NAME_MAP = {
30383
31084
  omo: "Sisyphus",
30384
31085
  OmO: "Sisyphus",
@@ -30477,14 +31178,20 @@ function mergeConfigs(base, override) {
30477
31178
  ...override.disabled_hooks ?? []
30478
31179
  ])
30479
31180
  ],
31181
+ disabled_commands: [
31182
+ ...new Set([
31183
+ ...base.disabled_commands ?? [],
31184
+ ...override.disabled_commands ?? []
31185
+ ])
31186
+ ],
30480
31187
  claude_code: deepMerge(base.claude_code, override.claude_code)
30481
31188
  };
30482
31189
  }
30483
31190
  function loadPluginConfig(directory, ctx) {
30484
- const userBasePath = path8.join(getUserConfigDir(), "opencode", "oh-my-opencode");
31191
+ const userBasePath = path7.join(getUserConfigDir(), "opencode", "oh-my-opencode");
30485
31192
  const userDetected = detectConfigFile(userBasePath);
30486
31193
  const userConfigPath = userDetected.format !== "none" ? userDetected.path : userBasePath + ".json";
30487
- const projectBasePath = path8.join(directory, ".opencode", "oh-my-opencode");
31194
+ const projectBasePath = path7.join(directory, ".opencode", "oh-my-opencode");
30488
31195
  const projectDetected = detectConfigFile(projectBasePath);
30489
31196
  const projectConfigPath = projectDetected.format !== "none" ? projectDetected.path : projectBasePath + ".json";
30490
31197
  let config3 = loadConfigFromPath2(userConfigPath, ctx) ?? {};
@@ -30520,7 +31227,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
30520
31227
  const contextWindowMonitor = isHookEnabled("context-window-monitor") ? createContextWindowMonitorHook(ctx) : null;
30521
31228
  const sessionRecovery = isHookEnabled("session-recovery") ? createSessionRecoveryHook(ctx, { experimental: pluginConfig.experimental }) : null;
30522
31229
  const sessionNotification = isHookEnabled("session-notification") ? createSessionNotification(ctx) : null;
30523
- const commentChecker = isHookEnabled("comment-checker") ? createCommentCheckerHooks() : null;
31230
+ const commentChecker = isHookEnabled("comment-checker") ? createCommentCheckerHooks(pluginConfig.comment_checker) : null;
30524
31231
  const toolOutputTruncator = isHookEnabled("tool-output-truncator") ? createToolOutputTruncatorHook(ctx, { experimental: pluginConfig.experimental }) : null;
30525
31232
  const directoryAgentsInjector = isHookEnabled("directory-agents-injector") ? createDirectoryAgentsInjectorHook(ctx) : null;
30526
31233
  const directoryReadmeInjector = isHookEnabled("directory-readme-injector") ? createDirectoryReadmeInjectorHook(ctx) : null;
@@ -30700,12 +31407,14 @@ var OhMyOpenCodePlugin = async (ctx) => {
30700
31407
  ...mcpResult.servers,
30701
31408
  ...pluginComponents.mcpServers
30702
31409
  };
31410
+ const builtinCommands = loadBuiltinCommands(pluginConfig.disabled_commands);
30703
31411
  const userCommands = pluginConfig.claude_code?.commands ?? true ? loadUserCommands() : {};
30704
31412
  const opencodeGlobalCommands = loadOpencodeGlobalCommands();
30705
31413
  const systemCommands = config3.command ?? {};
30706
31414
  const projectCommands = pluginConfig.claude_code?.commands ?? true ? loadProjectCommands() : {};
30707
31415
  const opencodeProjectCommands = loadOpencodeProjectCommands();
30708
31416
  config3.command = {
31417
+ ...builtinCommands,
30709
31418
  ...userCommands,
30710
31419
  ...opencodeGlobalCommands,
30711
31420
  ...systemCommands,