oh-my-opencode 2.6.1 → 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;
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;
4684
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();
@@ -13186,6 +13726,12 @@ function isPluginEnabled(pluginKey, settingsEnabledPlugins, overrideEnabledPlugi
13186
13726
  }
13187
13727
  return true;
13188
13728
  }
13729
+ function extractPluginEntries(db) {
13730
+ if (db.version === 1) {
13731
+ return Object.entries(db.plugins).map(([key, installation]) => [key, installation]);
13732
+ }
13733
+ return Object.entries(db.plugins).map(([key, installations]) => [key, installations[0]]);
13734
+ }
13189
13735
  function discoverInstalledPlugins(options) {
13190
13736
  const db = loadInstalledPlugins();
13191
13737
  const settings = loadClaudeSettings();
@@ -13196,14 +13742,13 @@ function discoverInstalledPlugins(options) {
13196
13742
  }
13197
13743
  const settingsEnabledPlugins = settings?.enabledPlugins;
13198
13744
  const overrideEnabledPlugins = options?.enabledPluginsOverride;
13199
- for (const [pluginKey, installations] of Object.entries(db.plugins)) {
13200
- if (!installations || installations.length === 0)
13745
+ for (const [pluginKey, installation] of extractPluginEntries(db)) {
13746
+ if (!installation)
13201
13747
  continue;
13202
13748
  if (!isPluginEnabled(pluginKey, settingsEnabledPlugins, overrideEnabledPlugins)) {
13203
13749
  log(`Plugin disabled: ${pluginKey}`);
13204
13750
  continue;
13205
13751
  }
13206
- const installation = installations[0];
13207
13752
  const { installPath, scope, version } = installation;
13208
13753
  if (!existsSync33(installPath)) {
13209
13754
  errors.push({
@@ -13246,7 +13791,7 @@ function discoverInstalledPlugins(options) {
13246
13791
  return { plugins, errors };
13247
13792
  }
13248
13793
  function loadPluginCommands(plugins) {
13249
- const commands = {};
13794
+ const commands2 = {};
13250
13795
  for (const plugin2 of plugins) {
13251
13796
  if (!plugin2.commandsDir || !existsSync33(plugin2.commandsDir))
13252
13797
  continue;
@@ -13268,7 +13813,7 @@ ${body.trim()}
13268
13813
  $ARGUMENTS
13269
13814
  </user-request>`;
13270
13815
  const formattedDescription = `(plugin: ${plugin2.name}) ${data.description || ""}`;
13271
- commands[namespacedName] = {
13816
+ commands2[namespacedName] = {
13272
13817
  name: namespacedName,
13273
13818
  description: formattedDescription,
13274
13819
  template: wrappedTemplate,
@@ -13283,7 +13828,7 @@ $ARGUMENTS
13283
13828
  }
13284
13829
  }
13285
13830
  }
13286
- return commands;
13831
+ return commands2;
13287
13832
  }
13288
13833
  function loadPluginSkillsAsCommands(plugins) {
13289
13834
  const skills = {};
@@ -13431,14 +13976,14 @@ function loadPluginHooksConfigs(plugins) {
13431
13976
  }
13432
13977
  async function loadAllPluginComponents(options) {
13433
13978
  const { plugins, errors } = discoverInstalledPlugins(options);
13434
- const commands = loadPluginCommands(plugins);
13979
+ const commands2 = loadPluginCommands(plugins);
13435
13980
  const skills = loadPluginSkillsAsCommands(plugins);
13436
13981
  const agents = loadPluginAgents(plugins);
13437
13982
  const mcpServers = await loadPluginMcpServers(plugins);
13438
13983
  const hooksConfigs = loadPluginHooksConfigs(plugins);
13439
- 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`);
13440
13985
  return {
13441
- commands,
13986
+ commands: commands2,
13442
13987
  skills,
13443
13988
  agents,
13444
13989
  mcpServers,
@@ -13485,6 +14030,36 @@ var SEVERITY_MAP = {
13485
14030
  var DEFAULT_MAX_REFERENCES = 200;
13486
14031
  var DEFAULT_MAX_SYMBOLS = 200;
13487
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
+ };
13488
14063
  var BUILTIN_SERVERS = {
13489
14064
  typescript: {
13490
14065
  command: ["typescript-language-server", "--stdio"],
@@ -13744,12 +14319,12 @@ var EXT_TO_LANG = {
13744
14319
  // src/tools/lsp/config.ts
13745
14320
  import { existsSync as existsSync34, readFileSync as readFileSync23 } from "fs";
13746
14321
  import { join as join44 } from "path";
13747
- import { homedir as homedir11 } from "os";
13748
- function loadJsonFile(path8) {
13749
- if (!existsSync34(path8))
14322
+ import { homedir as homedir10 } from "os";
14323
+ function loadJsonFile(path7) {
14324
+ if (!existsSync34(path7))
13750
14325
  return null;
13751
14326
  try {
13752
- return JSON.parse(readFileSync23(path8, "utf-8"));
14327
+ return JSON.parse(readFileSync23(path7, "utf-8"));
13753
14328
  } catch {
13754
14329
  return null;
13755
14330
  }
@@ -13758,8 +14333,8 @@ function getConfigPaths2() {
13758
14333
  const cwd = process.cwd();
13759
14334
  return {
13760
14335
  project: join44(cwd, ".opencode", "oh-my-opencode.json"),
13761
- user: join44(homedir11(), ".config", "opencode", "oh-my-opencode.json"),
13762
- 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")
13763
14338
  };
13764
14339
  }
13765
14340
  function loadAllConfigs() {
@@ -13831,16 +14406,38 @@ function findServerForExtension(ext) {
13831
14406
  for (const server of servers) {
13832
14407
  if (server.extensions.includes(ext) && isServerInstalled(server.command)) {
13833
14408
  return {
13834
- id: server.id,
13835
- command: server.command,
13836
- extensions: server.extensions,
13837
- priority: server.priority,
13838
- env: server.env,
13839
- 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
+ }
13840
14418
  };
13841
14419
  }
13842
14420
  }
13843
- 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
+ };
13844
14441
  }
13845
14442
  function getLanguageId(ext) {
13846
14443
  return EXT_TO_LANG[ext] || "plaintext";
@@ -13863,10 +14460,10 @@ function isServerInstalled(command) {
13863
14460
  const additionalPaths = [
13864
14461
  join44(cwd, "node_modules", ".bin", cmd),
13865
14462
  join44(cwd, "node_modules", ".bin", cmd + ext),
13866
- join44(homedir11(), ".config", "opencode", "bin", cmd),
13867
- join44(homedir11(), ".config", "opencode", "bin", cmd + ext),
13868
- join44(homedir11(), ".config", "opencode", "node_modules", ".bin", cmd),
13869
- 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)
13870
14467
  ];
13871
14468
  for (const p of additionalPaths) {
13872
14469
  if (existsSync34(p)) {
@@ -14481,13 +15078,49 @@ function findWorkspaceRoot(filePath) {
14481
15078
  }
14482
15079
  return __require("path").dirname(resolve6(filePath));
14483
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
+ }
14484
15116
  async function withLspClient(filePath, fn) {
14485
15117
  const absPath = resolve6(filePath);
14486
15118
  const ext = extname2(absPath);
14487
- const server = findServerForExtension(ext);
14488
- if (!server) {
14489
- 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));
14490
15122
  }
15123
+ const server = result.server;
14491
15124
  const root = findWorkspaceRoot(absPath);
14492
15125
  const client = await lspManager.getClient(root, server);
14493
15126
  try {
@@ -15478,10 +16111,10 @@ function mergeDefs(...defs) {
15478
16111
  function cloneDef(schema) {
15479
16112
  return mergeDefs(schema._zod.def);
15480
16113
  }
15481
- function getElementAtPath(obj, path8) {
15482
- if (!path8)
16114
+ function getElementAtPath(obj, path7) {
16115
+ if (!path7)
15483
16116
  return obj;
15484
- return path8.reduce((acc, key) => acc?.[key], obj);
16117
+ return path7.reduce((acc, key) => acc?.[key], obj);
15485
16118
  }
15486
16119
  function promiseAllObject(promisesObj) {
15487
16120
  const keys = Object.keys(promisesObj);
@@ -15840,11 +16473,11 @@ function aborted(x, startIndex = 0) {
15840
16473
  }
15841
16474
  return false;
15842
16475
  }
15843
- function prefixIssues(path8, issues) {
16476
+ function prefixIssues(path7, issues) {
15844
16477
  return issues.map((iss) => {
15845
16478
  var _a;
15846
16479
  (_a = iss).path ?? (_a.path = []);
15847
- iss.path.unshift(path8);
16480
+ iss.path.unshift(path7);
15848
16481
  return iss;
15849
16482
  });
15850
16483
  }
@@ -16012,7 +16645,7 @@ function treeifyError(error, _mapper) {
16012
16645
  return issue2.message;
16013
16646
  };
16014
16647
  const result = { errors: [] };
16015
- const processError = (error2, path8 = []) => {
16648
+ const processError = (error2, path7 = []) => {
16016
16649
  var _a, _b;
16017
16650
  for (const issue2 of error2.issues) {
16018
16651
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -16022,7 +16655,7 @@ function treeifyError(error, _mapper) {
16022
16655
  } else if (issue2.code === "invalid_element") {
16023
16656
  processError({ issues: issue2.issues }, issue2.path);
16024
16657
  } else {
16025
- const fullpath = [...path8, ...issue2.path];
16658
+ const fullpath = [...path7, ...issue2.path];
16026
16659
  if (fullpath.length === 0) {
16027
16660
  result.errors.push(mapper(issue2));
16028
16661
  continue;
@@ -16054,8 +16687,8 @@ function treeifyError(error, _mapper) {
16054
16687
  }
16055
16688
  function toDotPath(_path) {
16056
16689
  const segs = [];
16057
- const path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16058
- for (const seg of path8) {
16690
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16691
+ for (const seg of path7) {
16059
16692
  if (typeof seg === "number")
16060
16693
  segs.push(`[${seg}]`);
16061
16694
  else if (typeof seg === "symbol")
@@ -24833,10 +25466,10 @@ function _property(property, schema, params) {
24833
25466
  ...normalizeParams(params)
24834
25467
  });
24835
25468
  }
24836
- function _mime(types10, params) {
25469
+ function _mime(types11, params) {
24837
25470
  return new $ZodCheckMimeType({
24838
25471
  check: "mime_type",
24839
- mime: types10,
25472
+ mime: types11,
24840
25473
  ...normalizeParams(params)
24841
25474
  });
24842
25475
  }
@@ -26746,7 +27379,7 @@ var ZodFile = /* @__PURE__ */ $constructor("ZodFile", (inst, def) => {
26746
27379
  ZodType.init(inst, def);
26747
27380
  inst.min = (size, params) => inst.check(_minSize(size, params));
26748
27381
  inst.max = (size, params) => inst.check(_maxSize(size, params));
26749
- 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));
26750
27383
  });
26751
27384
  function file(params) {
26752
27385
  return _file(ZodFile, params);
@@ -27405,7 +28038,7 @@ import { existsSync as existsSync37, statSync as statSync4 } from "fs";
27405
28038
  var {spawn: spawn6 } = globalThis.Bun;
27406
28039
  import { existsSync as existsSync36, mkdirSync as mkdirSync10, chmodSync as chmodSync2, unlinkSync as unlinkSync9 } from "fs";
27407
28040
  import { join as join45 } from "path";
27408
- import { homedir as homedir12 } from "os";
28041
+ import { homedir as homedir11 } from "os";
27409
28042
  import { createRequire as createRequire3 } from "module";
27410
28043
  var REPO2 = "ast-grep/ast-grep";
27411
28044
  var DEFAULT_VERSION = "0.40.0";
@@ -27430,11 +28063,11 @@ var PLATFORM_MAP2 = {
27430
28063
  function getCacheDir3() {
27431
28064
  if (process.platform === "win32") {
27432
28065
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
27433
- const base2 = localAppData || join45(homedir12(), "AppData", "Local");
28066
+ const base2 = localAppData || join45(homedir11(), "AppData", "Local");
27434
28067
  return join45(base2, "oh-my-opencode", "bin");
27435
28068
  }
27436
- const xdgCache2 = process.env.XDG_CACHE_HOME;
27437
- const base = xdgCache2 || join45(homedir12(), ".cache");
28069
+ const xdgCache = process.env.XDG_CACHE_HOME;
28070
+ const base = xdgCache || join45(homedir11(), ".cache");
27438
28071
  return join45(base, "oh-my-opencode", "bin");
27439
28072
  }
27440
28073
  function getBinaryName3() {
@@ -27472,8 +28105,8 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
27472
28105
  if (existsSync36(binaryPath)) {
27473
28106
  return binaryPath;
27474
28107
  }
27475
- const { arch, os: os6 } = platformInfo;
27476
- const assetName = `app-${arch}-${os6}.zip`;
28108
+ const { arch, os: os5 } = platformInfo;
28109
+ const assetName = `app-${arch}-${os5}.zip`;
27477
28110
  const downloadUrl = `https://github.com/${REPO2}/releases/download/${version2}/${assetName}`;
27478
28111
  console.log(`[oh-my-opencode] Downloading ast-grep binary...`);
27479
28112
  try {
@@ -27562,9 +28195,9 @@ function findSgCliPathSync() {
27562
28195
  }
27563
28196
  if (process.platform === "darwin") {
27564
28197
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
27565
- for (const path8 of homebrewPaths) {
27566
- if (existsSync37(path8) && isValidBinary(path8)) {
27567
- return path8;
28198
+ for (const path7 of homebrewPaths) {
28199
+ if (existsSync37(path7) && isValidBinary(path7)) {
28200
+ return path7;
27568
28201
  }
27569
28202
  }
27570
28203
  }
@@ -27582,10 +28215,9 @@ function getSgCliPath() {
27582
28215
  }
27583
28216
  return "sg";
27584
28217
  }
27585
- function setSgCliPath(path8) {
27586
- resolvedCliPath2 = path8;
28218
+ function setSgCliPath(path7) {
28219
+ resolvedCliPath2 = path7;
27587
28220
  }
27588
- var SG_CLI_PATH = getSgCliPath();
27589
28221
  var CLI_LANGUAGES = [
27590
28222
  "bash",
27591
28223
  "c",
@@ -28385,7 +29017,7 @@ function discoverCommandsFromDir(commandsDir, scope) {
28385
29017
  return [];
28386
29018
  }
28387
29019
  const entries = readdirSync15(commandsDir, { withFileTypes: true });
28388
- const commands = [];
29020
+ const commands2 = [];
28389
29021
  for (const entry of entries) {
28390
29022
  if (!isMarkdownFile(entry))
28391
29023
  continue;
@@ -28403,7 +29035,7 @@ function discoverCommandsFromDir(commandsDir, scope) {
28403
29035
  agent: data.agent,
28404
29036
  subtask: Boolean(data.subtask)
28405
29037
  };
28406
- commands.push({
29038
+ commands2.push({
28407
29039
  name: commandName,
28408
29040
  path: commandPath,
28409
29041
  metadata,
@@ -28414,13 +29046,13 @@ function discoverCommandsFromDir(commandsDir, scope) {
28414
29046
  continue;
28415
29047
  }
28416
29048
  }
28417
- return commands;
29049
+ return commands2;
28418
29050
  }
28419
29051
  function discoverCommandsSync() {
28420
- const { homedir: homedir13 } = __require("os");
29052
+ const { homedir: homedir12 } = __require("os");
28421
29053
  const userCommandsDir = join49(getClaudeConfigDir(), "commands");
28422
29054
  const projectCommandsDir = join49(process.cwd(), ".claude", "commands");
28423
- const opencodeGlobalDir = join49(homedir13(), ".config", "opencode", "command");
29055
+ const opencodeGlobalDir = join49(homedir12(), ".config", "opencode", "command");
28424
29056
  const opencodeProjectDir = join49(process.cwd(), ".opencode", "command");
28425
29057
  const userCommands = discoverCommandsFromDir(userCommandsDir, "user");
28426
29058
  const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode");
@@ -28471,18 +29103,18 @@ async function formatLoadedCommand(cmd) {
28471
29103
  return sections.join(`
28472
29104
  `);
28473
29105
  }
28474
- function formatCommandList(commands) {
28475
- if (commands.length === 0) {
29106
+ function formatCommandList(commands2) {
29107
+ if (commands2.length === 0) {
28476
29108
  return "No commands found.";
28477
29109
  }
28478
29110
  const lines = [`# Available Commands
28479
29111
  `];
28480
- for (const cmd of commands) {
29112
+ for (const cmd of commands2) {
28481
29113
  const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : "";
28482
29114
  lines.push(`- **/${cmd.name}${hint}**: ${cmd.metadata.description || "(no description)"} (${cmd.scope})`);
28483
29115
  }
28484
29116
  lines.push(`
28485
- **Total**: ${commands.length} commands`);
29117
+ **Total**: ${commands2.length} commands`);
28486
29118
  return lines.join(`
28487
29119
  `);
28488
29120
  }
@@ -28519,27 +29151,27 @@ ${commandListForDescription}`,
28519
29151
  command: tool.schema.string().describe("The slash command to execute (without the leading slash). E.g., 'commit', 'plan', 'execute'.")
28520
29152
  },
28521
29153
  async execute(args) {
28522
- const commands = discoverCommandsSync();
29154
+ const commands2 = discoverCommandsSync();
28523
29155
  if (!args.command) {
28524
- return formatCommandList(commands) + `
29156
+ return formatCommandList(commands2) + `
28525
29157
 
28526
29158
  Provide a command name to execute.`;
28527
29159
  }
28528
29160
  const cmdName = args.command.replace(/^\//, "");
28529
- const exactMatch = commands.find((cmd) => cmd.name.toLowerCase() === cmdName.toLowerCase());
29161
+ const exactMatch = commands2.find((cmd) => cmd.name.toLowerCase() === cmdName.toLowerCase());
28530
29162
  if (exactMatch) {
28531
29163
  return await formatLoadedCommand(exactMatch);
28532
29164
  }
28533
- const partialMatches = commands.filter((cmd) => cmd.name.toLowerCase().includes(cmdName.toLowerCase()));
29165
+ const partialMatches = commands2.filter((cmd) => cmd.name.toLowerCase().includes(cmdName.toLowerCase()));
28534
29166
  if (partialMatches.length > 0) {
28535
29167
  const matchList = partialMatches.map((cmd) => `/${cmd.name}`).join(", ");
28536
29168
  return `No exact match for "/${cmdName}". Did you mean: ${matchList}?
28537
29169
 
28538
- ` + formatCommandList(commands);
29170
+ ` + formatCommandList(commands2);
28539
29171
  }
28540
29172
  return `Command "/${cmdName}" not found.
28541
29173
 
28542
- ` + formatCommandList(commands) + `
29174
+ ` + formatCommandList(commands2) + `
28543
29175
 
28544
29176
  Try a different command name.`;
28545
29177
  }
@@ -28547,7 +29179,7 @@ Try a different command name.`;
28547
29179
  // src/tools/session-manager/constants.ts
28548
29180
  import { join as join50 } from "path";
28549
29181
  var OPENCODE_STORAGE9 = getOpenCodeStorageDir();
28550
- var MESSAGE_STORAGE8 = join50(OPENCODE_STORAGE9, "message");
29182
+ var MESSAGE_STORAGE4 = join50(OPENCODE_STORAGE9, "message");
28551
29183
  var PART_STORAGE4 = join50(OPENCODE_STORAGE9, "part");
28552
29184
  var TODO_DIR2 = join50(getClaudeConfigDir(), "todos");
28553
29185
  var TRANSCRIPT_DIR2 = join50(getClaudeConfigDir(), "transcripts");
@@ -28623,22 +29255,24 @@ Has Todos: Yes (12 items, 8 completed)
28623
29255
  Has Transcript: Yes (234 entries)`;
28624
29256
 
28625
29257
  // src/tools/session-manager/storage.ts
28626
- 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";
28627
29260
  import { join as join51 } from "path";
28628
- function getAllSessions() {
28629
- if (!existsSync42(MESSAGE_STORAGE8))
29261
+ async function getAllSessions() {
29262
+ if (!existsSync42(MESSAGE_STORAGE4))
28630
29263
  return [];
28631
29264
  const sessions = [];
28632
- function scanDirectory(dir) {
29265
+ async function scanDirectory(dir) {
28633
29266
  try {
28634
- for (const entry of readdirSync16(dir, { withFileTypes: true })) {
29267
+ const entries = await readdir(dir, { withFileTypes: true });
29268
+ for (const entry of entries) {
28635
29269
  if (entry.isDirectory()) {
28636
29270
  const sessionPath = join51(dir, entry.name);
28637
- const files = readdirSync16(sessionPath);
29271
+ const files = await readdir(sessionPath);
28638
29272
  if (files.some((f) => f.endsWith(".json"))) {
28639
29273
  sessions.push(entry.name);
28640
29274
  } else {
28641
- scanDirectory(sessionPath);
29275
+ await scanDirectory(sessionPath);
28642
29276
  }
28643
29277
  }
28644
29278
  }
@@ -28646,49 +29280,58 @@ function getAllSessions() {
28646
29280
  return;
28647
29281
  }
28648
29282
  }
28649
- scanDirectory(MESSAGE_STORAGE8);
29283
+ await scanDirectory(MESSAGE_STORAGE4);
28650
29284
  return [...new Set(sessions)];
28651
29285
  }
28652
29286
  function getMessageDir9(sessionID) {
28653
- if (!existsSync42(MESSAGE_STORAGE8))
29287
+ if (!existsSync42(MESSAGE_STORAGE4))
28654
29288
  return "";
28655
- const directPath = join51(MESSAGE_STORAGE8, sessionID);
29289
+ const directPath = join51(MESSAGE_STORAGE4, sessionID);
28656
29290
  if (existsSync42(directPath)) {
28657
29291
  return directPath;
28658
29292
  }
28659
- for (const dir of readdirSync16(MESSAGE_STORAGE8)) {
28660
- const sessionPath = join51(MESSAGE_STORAGE8, dir, sessionID);
28661
- if (existsSync42(sessionPath)) {
28662
- 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
+ }
28663
29299
  }
29300
+ } catch {
29301
+ return "";
28664
29302
  }
28665
29303
  return "";
28666
29304
  }
28667
29305
  function sessionExists(sessionID) {
28668
29306
  return getMessageDir9(sessionID) !== "";
28669
29307
  }
28670
- function readSessionMessages(sessionID) {
29308
+ async function readSessionMessages(sessionID) {
28671
29309
  const messageDir = getMessageDir9(sessionID);
28672
29310
  if (!messageDir || !existsSync42(messageDir))
28673
29311
  return [];
28674
29312
  const messages = [];
28675
- for (const file2 of readdirSync16(messageDir)) {
28676
- if (!file2.endsWith(".json"))
28677
- continue;
28678
- try {
28679
- const content = readFileSync27(join51(messageDir, file2), "utf-8");
28680
- const meta = JSON.parse(content);
28681
- const parts = readParts2(meta.id);
28682
- messages.push({
28683
- id: meta.id,
28684
- role: meta.role,
28685
- agent: meta.agent,
28686
- time: meta.time,
28687
- parts
28688
- });
28689
- } catch {
28690
- 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
+ }
28691
29332
  }
29333
+ } catch {
29334
+ return [];
28692
29335
  }
28693
29336
  return messages.sort((a, b) => {
28694
29337
  const aTime = a.time?.created ?? 0;
@@ -28698,61 +29341,71 @@ function readSessionMessages(sessionID) {
28698
29341
  return a.id.localeCompare(b.id);
28699
29342
  });
28700
29343
  }
28701
- function readParts2(messageID) {
29344
+ async function readParts2(messageID) {
28702
29345
  const partDir = join51(PART_STORAGE4, messageID);
28703
29346
  if (!existsSync42(partDir))
28704
29347
  return [];
28705
29348
  const parts = [];
28706
- for (const file2 of readdirSync16(partDir)) {
28707
- if (!file2.endsWith(".json"))
28708
- continue;
28709
- try {
28710
- const content = readFileSync27(join51(partDir, file2), "utf-8");
28711
- parts.push(JSON.parse(content));
28712
- } catch {
28713
- 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
+ }
28714
29360
  }
29361
+ } catch {
29362
+ return [];
28715
29363
  }
28716
29364
  return parts.sort((a, b) => a.id.localeCompare(b.id));
28717
29365
  }
28718
- function readSessionTodos(sessionID) {
29366
+ async function readSessionTodos(sessionID) {
28719
29367
  if (!existsSync42(TODO_DIR2))
28720
29368
  return [];
28721
- const todoFiles = readdirSync16(TODO_DIR2).filter((f) => f.includes(sessionID) && f.endsWith(".json"));
28722
- for (const file2 of todoFiles) {
28723
- try {
28724
- const content = readFileSync27(join51(TODO_DIR2, file2), "utf-8");
28725
- const data = JSON.parse(content);
28726
- if (Array.isArray(data)) {
28727
- return data.map((item) => ({
28728
- id: item.id || "",
28729
- content: item.content || "",
28730
- status: item.status || "pending",
28731
- priority: item.priority
28732
- }));
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;
28733
29386
  }
28734
- } catch {
28735
- continue;
28736
29387
  }
29388
+ } catch {
29389
+ return [];
28737
29390
  }
28738
29391
  return [];
28739
29392
  }
28740
- function readSessionTranscript(sessionID) {
29393
+ async function readSessionTranscript(sessionID) {
28741
29394
  if (!existsSync42(TRANSCRIPT_DIR2))
28742
29395
  return 0;
28743
29396
  const transcriptFile = join51(TRANSCRIPT_DIR2, `${sessionID}.jsonl`);
28744
29397
  if (!existsSync42(transcriptFile))
28745
29398
  return 0;
28746
29399
  try {
28747
- const content = readFileSync27(transcriptFile, "utf-8");
29400
+ const content = await readFile(transcriptFile, "utf-8");
28748
29401
  return content.trim().split(`
28749
29402
  `).filter(Boolean).length;
28750
29403
  } catch {
28751
29404
  return 0;
28752
29405
  }
28753
29406
  }
28754
- function getSessionInfo(sessionID) {
28755
- const messages = readSessionMessages(sessionID);
29407
+ async function getSessionInfo(sessionID) {
29408
+ const messages = await readSessionMessages(sessionID);
28756
29409
  if (messages.length === 0)
28757
29410
  return null;
28758
29411
  const agentsUsed = new Set;
@@ -28769,8 +29422,8 @@ function getSessionInfo(sessionID) {
28769
29422
  lastMessage = date5;
28770
29423
  }
28771
29424
  }
28772
- const todos = readSessionTodos(sessionID);
28773
- const transcriptEntries = readSessionTranscript(sessionID);
29425
+ const todos = await readSessionTodos(sessionID);
29426
+ const transcriptEntries = await readSessionTranscript(sessionID);
28774
29427
  return {
28775
29428
  id: sessionID,
28776
29429
  message_count: messages.length,
@@ -28785,11 +29438,11 @@ function getSessionInfo(sessionID) {
28785
29438
  }
28786
29439
 
28787
29440
  // src/tools/session-manager/utils.ts
28788
- function formatSessionList(sessionIDs) {
29441
+ async function formatSessionList(sessionIDs) {
28789
29442
  if (sessionIDs.length === 0) {
28790
29443
  return "No sessions found.";
28791
29444
  }
28792
- 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);
28793
29446
  if (infos.length === 0) {
28794
29447
  return "No valid sessions found.";
28795
29448
  }
@@ -28881,29 +29534,33 @@ function formatSearchResults(results) {
28881
29534
  return lines.join(`
28882
29535
  `);
28883
29536
  }
28884
- function filterSessionsByDate(sessionIDs, fromDate, toDate) {
29537
+ async function filterSessionsByDate(sessionIDs, fromDate, toDate) {
28885
29538
  if (!fromDate && !toDate)
28886
29539
  return sessionIDs;
28887
29540
  const from = fromDate ? new Date(fromDate) : null;
28888
29541
  const to = toDate ? new Date(toDate) : null;
28889
- return sessionIDs.filter((id) => {
28890
- const info = getSessionInfo(id);
29542
+ const results = [];
29543
+ for (const id of sessionIDs) {
29544
+ const info = await getSessionInfo(id);
28891
29545
  if (!info || !info.last_message)
28892
- return false;
29546
+ continue;
28893
29547
  if (from && info.last_message < from)
28894
- return false;
29548
+ continue;
28895
29549
  if (to && info.last_message > to)
28896
- return false;
28897
- return true;
28898
- });
29550
+ continue;
29551
+ results.push(id);
29552
+ }
29553
+ return results;
28899
29554
  }
28900
- function searchInSession(sessionID, query, caseSensitive = false) {
28901
- const messages = readSessionMessages(sessionID);
29555
+ async function searchInSession(sessionID, query, caseSensitive = false, maxResults) {
29556
+ const messages = await readSessionMessages(sessionID);
28902
29557
  const results = [];
28903
29558
  const searchQuery = caseSensitive ? query : query.toLowerCase();
28904
29559
  for (const msg of messages) {
29560
+ if (maxResults && results.length >= maxResults)
29561
+ break;
28905
29562
  let matchCount = 0;
28906
- let excerpts = [];
29563
+ const excerpts = [];
28907
29564
  for (const part of msg.parts) {
28908
29565
  if (part.type === "text" && part.text) {
28909
29566
  const text = caseSensitive ? part.text : part.text.toLowerCase();
@@ -28939,6 +29596,14 @@ function searchInSession(sessionID, query, caseSensitive = false) {
28939
29596
  }
28940
29597
 
28941
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
+ }
28942
29607
  var session_list = tool({
28943
29608
  description: SESSION_LIST_DESCRIPTION,
28944
29609
  args: {
@@ -28948,14 +29613,14 @@ var session_list = tool({
28948
29613
  },
28949
29614
  execute: async (args, _context) => {
28950
29615
  try {
28951
- let sessions = getAllSessions();
29616
+ let sessions = await getAllSessions();
28952
29617
  if (args.from_date || args.to_date) {
28953
- sessions = filterSessionsByDate(sessions, args.from_date, args.to_date);
29618
+ sessions = await filterSessionsByDate(sessions, args.from_date, args.to_date);
28954
29619
  }
28955
29620
  if (args.limit && args.limit > 0) {
28956
29621
  sessions = sessions.slice(0, args.limit);
28957
29622
  }
28958
- return formatSessionList(sessions);
29623
+ return await formatSessionList(sessions);
28959
29624
  } catch (e) {
28960
29625
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
28961
29626
  }
@@ -28974,11 +29639,11 @@ var session_read = tool({
28974
29639
  if (!sessionExists(args.session_id)) {
28975
29640
  return `Session not found: ${args.session_id}`;
28976
29641
  }
28977
- let messages = readSessionMessages(args.session_id);
29642
+ let messages = await readSessionMessages(args.session_id);
28978
29643
  if (args.limit && args.limit > 0) {
28979
29644
  messages = messages.slice(0, args.limit);
28980
29645
  }
28981
- const todos = args.include_todos ? readSessionTodos(args.session_id) : undefined;
29646
+ const todos = args.include_todos ? await readSessionTodos(args.session_id) : undefined;
28982
29647
  return formatSessionMessages(messages, args.include_todos, todos);
28983
29648
  } catch (e) {
28984
29649
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
@@ -28995,10 +29660,25 @@ var session_search = tool({
28995
29660
  },
28996
29661
  execute: async (args, _context) => {
28997
29662
  try {
28998
- const sessions = args.session_id ? [args.session_id] : getAllSessions();
28999
- const allResults = sessions.flatMap((sid) => searchInSession(sid, args.query, args.case_sensitive));
29000
- const limited = args.limit && args.limit > 0 ? allResults.slice(0, args.limit) : allResults.slice(0, 20);
29001
- 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);
29002
29682
  } catch (e) {
29003
29683
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
29004
29684
  }
@@ -29011,7 +29691,7 @@ var session_info = tool({
29011
29691
  },
29012
29692
  execute: async (args, _context) => {
29013
29693
  try {
29014
- const info = getSessionInfo(args.session_id);
29694
+ const info = await getSessionInfo(args.session_id);
29015
29695
  if (!info) {
29016
29696
  return `Session not found: ${args.session_id}`;
29017
29697
  }
@@ -29054,12 +29734,12 @@ async function findTmuxPath() {
29054
29734
  return null;
29055
29735
  }
29056
29736
  const stdout = await new Response(proc.stdout).text();
29057
- const path8 = stdout.trim().split(`
29737
+ const path7 = stdout.trim().split(`
29058
29738
  `)[0];
29059
- if (!path8) {
29739
+ if (!path7) {
29060
29740
  return null;
29061
29741
  }
29062
- const verifyProc = spawn10([path8, "-V"], {
29742
+ const verifyProc = spawn10([path7, "-V"], {
29063
29743
  stdout: "pipe",
29064
29744
  stderr: "pipe"
29065
29745
  });
@@ -29067,7 +29747,7 @@ async function findTmuxPath() {
29067
29747
  if (verifyExitCode !== 0) {
29068
29748
  return null;
29069
29749
  }
29070
- return path8;
29750
+ return path7;
29071
29751
  } catch {
29072
29752
  return null;
29073
29753
  }
@@ -29080,9 +29760,9 @@ async function getTmuxPath() {
29080
29760
  return initPromise3;
29081
29761
  }
29082
29762
  initPromise3 = (async () => {
29083
- const path8 = await findTmuxPath();
29084
- tmuxPath = path8;
29085
- return path8;
29763
+ const path7 = await findTmuxPath();
29764
+ tmuxPath = path7;
29765
+ return path7;
29086
29766
  })();
29087
29767
  return initPromise3;
29088
29768
  }
@@ -29219,21 +29899,26 @@ function createBackgroundTask(manager) {
29219
29899
  agent: tool.schema.string().describe("Agent type to use (any registered agent)")
29220
29900
  },
29221
29901
  async execute(args, toolContext) {
29902
+ const ctx = toolContext;
29222
29903
  if (!args.agent || args.agent.trim() === "") {
29223
29904
  return `\u274C Agent parameter is required. Please specify which agent to use (e.g., "explore", "librarian", "build", etc.)`;
29224
29905
  }
29225
29906
  try {
29226
- const messageDir = getMessageDir10(toolContext.sessionID);
29907
+ const messageDir = getMessageDir10(ctx.sessionID);
29227
29908
  const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null;
29228
29909
  const parentModel = prevMessage?.model?.providerID && prevMessage?.model?.modelID ? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID } : undefined;
29229
29910
  const task = await manager.launch({
29230
29911
  description: args.description,
29231
29912
  prompt: args.prompt,
29232
29913
  agent: args.agent.trim(),
29233
- parentSessionID: toolContext.sessionID,
29234
- parentMessageID: toolContext.messageID,
29914
+ parentSessionID: ctx.sessionID,
29915
+ parentMessageID: ctx.messageID,
29235
29916
  parentModel
29236
29917
  });
29918
+ ctx.metadata?.({
29919
+ title: args.description,
29920
+ metadata: { sessionId: task.sessionID }
29921
+ });
29237
29922
  return `Background task launched successfully.
29238
29923
 
29239
29924
  Task ID: ${task.id}
@@ -29489,6 +30174,7 @@ function createCallOmoAgent(ctx, backgroundManager) {
29489
30174
  session_id: tool.schema.string().describe("Existing Task session to continue").optional()
29490
30175
  },
29491
30176
  async execute(args, toolContext) {
30177
+ const toolCtx = toolContext;
29492
30178
  log(`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`);
29493
30179
  if (!ALLOWED_AGENTS.includes(args.subagent_type)) {
29494
30180
  return `Error: Invalid agent type "${args.subagent_type}". Only ${ALLOWED_AGENTS.join(", ")} are allowed.`;
@@ -29497,9 +30183,9 @@ function createCallOmoAgent(ctx, backgroundManager) {
29497
30183
  if (args.session_id) {
29498
30184
  return `Error: session_id is not supported in background mode. Use run_in_background=false to continue an existing session.`;
29499
30185
  }
29500
- return await executeBackground(args, toolContext, backgroundManager);
30186
+ return await executeBackground(args, toolCtx, backgroundManager);
29501
30187
  }
29502
- return await executeSync(args, toolContext, ctx);
30188
+ return await executeSync(args, toolCtx, ctx);
29503
30189
  }
29504
30190
  });
29505
30191
  }
@@ -29512,6 +30198,10 @@ async function executeBackground(args, toolContext, manager) {
29512
30198
  parentSessionID: toolContext.sessionID,
29513
30199
  parentMessageID: toolContext.messageID
29514
30200
  });
30201
+ toolContext.metadata?.({
30202
+ title: args.description,
30203
+ metadata: { sessionId: task.sessionID }
30204
+ });
29515
30205
  return `Background agent task launched successfully.
29516
30206
 
29517
30207
  Task ID: ${task.id}
@@ -29556,6 +30246,10 @@ async function executeSync(args, toolContext, ctx) {
29556
30246
  sessionID = createResult.data.id;
29557
30247
  log(`[call_omo_agent] Created session: ${sessionID}`);
29558
30248
  }
30249
+ toolContext.metadata?.({
30250
+ title: args.description,
30251
+ metadata: { sessionId: sessionID }
30252
+ });
29559
30253
  log(`[call_omo_agent] Sending prompt to session ${sessionID}`);
29560
30254
  log(`[call_omo_agent] Prompt text:`, args.prompt.substring(0, 100));
29561
30255
  try {
@@ -30001,6 +30695,7 @@ class BackgroundManager {
30001
30695
  }
30002
30696
  const message = `[BACKGROUND TASK COMPLETED] Task "${task.description}" finished in ${duration3}. Use background_output with task_id="${task.id}" to get results.`;
30003
30697
  log("[background-agent] Sending notification to parent session:", { parentSessionID: task.parentSessionID });
30698
+ const taskId = task.id;
30004
30699
  setTimeout(async () => {
30005
30700
  try {
30006
30701
  const messageDir = getMessageDir11(task.parentSessionID);
@@ -30016,10 +30711,13 @@ class BackgroundManager {
30016
30711
  },
30017
30712
  query: { directory: this.directory }
30018
30713
  });
30019
- this.clearNotificationsForTask(task.id);
30714
+ this.clearNotificationsForTask(taskId);
30020
30715
  log("[background-agent] Successfully sent prompt to parent session:", { parentSessionID: task.parentSessionID });
30021
30716
  } catch (error45) {
30022
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);
30023
30721
  }
30024
30722
  }, 200);
30025
30723
  }
@@ -30207,6 +30905,9 @@ var HookNameSchema = exports_external.enum([
30207
30905
  "empty-message-sanitizer",
30208
30906
  "thinking-block-validator"
30209
30907
  ]);
30908
+ var BuiltinCommandNameSchema = exports_external.enum([
30909
+ "init-deep"
30910
+ ]);
30210
30911
  var AgentOverrideConfigSchema = exports_external.object({
30211
30912
  model: exports_external.string().optional(),
30212
30913
  temperature: exports_external.number().min(0).max(2).optional(),
@@ -30248,6 +30949,9 @@ var SisyphusAgentConfigSchema = exports_external.object({
30248
30949
  planner_enabled: exports_external.boolean().optional(),
30249
30950
  replace_plan: exports_external.boolean().optional()
30250
30951
  });
30952
+ var CommentCheckerConfigSchema = exports_external.object({
30953
+ custom_prompt: exports_external.string().optional()
30954
+ });
30251
30955
  var DynamicContextPruningConfigSchema = exports_external.object({
30252
30956
  enabled: exports_external.boolean().default(false),
30253
30957
  notification: exports_external.enum(["off", "minimal", "detailed"]).default("detailed"),
@@ -30286,17 +30990,19 @@ var ExperimentalConfigSchema = exports_external.object({
30286
30990
  preemptive_compaction_threshold: exports_external.number().min(0.5).max(0.95).optional(),
30287
30991
  truncate_all_tool_outputs: exports_external.boolean().default(true),
30288
30992
  dynamic_context_pruning: DynamicContextPruningConfigSchema.optional(),
30289
- dcp_on_compaction_failure: exports_external.boolean().optional()
30993
+ dcp_for_compaction: exports_external.boolean().optional()
30290
30994
  });
30291
30995
  var OhMyOpenCodeConfigSchema = exports_external.object({
30292
30996
  $schema: exports_external.string().optional(),
30293
30997
  disabled_mcps: exports_external.array(McpNameSchema).optional(),
30294
30998
  disabled_agents: exports_external.array(BuiltinAgentNameSchema).optional(),
30295
30999
  disabled_hooks: exports_external.array(HookNameSchema).optional(),
31000
+ disabled_commands: exports_external.array(BuiltinCommandNameSchema).optional(),
30296
31001
  agents: AgentOverridesSchema.optional(),
30297
31002
  claude_code: ClaudeCodeConfigSchema.optional(),
30298
31003
  google_auth: exports_external.boolean().optional(),
30299
31004
  sisyphus_agent: SisyphusAgentConfigSchema.optional(),
31005
+ comment_checker: CommentCheckerConfigSchema.optional(),
30300
31006
  experimental: ExperimentalConfigSchema.optional(),
30301
31007
  auto_update: exports_external.boolean().optional()
30302
31008
  });
@@ -30373,7 +31079,7 @@ var PLAN_PERMISSION = {
30373
31079
 
30374
31080
  // src/index.ts
30375
31081
  import * as fs7 from "fs";
30376
- import * as path8 from "path";
31082
+ import * as path7 from "path";
30377
31083
  var AGENT_NAME_MAP = {
30378
31084
  omo: "Sisyphus",
30379
31085
  OmO: "Sisyphus",
@@ -30472,14 +31178,20 @@ function mergeConfigs(base, override) {
30472
31178
  ...override.disabled_hooks ?? []
30473
31179
  ])
30474
31180
  ],
31181
+ disabled_commands: [
31182
+ ...new Set([
31183
+ ...base.disabled_commands ?? [],
31184
+ ...override.disabled_commands ?? []
31185
+ ])
31186
+ ],
30475
31187
  claude_code: deepMerge(base.claude_code, override.claude_code)
30476
31188
  };
30477
31189
  }
30478
31190
  function loadPluginConfig(directory, ctx) {
30479
- const userBasePath = path8.join(getUserConfigDir(), "opencode", "oh-my-opencode");
31191
+ const userBasePath = path7.join(getUserConfigDir(), "opencode", "oh-my-opencode");
30480
31192
  const userDetected = detectConfigFile(userBasePath);
30481
31193
  const userConfigPath = userDetected.format !== "none" ? userDetected.path : userBasePath + ".json";
30482
- const projectBasePath = path8.join(directory, ".opencode", "oh-my-opencode");
31194
+ const projectBasePath = path7.join(directory, ".opencode", "oh-my-opencode");
30483
31195
  const projectDetected = detectConfigFile(projectBasePath);
30484
31196
  const projectConfigPath = projectDetected.format !== "none" ? projectDetected.path : projectBasePath + ".json";
30485
31197
  let config3 = loadConfigFromPath2(userConfigPath, ctx) ?? {};
@@ -30515,7 +31227,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
30515
31227
  const contextWindowMonitor = isHookEnabled("context-window-monitor") ? createContextWindowMonitorHook(ctx) : null;
30516
31228
  const sessionRecovery = isHookEnabled("session-recovery") ? createSessionRecoveryHook(ctx, { experimental: pluginConfig.experimental }) : null;
30517
31229
  const sessionNotification = isHookEnabled("session-notification") ? createSessionNotification(ctx) : null;
30518
- const commentChecker = isHookEnabled("comment-checker") ? createCommentCheckerHooks() : null;
31230
+ const commentChecker = isHookEnabled("comment-checker") ? createCommentCheckerHooks(pluginConfig.comment_checker) : null;
30519
31231
  const toolOutputTruncator = isHookEnabled("tool-output-truncator") ? createToolOutputTruncatorHook(ctx, { experimental: pluginConfig.experimental }) : null;
30520
31232
  const directoryAgentsInjector = isHookEnabled("directory-agents-injector") ? createDirectoryAgentsInjectorHook(ctx) : null;
30521
31233
  const directoryReadmeInjector = isHookEnabled("directory-readme-injector") ? createDirectoryReadmeInjectorHook(ctx) : null;
@@ -30695,12 +31407,14 @@ var OhMyOpenCodePlugin = async (ctx) => {
30695
31407
  ...mcpResult.servers,
30696
31408
  ...pluginComponents.mcpServers
30697
31409
  };
31410
+ const builtinCommands = loadBuiltinCommands(pluginConfig.disabled_commands);
30698
31411
  const userCommands = pluginConfig.claude_code?.commands ?? true ? loadUserCommands() : {};
30699
31412
  const opencodeGlobalCommands = loadOpencodeGlobalCommands();
30700
31413
  const systemCommands = config3.command ?? {};
30701
31414
  const projectCommands = pluginConfig.claude_code?.commands ?? true ? loadProjectCommands() : {};
30702
31415
  const opencodeProjectCommands = loadOpencodeProjectCommands();
30703
31416
  config3.command = {
31417
+ ...builtinCommands,
30704
31418
  ...userCommands,
30705
31419
  ...opencodeGlobalCommands,
30706
31420
  ...systemCommands,