oh-my-opencode-slim 1.0.6 → 1.1.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.
Files changed (56) hide show
  1. package/README.md +30 -17
  2. package/dist/cli/config-io.d.ts +1 -0
  3. package/dist/cli/divoom.d.ts +23 -0
  4. package/dist/cli/doctor.d.ts +38 -0
  5. package/dist/cli/index.js +469 -58
  6. package/dist/cli/providers.d.ts +3 -0
  7. package/dist/config/council-schema.d.ts +2 -2
  8. package/dist/config/index.d.ts +1 -1
  9. package/dist/config/loader.d.ts +46 -1
  10. package/dist/config/schema.d.ts +23 -0
  11. package/dist/divoom/council.gif +0 -0
  12. package/dist/divoom/designer.gif +0 -0
  13. package/dist/divoom/explorer.gif +0 -0
  14. package/dist/divoom/fixer.gif +0 -0
  15. package/dist/divoom/input.gif +0 -0
  16. package/dist/divoom/intro.gif +0 -0
  17. package/dist/divoom/librarian.gif +0 -0
  18. package/dist/divoom/manager.d.ts +57 -0
  19. package/dist/divoom/oracle.gif +0 -0
  20. package/dist/divoom/orchestrator.gif +0 -0
  21. package/dist/index.js +1304 -291
  22. package/dist/integrations/divoom/index.d.ts +3 -0
  23. package/dist/integrations/divoom/status-manager.d.ts +31 -0
  24. package/dist/integrations/divoom/swift-helper-source.d.ts +1 -0
  25. package/dist/integrations/divoom/swift-transport.d.ts +26 -0
  26. package/dist/integrations/divoom/types.d.ts +41 -0
  27. package/dist/multiplexer/tmux/index.d.ts +5 -0
  28. package/dist/tools/council.d.ts +2 -2
  29. package/dist/tools/fork/command.d.ts +28 -0
  30. package/dist/tools/fork/files.d.ts +33 -0
  31. package/dist/tools/fork/index.d.ts +10 -0
  32. package/dist/tools/fork/state.d.ts +7 -0
  33. package/dist/tools/fork/tools.d.ts +23 -0
  34. package/dist/tools/fork/vendor.d.ts +28 -0
  35. package/dist/tools/handoff/command.d.ts +29 -0
  36. package/dist/tools/handoff/files.d.ts +33 -0
  37. package/dist/tools/handoff/index.d.ts +10 -0
  38. package/dist/tools/handoff/state.d.ts +7 -0
  39. package/dist/tools/handoff/tools.d.ts +23 -0
  40. package/dist/tools/handoff/vendor.d.ts +28 -0
  41. package/dist/tools/index.d.ts +2 -0
  42. package/dist/tools/subtask/command.d.ts +30 -0
  43. package/dist/tools/subtask/files.d.ts +34 -0
  44. package/dist/tools/subtask/index.d.ts +11 -0
  45. package/dist/tools/subtask/state.d.ts +7 -0
  46. package/dist/tools/subtask/tools.d.ts +23 -0
  47. package/dist/tools/subtask/vendor.d.ts +27 -0
  48. package/dist/tui.d.ts +1 -0
  49. package/dist/tui.js +679 -11
  50. package/dist/utils/session.d.ts +11 -4
  51. package/oh-my-opencode-slim.schema.json +59 -0
  52. package/package.json +3 -2
  53. package/src/skills/clonedeps/README.md +23 -0
  54. package/src/skills/clonedeps/SKILL.md +237 -0
  55. package/src/skills/clonedeps/codemap.md +41 -0
  56. package/src/skills/codemap.md +8 -5
package/dist/index.js CHANGED
@@ -6246,33 +6246,33 @@ var require_URL = __commonJS((exports, module) => {
6246
6246
  else
6247
6247
  return basepath.substring(0, lastslash + 1) + refpath;
6248
6248
  }
6249
- function remove_dot_segments(path15) {
6250
- if (!path15)
6251
- return path15;
6249
+ function remove_dot_segments(path16) {
6250
+ if (!path16)
6251
+ return path16;
6252
6252
  var output = "";
6253
- while (path15.length > 0) {
6254
- if (path15 === "." || path15 === "..") {
6255
- path15 = "";
6253
+ while (path16.length > 0) {
6254
+ if (path16 === "." || path16 === "..") {
6255
+ path16 = "";
6256
6256
  break;
6257
6257
  }
6258
- var twochars = path15.substring(0, 2);
6259
- var threechars = path15.substring(0, 3);
6260
- var fourchars = path15.substring(0, 4);
6258
+ var twochars = path16.substring(0, 2);
6259
+ var threechars = path16.substring(0, 3);
6260
+ var fourchars = path16.substring(0, 4);
6261
6261
  if (threechars === "../") {
6262
- path15 = path15.substring(3);
6262
+ path16 = path16.substring(3);
6263
6263
  } else if (twochars === "./") {
6264
- path15 = path15.substring(2);
6264
+ path16 = path16.substring(2);
6265
6265
  } else if (threechars === "/./") {
6266
- path15 = "/" + path15.substring(3);
6267
- } else if (twochars === "/." && path15.length === 2) {
6268
- path15 = "/";
6269
- } else if (fourchars === "/../" || threechars === "/.." && path15.length === 3) {
6270
- path15 = "/" + path15.substring(4);
6266
+ path16 = "/" + path16.substring(3);
6267
+ } else if (twochars === "/." && path16.length === 2) {
6268
+ path16 = "/";
6269
+ } else if (fourchars === "/../" || threechars === "/.." && path16.length === 3) {
6270
+ path16 = "/" + path16.substring(4);
6271
6271
  output = output.replace(/\/?[^\/]*$/, "");
6272
6272
  } else {
6273
- var segment = path15.match(/(\/?([^\/]*))/)[0];
6273
+ var segment = path16.match(/(\/?([^\/]*))/)[0];
6274
6274
  output += segment;
6275
- path15 = path15.substring(segment.length);
6275
+ path16 = path16.substring(segment.length);
6276
6276
  }
6277
6277
  }
6278
6278
  return output;
@@ -18220,6 +18220,12 @@ var CUSTOM_SKILLS = [
18220
18220
  description: "Repository understanding and hierarchical codemap generation",
18221
18221
  allowedAgents: ["orchestrator"],
18222
18222
  sourcePath: "src/skills/codemap"
18223
+ },
18224
+ {
18225
+ name: "clonedeps",
18226
+ description: "Clone important dependency source for local inspection",
18227
+ allowedAgents: ["orchestrator"],
18228
+ sourcePath: "src/skills/clonedeps"
18223
18229
  }
18224
18230
  ];
18225
18231
 
@@ -18393,6 +18399,56 @@ var CouncilConfigSchema = z.object({
18393
18399
  import * as fs from "node:fs";
18394
18400
  import * as path from "node:path";
18395
18401
 
18402
+ // src/utils/compat.ts
18403
+ import { spawn as nodeSpawn } from "node:child_process";
18404
+ import { writeFile as fsWriteFile } from "node:fs/promises";
18405
+ var isBun = typeof globalThis.Bun !== "undefined";
18406
+ function collectStream(stream) {
18407
+ if (!stream)
18408
+ return () => Promise.resolve("");
18409
+ const chunks = [];
18410
+ stream.on("data", (chunk) => chunks.push(chunk));
18411
+ return () => new Promise((resolve, reject) => {
18412
+ if (!stream.readable) {
18413
+ resolve(Buffer.concat(chunks).toString("utf-8"));
18414
+ return;
18415
+ }
18416
+ stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
18417
+ stream.on("error", reject);
18418
+ });
18419
+ }
18420
+ function crossSpawn(command, options) {
18421
+ const [cmd, ...args] = command;
18422
+ const proc = nodeSpawn(cmd, args, {
18423
+ stdio: [
18424
+ options?.stdin ?? "ignore",
18425
+ options?.stdout ?? "pipe",
18426
+ options?.stderr ?? "pipe"
18427
+ ],
18428
+ cwd: options?.cwd,
18429
+ env: options?.env
18430
+ });
18431
+ const stdoutCollector = collectStream(proc.stdout);
18432
+ const stderrCollector = collectStream(proc.stderr);
18433
+ const exited = new Promise((resolve, reject) => {
18434
+ proc.on("error", reject);
18435
+ proc.on("close", (code) => resolve(code ?? 1));
18436
+ });
18437
+ return {
18438
+ proc,
18439
+ stdout: stdoutCollector,
18440
+ stderr: stderrCollector,
18441
+ exited,
18442
+ kill: (signal) => proc.kill(signal),
18443
+ get exitCode() {
18444
+ return proc.exitCode;
18445
+ }
18446
+ };
18447
+ }
18448
+ async function crossWrite(path, data) {
18449
+ await fsWriteFile(path, Buffer.from(data));
18450
+ }
18451
+
18396
18452
  // src/config/agent-mcps.ts
18397
18453
  var DEFAULT_AGENT_MCPS = {
18398
18454
  orchestrator: ["*", "!context7"],
@@ -18530,6 +18586,17 @@ var SessionManagerConfigSchema = z2.object({
18530
18586
  readContextMinLines: z2.number().int().min(0).max(1000).default(10),
18531
18587
  readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
18532
18588
  });
18589
+ var DivoomConfigSchema = z2.object({
18590
+ enabled: z2.boolean().default(false),
18591
+ python: z2.string().min(1).default("/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python"),
18592
+ script: z2.string().min(1).default("/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py"),
18593
+ size: z2.number().int().min(1).max(1024).default(128),
18594
+ fps: z2.number().int().min(1).max(60).default(8),
18595
+ speed: z2.number().int().min(1).max(1e4).default(125),
18596
+ maxFrames: z2.number().int().min(1).max(500).default(24),
18597
+ posterizeBits: z2.number().int().min(1).max(8).default(3),
18598
+ gifs: z2.record(z2.string(), z2.string().min(1)).optional()
18599
+ });
18533
18600
  var TodoContinuationConfigSchema = z2.object({
18534
18601
  maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
18535
18602
  cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
@@ -18581,6 +18648,7 @@ var PluginConfigSchema = z2.object({
18581
18648
  websearch: WebsearchConfigSchema.optional(),
18582
18649
  interview: InterviewConfigSchema.optional(),
18583
18650
  sessionManager: SessionManagerConfigSchema.optional(),
18651
+ divoom: DivoomConfigSchema.optional(),
18584
18652
  todoContinuation: TodoContinuationConfigSchema.optional(),
18585
18653
  fallback: FailoverConfigSchema.optional(),
18586
18654
  council: CouncilConfigSchema.optional()
@@ -18597,20 +18665,49 @@ var PluginConfigSchema = z2.object({
18597
18665
 
18598
18666
  // src/config/loader.ts
18599
18667
  var PROMPTS_DIR_NAME = "oh-my-opencode-slim";
18600
- function loadConfigFromPath(configPath) {
18668
+ function loadConfigFromPath(configPath, options) {
18601
18669
  try {
18602
18670
  const content = fs.readFileSync(configPath, "utf-8");
18603
- const rawConfig = JSON.parse(stripJsonComments(content));
18671
+ let rawConfig;
18672
+ try {
18673
+ rawConfig = JSON.parse(stripJsonComments(content));
18674
+ } catch (error) {
18675
+ const message = error instanceof Error ? error.message : String(error);
18676
+ options?.onWarning?.({
18677
+ path: configPath,
18678
+ kind: "invalid-json",
18679
+ message
18680
+ });
18681
+ if (!options?.silent) {
18682
+ console.warn(`[oh-my-opencode-slim] Invalid JSON in ${configPath}:`, message);
18683
+ }
18684
+ return null;
18685
+ }
18604
18686
  const result = PluginConfigSchema.safeParse(rawConfig);
18605
18687
  if (!result.success) {
18606
- console.warn(`[oh-my-opencode-slim] Invalid config at ${configPath}:`);
18607
- console.warn(result.error.format());
18688
+ options?.onWarning?.({
18689
+ path: configPath,
18690
+ kind: "invalid-schema",
18691
+ message: "Config does not match schema",
18692
+ formatted: result.error.format()
18693
+ });
18694
+ if (!options?.silent) {
18695
+ console.warn(`[oh-my-opencode-slim] Invalid config at ${configPath}:`);
18696
+ console.warn(result.error.format());
18697
+ }
18608
18698
  return null;
18609
18699
  }
18610
18700
  return result.data;
18611
18701
  } catch (error) {
18612
18702
  if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
18613
- console.warn(`[oh-my-opencode-slim] Error reading config from ${configPath}:`, error.message);
18703
+ options?.onWarning?.({
18704
+ path: configPath,
18705
+ kind: "read-error",
18706
+ message: error.message
18707
+ });
18708
+ if (!options?.silent) {
18709
+ console.warn(`[oh-my-opencode-slim] Error reading config from ${configPath}:`, error.message);
18710
+ }
18614
18711
  }
18615
18712
  return null;
18616
18713
  }
@@ -18635,6 +18732,26 @@ function findConfigPathInDirs(configDirs, baseName) {
18635
18732
  }
18636
18733
  return null;
18637
18734
  }
18735
+ function findPluginConfigPaths(directory) {
18736
+ const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
18737
+ const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
18738
+ const projectConfigPath = findConfigPath(projectConfigBasePath);
18739
+ return { userConfigPath, projectConfigPath };
18740
+ }
18741
+ function mergePluginConfigs(base, override) {
18742
+ return {
18743
+ ...base,
18744
+ ...override,
18745
+ agents: deepMerge(base.agents, override.agents),
18746
+ tmux: deepMerge(base.tmux, override.tmux),
18747
+ multiplexer: deepMerge(base.multiplexer, override.multiplexer),
18748
+ interview: deepMerge(base.interview, override.interview),
18749
+ sessionManager: deepMerge(base.sessionManager, override.sessionManager),
18750
+ divoom: deepMerge(base.divoom, override.divoom),
18751
+ fallback: deepMerge(base.fallback, override.fallback),
18752
+ council: deepMerge(base.council, override.council)
18753
+ };
18754
+ }
18638
18755
  function deepMerge(base, override) {
18639
18756
  if (!base)
18640
18757
  return override;
@@ -18652,24 +18769,12 @@ function deepMerge(base, override) {
18652
18769
  }
18653
18770
  return result;
18654
18771
  }
18655
- function loadPluginConfig(directory) {
18656
- const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
18657
- const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
18658
- const projectConfigPath = findConfigPath(projectConfigBasePath);
18659
- let config = userConfigPath ? loadConfigFromPath(userConfigPath) ?? {} : {};
18660
- const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath) : null;
18772
+ function loadPluginConfig(directory, options) {
18773
+ const { userConfigPath, projectConfigPath } = findPluginConfigPaths(directory);
18774
+ let config = userConfigPath ? loadConfigFromPath(userConfigPath, options) ?? {} : {};
18775
+ const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath, options) : null;
18661
18776
  if (projectConfig) {
18662
- config = {
18663
- ...config,
18664
- ...projectConfig,
18665
- agents: deepMerge(config.agents, projectConfig.agents),
18666
- tmux: deepMerge(config.tmux, projectConfig.tmux),
18667
- multiplexer: deepMerge(config.multiplexer, projectConfig.multiplexer),
18668
- interview: deepMerge(config.interview, projectConfig.interview),
18669
- sessionManager: deepMerge(config.sessionManager, projectConfig.sessionManager),
18670
- fallback: deepMerge(config.fallback, projectConfig.fallback),
18671
- council: deepMerge(config.council, projectConfig.council)
18672
- };
18777
+ config = mergePluginConfigs(config, projectConfig);
18673
18778
  }
18674
18779
  config = migrateTmuxToMultiplexer(config);
18675
18780
  const envPreset = process.env.OH_MY_OPENCODE_SLIM_PRESET;
@@ -18683,7 +18788,15 @@ function loadPluginConfig(directory) {
18683
18788
  } else {
18684
18789
  const presetSource = envPreset === config.preset ? "environment variable" : "config file";
18685
18790
  const availablePresets = config.presets ? Object.keys(config.presets).join(", ") : "none";
18686
- console.warn(`[oh-my-opencode-slim] Preset "${config.preset}" not found (from ${presetSource}). Available presets: ${availablePresets}`);
18791
+ const message = `Preset "${config.preset}" not found (from ${presetSource}). Available presets: ${availablePresets}`;
18792
+ options?.onWarning?.({
18793
+ path: projectConfigPath ?? userConfigPath ?? "",
18794
+ kind: "missing-preset",
18795
+ message
18796
+ });
18797
+ if (!options?.silent) {
18798
+ console.warn(`[oh-my-opencode-slim] ${message}`);
18799
+ }
18687
18800
  }
18688
18801
  }
18689
18802
  return config;
@@ -18744,6 +18857,34 @@ function getCustomAgentNames(config) {
18744
18857
  });
18745
18858
  }
18746
18859
  // src/utils/session.ts
18860
+ var SESSION_ABORT_TIMEOUT_MS = 1000;
18861
+
18862
+ class OperationTimeoutError extends Error {
18863
+ constructor(message) {
18864
+ super(message);
18865
+ this.name = "OperationTimeoutError";
18866
+ }
18867
+ }
18868
+ async function withTimeout(operation, timeoutMs, message) {
18869
+ if (timeoutMs <= 0)
18870
+ return operation;
18871
+ let timer;
18872
+ try {
18873
+ return await Promise.race([
18874
+ operation,
18875
+ new Promise((_, reject) => {
18876
+ timer = setTimeout(() => {
18877
+ reject(new OperationTimeoutError(message));
18878
+ }, timeoutMs);
18879
+ })
18880
+ ]);
18881
+ } finally {
18882
+ clearTimeout(timer);
18883
+ }
18884
+ }
18885
+ async function abortSessionWithTimeout(client, sessionId, timeoutMs = SESSION_ABORT_TIMEOUT_MS) {
18886
+ await withTimeout(client.session.abort({ path: { id: sessionId } }), timeoutMs, `Session abort timed out after ${timeoutMs}ms`);
18887
+ }
18747
18888
  function shortModelLabel(model) {
18748
18889
  return model.split("/").pop() ?? model;
18749
18890
  }
@@ -18757,13 +18898,16 @@ function parseModelReference(model) {
18757
18898
  modelID: model.slice(slashIndex + 1)
18758
18899
  };
18759
18900
  }
18760
- async function promptWithTimeout(client, args, timeoutMs) {
18901
+ async function promptWithTimeout(client, args, timeoutMs, signal) {
18902
+ if (signal?.aborted)
18903
+ throw new Error("Prompt cancelled");
18761
18904
  if (timeoutMs <= 0) {
18762
18905
  await client.session.prompt(args);
18763
18906
  return;
18764
18907
  }
18765
18908
  const sessionId = args.path.id;
18766
18909
  let timer;
18910
+ let onAbort;
18767
18911
  try {
18768
18912
  const promptPromise = client.session.prompt(args);
18769
18913
  promptPromise.catch(() => {});
@@ -18771,19 +18915,38 @@ async function promptWithTimeout(client, args, timeoutMs) {
18771
18915
  promptPromise,
18772
18916
  new Promise((_, reject) => {
18773
18917
  timer = setTimeout(() => {
18774
- client.session.abort({ path: { id: sessionId } }).catch(() => {});
18775
- reject(new Error(`Prompt timed out after ${timeoutMs}ms`));
18918
+ reject(new OperationTimeoutError(`Prompt timed out after ${timeoutMs}ms`));
18776
18919
  }, timeoutMs);
18920
+ }),
18921
+ new Promise((_, reject) => {
18922
+ if (!signal)
18923
+ return;
18924
+ if (signal.aborted) {
18925
+ reject(new Error("Prompt cancelled"));
18926
+ return;
18927
+ }
18928
+ onAbort = () => reject(new Error("Prompt cancelled"));
18929
+ signal.addEventListener("abort", onAbort, { once: true });
18777
18930
  })
18778
18931
  ]);
18932
+ } catch (error) {
18933
+ if (error instanceof OperationTimeoutError) {
18934
+ try {
18935
+ await abortSessionWithTimeout(client, sessionId);
18936
+ } catch {}
18937
+ }
18938
+ throw error;
18779
18939
  } finally {
18780
18940
  clearTimeout(timer);
18941
+ if (onAbort)
18942
+ signal?.removeEventListener("abort", onAbort);
18781
18943
  }
18782
18944
  }
18783
18945
  async function extractSessionResult(client, sessionId, options) {
18784
18946
  const includeReasoning = options?.includeReasoning ?? true;
18785
18947
  const messagesResult = await client.session.messages({
18786
- path: { id: sessionId }
18948
+ path: { id: sessionId },
18949
+ ...options?.directory ? { query: { directory: options.directory } } : {}
18787
18950
  });
18788
18951
  const messages = messagesResult.data ?? [];
18789
18952
  const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
@@ -18822,7 +18985,7 @@ var AGENT_DESCRIPTIONS = {
18822
18985
  - **Don't delegate when:** Know the path and need actual content • Need full file anyway • Single specific lookup • About to edit the file`,
18823
18986
  librarian: `@librarian
18824
18987
  - Role: Authoritative source for current library docs and API references
18825
- - Permissions: None
18988
+ - Permissions: External docs/search MCPs; no file edits
18826
18989
  - Stats: 10x better finding up-to-date library docs than orchestrator, 1/2 cost of orchestrator
18827
18990
  - Capabilities: Fetches latest official docs, examples, API signatures, version-specific behavior via grep_app MCP
18828
18991
  - **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) • Complex APIs needing official examples (ORMs, auth) • Version-specific behavior matters • Unfamiliar library • Edge cases or advanced features • Nuanced best practices
@@ -18939,6 +19102,25 @@ ${enabledParallelExamples}
18939
19102
 
18940
19103
  Balance: respect dependencies, avoid parallelizing what must be sequential.
18941
19104
 
19105
+ ### Context Isolation
19106
+ If no specialist delegation is needed, consider \`subtask\` before doing
19107
+ context-heavy work directly.
19108
+
19109
+ Ask whether the parent context needs the details or only the result. Use
19110
+ \`subtask\` when the work is bounded, context-heavy, and the parent only needs a
19111
+ compact outcome.
19112
+
19113
+ Use \`subtask\` for focused investigation, bounded analysis, cleanup, or
19114
+ verification across files/logs/messages.
19115
+
19116
+ Do not use \`subtask\` for tiny tasks, open-ended work, interactive decisions,
19117
+ work better handled by a named specialist, or cases where the parent must reason
19118
+ over the details.
19119
+
19120
+ When calling \`subtask\`, give a self-contained prompt with objective,
19121
+ constraints, relevant context, deliverable, and validation. Pass only clearly
19122
+ relevant files. Wait for the summary, then integrate and verify it.
19123
+
18942
19124
  ### OpenCode subagent execution model
18943
19125
  - A delegated specialist runs in a separate child session.
18944
19126
  - Delegation is blocking for the parent at that point: send work out, then continue that line after results return.
@@ -19207,7 +19389,8 @@ function createCouncillorAgent(model, customPrompt, customAppendPrompt) {
19207
19389
  grep: "allow",
19208
19390
  lsp: "allow",
19209
19391
  list: "allow",
19210
- codesearch: "allow"
19392
+ codesearch: "allow",
19393
+ ast_grep_search: "allow"
19211
19394
  }
19212
19395
  }
19213
19396
  };
@@ -19529,10 +19712,14 @@ ${customAppendPrompt}`;
19529
19712
 
19530
19713
  // src/agents/index.ts
19531
19714
  var COUNCIL_TOOL_ALLOWED_AGENTS = new Set(["council"]);
19715
+ var SAFE_AGENT_ALIAS_RE = /^[a-z][a-z0-9_-]*$/i;
19532
19716
  function normalizeDisplayName(displayName) {
19533
19717
  const trimmed = displayName.trim();
19534
19718
  return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
19535
19719
  }
19720
+ function isSafeDisplayName(displayName) {
19721
+ return SAFE_AGENT_ALIAS_RE.test(displayName);
19722
+ }
19536
19723
  function escapeRegExp(value) {
19537
19724
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19538
19725
  }
@@ -19566,7 +19753,7 @@ function normalizeCustomAgentName(name) {
19566
19753
  return name.trim();
19567
19754
  }
19568
19755
  function isSafeCustomAgentName(name) {
19569
- return /^[a-z][a-z0-9_-]*$/i.test(name) && !isKnownAgentName(name);
19756
+ return SAFE_AGENT_ALIAS_RE.test(name) && !isKnownAgentName(name);
19570
19757
  }
19571
19758
  function hasCustomAgentModel(override) {
19572
19759
  if (!override?.model) {
@@ -19626,6 +19813,9 @@ var SUBAGENT_FACTORIES = {
19626
19813
  };
19627
19814
  function createAgents(config) {
19628
19815
  const disabled = getDisabledAgents(config);
19816
+ if (!config?.council) {
19817
+ disabled.add("council");
19818
+ }
19629
19819
  const getModelForAgent = (name) => {
19630
19820
  if (name === "fixer" && !getAgentOverride(config, "fixer")?.model) {
19631
19821
  const librarianOverride = getAgentOverride(config, "librarian")?.model;
@@ -19712,6 +19902,9 @@ function createAgents(config) {
19712
19902
  const usedDisplayNames = new Set;
19713
19903
  for (const [, displayName] of displayNameMap) {
19714
19904
  const normalizedDisplayName = normalizeDisplayName(displayName);
19905
+ if (!isSafeDisplayName(normalizedDisplayName)) {
19906
+ throw new Error(`displayName '${normalizedDisplayName}' must match /^[a-z][a-z0-9_-]*$/i`);
19907
+ }
19715
19908
  if (usedDisplayNames.has(normalizedDisplayName)) {
19716
19909
  throw new Error(`Duplicate displayName '${normalizedDisplayName}' assigned to multiple agents`);
19717
19910
  }
@@ -20138,6 +20331,286 @@ class CouncilManager {
20138
20331
  };
20139
20332
  }
20140
20333
  }
20334
+ // src/divoom/manager.ts
20335
+ import { spawn } from "node:child_process";
20336
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
20337
+ import * as os2 from "node:os";
20338
+ import path3 from "node:path";
20339
+ import { fileURLToPath } from "node:url";
20340
+ var AGENT_GIFS = {
20341
+ council: "council.gif",
20342
+ councillor: "council.gif",
20343
+ designer: "designer.gif",
20344
+ explorer: "explorer.gif",
20345
+ fixer: "fixer.gif",
20346
+ input: "input.gif",
20347
+ intro: "intro.gif",
20348
+ librarian: "librarian.gif",
20349
+ oracle: "oracle.gif",
20350
+ orchestrator: "orchestrator.gif"
20351
+ };
20352
+ var DEFAULT_DIVOOM_CONFIG = {
20353
+ enabled: false,
20354
+ python: "/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python",
20355
+ script: "/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py",
20356
+ size: 128,
20357
+ fps: 8,
20358
+ speed: 125,
20359
+ maxFrames: 24,
20360
+ posterizeBits: 3
20361
+ };
20362
+ var DIVOOM_ENABLE_ENV = "OH_MY_OPENCODE_SLIM_DIVOOM";
20363
+ function resolveAssetDir() {
20364
+ const moduleDir = path3.dirname(fileURLToPath(import.meta.url));
20365
+ const candidates = [
20366
+ moduleDir,
20367
+ path3.resolve(moduleDir, "divoom"),
20368
+ path3.resolve(moduleDir, "../../src/divoom"),
20369
+ path3.resolve(process.cwd(), "src/divoom")
20370
+ ];
20371
+ for (const candidate of candidates) {
20372
+ if (existsSync2(path3.join(candidate, "intro.gif"))) {
20373
+ return candidate;
20374
+ }
20375
+ }
20376
+ return null;
20377
+ }
20378
+ function normalizeAgentName(value) {
20379
+ if (typeof value !== "string")
20380
+ return null;
20381
+ const normalized = value.trim().toLowerCase();
20382
+ return normalized.length > 0 ? normalized : null;
20383
+ }
20384
+ function isEnvEnabled(value) {
20385
+ if (!value)
20386
+ return false;
20387
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
20388
+ }
20389
+ function inputKey(sessionId, requestId) {
20390
+ return `${sessionId}:${requestId}`;
20391
+ }
20392
+ function getDivoomOutDir(homeDir = os2.homedir()) {
20393
+ const xdg = process.env.XDG_DATA_HOME?.trim();
20394
+ const baseDir = xdg && xdg.length > 0 && path3.isAbsolute(xdg) ? xdg : path3.join(homeDir, ".local", "share");
20395
+ return path3.join(baseDir, "opencode", "storage", "oh-my-opencode-slim", "divoom", "captures");
20396
+ }
20397
+
20398
+ class DivoomManager {
20399
+ sender;
20400
+ assetDir;
20401
+ config;
20402
+ parentStates = new Map;
20403
+ pendingUserInputs = new Set;
20404
+ orchestratorBusy = false;
20405
+ latestRequestedGifPath;
20406
+ lastGifPath;
20407
+ sendQueue = Promise.resolve();
20408
+ constructor(config, sender = defaultSender, options = {}) {
20409
+ this.sender = sender;
20410
+ this.config = {
20411
+ ...DEFAULT_DIVOOM_CONFIG,
20412
+ ...config,
20413
+ enabled: isEnvEnabled(process.env[DIVOOM_ENABLE_ENV]) ? true : config?.enabled ?? false,
20414
+ gifs: config?.gifs
20415
+ };
20416
+ this.assetDir = options.assetDir ?? resolveAssetDir();
20417
+ if (options.sender) {
20418
+ this.sender = options.sender;
20419
+ }
20420
+ }
20421
+ onPluginLoad() {
20422
+ this.show("intro");
20423
+ }
20424
+ onTaskStart(input) {
20425
+ if (!input.parentSessionId || !input.callId)
20426
+ return;
20427
+ if (!isTaskArgs(input.args))
20428
+ return;
20429
+ const agent = normalizeAgentName(input.args.subagent_type);
20430
+ if (!agent)
20431
+ return;
20432
+ const state = this.getParentState(input.parentSessionId);
20433
+ const wasIdle = state.activeCalls.size === 0;
20434
+ state.activeCalls.set(input.callId, agent);
20435
+ this.orchestratorBusy = true;
20436
+ if (wasIdle && !state.displayedAgent) {
20437
+ state.displayedAgent = agent;
20438
+ }
20439
+ this.render();
20440
+ }
20441
+ onTaskEnd(input) {
20442
+ if (!input.parentSessionId || !input.callId)
20443
+ return;
20444
+ const state = this.parentStates.get(input.parentSessionId);
20445
+ if (!state)
20446
+ return;
20447
+ state.activeCalls.delete(input.callId);
20448
+ if (state.activeCalls.size === 0) {
20449
+ this.parentStates.delete(input.parentSessionId);
20450
+ }
20451
+ this.render();
20452
+ }
20453
+ onUserInputRequired(input) {
20454
+ if (!input.sessionId || !input.requestId)
20455
+ return;
20456
+ this.pendingUserInputs.add(inputKey(input.sessionId, input.requestId));
20457
+ this.render();
20458
+ }
20459
+ onUserInputResolved(input) {
20460
+ if (!input.sessionId || !input.requestId)
20461
+ return;
20462
+ this.pendingUserInputs.delete(inputKey(input.sessionId, input.requestId));
20463
+ this.render();
20464
+ }
20465
+ onOrchestratorStatus(input) {
20466
+ if (!input.sessionId || !input.isOrchestrator)
20467
+ return;
20468
+ if (input.status === "busy") {
20469
+ this.orchestratorBusy = true;
20470
+ this.render();
20471
+ return;
20472
+ }
20473
+ if (input.status === "idle") {
20474
+ this.orchestratorBusy = false;
20475
+ this.parentStates.delete(input.sessionId);
20476
+ this.render();
20477
+ }
20478
+ }
20479
+ onSessionDeleted(input) {
20480
+ const sessionId = input.sessionId;
20481
+ if (!sessionId)
20482
+ return;
20483
+ if (input.isOrchestrator)
20484
+ this.orchestratorBusy = false;
20485
+ this.parentStates.delete(sessionId);
20486
+ this.pendingUserInputs = new Set(Array.from(this.pendingUserInputs).filter((key) => !key.startsWith(`${sessionId}:`)));
20487
+ this.render();
20488
+ }
20489
+ render() {
20490
+ if (this.pendingUserInputs.size > 0) {
20491
+ this.show("input");
20492
+ return;
20493
+ }
20494
+ const activeAgent = Array.from(this.parentStates.values()).find((state) => state.displayedAgent && state.activeCalls.size > 0)?.displayedAgent;
20495
+ if (activeAgent) {
20496
+ this.show(activeAgent);
20497
+ return;
20498
+ }
20499
+ this.show(this.orchestratorBusy ? "orchestrator" : "intro");
20500
+ }
20501
+ getParentState(parentSessionId) {
20502
+ const existing = this.parentStates.get(parentSessionId);
20503
+ if (existing)
20504
+ return existing;
20505
+ const created = {
20506
+ activeCalls: new Map
20507
+ };
20508
+ this.parentStates.set(parentSessionId, created);
20509
+ return created;
20510
+ }
20511
+ show(agent) {
20512
+ if (!this.config.enabled)
20513
+ return;
20514
+ if (!this.assetDir) {
20515
+ log("[divoom] asset directory not found");
20516
+ return;
20517
+ }
20518
+ const fileName = this.config.gifs?.[agent] ?? AGENT_GIFS[agent] ?? AGENT_GIFS.orchestrator;
20519
+ const requestedGifPath = path3.isAbsolute(fileName) ? fileName : path3.join(this.assetDir, fileName);
20520
+ const fallbackGifPath = path3.join(this.assetDir, AGENT_GIFS.intro);
20521
+ const gifPath = existsSync2(requestedGifPath) ? requestedGifPath : agent === "input" ? fallbackGifPath : requestedGifPath;
20522
+ if (!existsSync2(gifPath)) {
20523
+ log("[divoom] gif not found", { agent, gifPath: requestedGifPath });
20524
+ return;
20525
+ }
20526
+ if (gifPath === this.latestRequestedGifPath)
20527
+ return;
20528
+ this.latestRequestedGifPath = gifPath;
20529
+ const outDir = getDivoomOutDir();
20530
+ try {
20531
+ mkdirSync2(outDir, { recursive: true });
20532
+ } catch (error) {
20533
+ this.clearLatestIfCurrent(gifPath);
20534
+ log("[divoom] output directory not writable", {
20535
+ outDir,
20536
+ error: String(error)
20537
+ });
20538
+ return;
20539
+ }
20540
+ const call = {
20541
+ command: this.config.python,
20542
+ args: [
20543
+ this.config.script,
20544
+ gifPath,
20545
+ "--size",
20546
+ String(this.config.size),
20547
+ "--fps",
20548
+ String(this.config.fps),
20549
+ "--speed",
20550
+ String(this.config.speed),
20551
+ "--max-frames",
20552
+ String(this.config.maxFrames),
20553
+ "--posterize-bits",
20554
+ String(this.config.posterizeBits),
20555
+ "--out-dir",
20556
+ outDir
20557
+ ]
20558
+ };
20559
+ this.sendQueue = this.sendQueue.catch(() => {}).then(async () => {
20560
+ if (gifPath !== this.latestRequestedGifPath)
20561
+ return;
20562
+ if (!existsSync2(this.config.python)) {
20563
+ this.clearLatestIfCurrent(gifPath);
20564
+ log("[divoom] python executable not found", this.config.python);
20565
+ return;
20566
+ }
20567
+ if (!existsSync2(this.config.script)) {
20568
+ this.clearLatestIfCurrent(gifPath);
20569
+ log("[divoom] sender script not found", this.config.script);
20570
+ return;
20571
+ }
20572
+ try {
20573
+ await this.sender(call);
20574
+ this.lastGifPath = gifPath;
20575
+ log("[divoom] showing gif", { agent, gifPath });
20576
+ } catch (error) {
20577
+ this.clearLatestIfCurrent(gifPath);
20578
+ log("[divoom] failed to send gif", String(error));
20579
+ }
20580
+ });
20581
+ }
20582
+ async flush() {
20583
+ await this.sendQueue.catch(() => {});
20584
+ }
20585
+ clearLatestIfCurrent(gifPath) {
20586
+ if (this.latestRequestedGifPath === gifPath) {
20587
+ this.latestRequestedGifPath = undefined;
20588
+ }
20589
+ if (this.lastGifPath === gifPath) {
20590
+ this.lastGifPath = undefined;
20591
+ }
20592
+ }
20593
+ }
20594
+ function isTaskArgs(value) {
20595
+ return typeof value === "object" && value !== null;
20596
+ }
20597
+ function defaultSender(call) {
20598
+ return new Promise((resolve, reject) => {
20599
+ const child = spawn(call.command, call.args, {
20600
+ detached: true,
20601
+ stdio: "ignore"
20602
+ });
20603
+ child.once("error", reject);
20604
+ child.once("spawn", () => {
20605
+ child.unref();
20606
+ resolve();
20607
+ });
20608
+ });
20609
+ }
20610
+ function createDivoomManager(config) {
20611
+ return new DivoomManager(config);
20612
+ }
20613
+
20141
20614
  // src/hooks/apply-patch/errors.ts
20142
20615
  var APPLY_PATCH_ERROR_PREFIX = {
20143
20616
  blocked: "apply_patch blocked",
@@ -20206,7 +20679,7 @@ function ensureApplyPatchError(error, context) {
20206
20679
 
20207
20680
  // src/hooks/apply-patch/execution-context.ts
20208
20681
  import * as fs3 from "node:fs/promises";
20209
- import path3 from "node:path";
20682
+ import path4 from "node:path";
20210
20683
 
20211
20684
  // src/hooks/apply-patch/codec.ts
20212
20685
  function normalizeLineEndings(text) {
@@ -21075,7 +21548,7 @@ function isMissingPathError(error) {
21075
21548
  }
21076
21549
  async function real(target) {
21077
21550
  const parts = [];
21078
- let current = path3.resolve(target);
21551
+ let current = path4.resolve(target);
21079
21552
  while (true) {
21080
21553
  const exact = await fs3.realpath(current).catch((error) => {
21081
21554
  if (isMissingPathError(error)) {
@@ -21084,19 +21557,19 @@ async function real(target) {
21084
21557
  throw createApplyPatchInternalError(`Failed to resolve real path: ${current}`, error);
21085
21558
  });
21086
21559
  if (exact) {
21087
- return parts.length === 0 ? exact : path3.join(exact, ...parts.reverse());
21560
+ return parts.length === 0 ? exact : path4.join(exact, ...parts.reverse());
21088
21561
  }
21089
- const parent = path3.dirname(current);
21562
+ const parent = path4.dirname(current);
21090
21563
  if (parent === current) {
21091
- return parts.length === 0 ? current : path3.join(current, ...parts.reverse());
21564
+ return parts.length === 0 ? current : path4.join(current, ...parts.reverse());
21092
21565
  }
21093
- parts.push(path3.basename(current));
21566
+ parts.push(path4.basename(current));
21094
21567
  current = parent;
21095
21568
  }
21096
21569
  }
21097
21570
  function inside(root, target) {
21098
- const rel = path3.relative(root, target);
21099
- return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
21571
+ const rel = path4.relative(root, target);
21572
+ return rel === "" || !rel.startsWith("..") && !path4.isAbsolute(rel);
21100
21573
  }
21101
21574
  function createPathGuardContext(root, worktree) {
21102
21575
  return {
@@ -21106,7 +21579,7 @@ function createPathGuardContext(root, worktree) {
21106
21579
  };
21107
21580
  }
21108
21581
  async function realCached(ctx, target) {
21109
- const resolvedTarget = path3.resolve(target);
21582
+ const resolvedTarget = path4.resolve(target);
21110
21583
  let pending = ctx.realCache.get(resolvedTarget);
21111
21584
  if (!pending) {
21112
21585
  pending = real(resolvedTarget);
@@ -21157,22 +21630,22 @@ async function assertRegularFile(ctx, filePath, verb) {
21157
21630
  function collectPatchTargets(root, hunks) {
21158
21631
  const targets = new Set;
21159
21632
  for (const hunk of hunks) {
21160
- targets.add(path3.resolve(root, hunk.path));
21633
+ targets.add(path4.resolve(root, hunk.path));
21161
21634
  if (hunk.type === "update" && hunk.move_path) {
21162
- targets.add(path3.resolve(root, hunk.move_path));
21635
+ targets.add(path4.resolve(root, hunk.move_path));
21163
21636
  }
21164
21637
  }
21165
21638
  return [...targets];
21166
21639
  }
21167
21640
  function toRelativePatchPath(root, target) {
21168
- const relative = path3.relative(root, target);
21641
+ const relative = path4.relative(root, target);
21169
21642
  return (relative.length === 0 ? "." : relative).replaceAll("\\", "/");
21170
21643
  }
21171
21644
  function normalizePatchPath(root, value) {
21172
- return path3.isAbsolute(value) ? toRelativePatchPath(root, path3.resolve(value)) : value;
21645
+ return path4.isAbsolute(value) ? toRelativePatchPath(root, path4.resolve(value)) : value;
21173
21646
  }
21174
21647
  function normalizePatchPaths(root, hunks) {
21175
- const resolvedRoot = path3.resolve(root);
21648
+ const resolvedRoot = path4.resolve(root);
21176
21649
  const normalized = [];
21177
21650
  let changed = false;
21178
21651
  for (const hunk of hunks) {
@@ -21296,7 +21769,7 @@ function stageAddedText(contents) {
21296
21769
  `;
21297
21770
  }
21298
21771
  // src/hooks/apply-patch/rewrite.ts
21299
- import path4 from "node:path";
21772
+ import path5 from "node:path";
21300
21773
  function normalizeTextLineEndings(text) {
21301
21774
  return text.replace(/\r\n/g, `
21302
21775
  `).replace(/\r/g, `
@@ -21453,7 +21926,7 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21453
21926
  const dependencyGroups = new Map;
21454
21927
  for (const hunk of hunks) {
21455
21928
  if (hunk.type === "add") {
21456
- const filePath2 = path4.resolve(root, hunk.path);
21929
+ const filePath2 = path5.resolve(root, hunk.path);
21457
21930
  await assertPreparedPathMissing(filePath2, "add");
21458
21931
  rewritten.push(hunk);
21459
21932
  clearDependencyGroup(filePath2);
@@ -21475,20 +21948,20 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21475
21948
  continue;
21476
21949
  }
21477
21950
  if (hunk.type === "delete") {
21478
- const filePath2 = path4.resolve(root, hunk.path);
21951
+ const filePath2 = path5.resolve(root, hunk.path);
21479
21952
  await getPreparedFileState(filePath2, "delete");
21480
21953
  clearDependencyGroup(filePath2);
21481
21954
  rewritten.push(hunk);
21482
21955
  staged.set(filePath2, { exists: false, derived: true });
21483
21956
  continue;
21484
21957
  }
21485
- const filePath = path4.resolve(root, hunk.path);
21958
+ const filePath = path5.resolve(root, hunk.path);
21486
21959
  const currentDependency = dependencyGroups.get(filePath);
21487
21960
  const current = await getPreparedFileState(filePath, "update");
21488
21961
  if (!current.exists) {
21489
21962
  throw createApplyPatchVerificationError(`Failed to read file to update: ${filePath}`);
21490
21963
  }
21491
- const movePath = hunk.move_path ? path4.resolve(root, hunk.move_path) : undefined;
21964
+ const movePath = hunk.move_path ? path5.resolve(root, hunk.move_path) : undefined;
21492
21965
  if (movePath && movePath !== filePath) {
21493
21966
  await assertPreparedPathMissing(movePath, "move");
21494
21967
  }
@@ -21584,6 +22057,15 @@ var APPLY_PATCH_RESCUE_OPTIONS = {
21584
22057
  prefixSuffix: true,
21585
22058
  lcsRescue: true
21586
22059
  };
22060
+ function replacePatchArgs(output, args, patchText) {
22061
+ const nextArgs = { ...args, patchText };
22062
+ try {
22063
+ output.args = nextArgs;
22064
+ } catch {
22065
+ return false;
22066
+ }
22067
+ return output.args?.patchText === patchText;
22068
+ }
21587
22069
  function createApplyPatchHook(ctx) {
21588
22070
  function logHookStatus(state, data) {
21589
22071
  log(`apply-patch hook ${state}`, data);
@@ -21603,8 +22085,16 @@ function createApplyPatchHook(ctx) {
21603
22085
  try {
21604
22086
  const result = await rewritePatch(root, patchText, APPLY_PATCH_RESCUE_OPTIONS, worktree);
21605
22087
  if (result.changed) {
21606
- args.patchText = result.patchText;
21607
- logHookStatus("rewrite");
22088
+ if (replacePatchArgs(output, args, result.patchText)) {
22089
+ logHookStatus("rewrite");
22090
+ } else {
22091
+ logHookStatus("skipped", {
22092
+ reason: "readonly output args",
22093
+ failOpen: true,
22094
+ rescueOptions: APPLY_PATCH_RESCUE_OPTIONS,
22095
+ rewriteStage: "before-native"
22096
+ });
22097
+ }
21608
22098
  return;
21609
22099
  }
21610
22100
  logHookStatus("unchanged");
@@ -21636,78 +22126,28 @@ function createApplyPatchHook(ctx) {
21636
22126
  }
21637
22127
  };
21638
22128
  }
21639
- // src/utils/compat.ts
21640
- import { spawn as nodeSpawn } from "node:child_process";
21641
- import { writeFile as fsWriteFile } from "node:fs/promises";
21642
- var isBun = typeof globalThis.Bun !== "undefined";
21643
- function collectStream(stream) {
21644
- if (!stream)
21645
- return () => Promise.resolve("");
21646
- const chunks = [];
21647
- stream.on("data", (chunk) => chunks.push(chunk));
21648
- return () => new Promise((resolve, reject) => {
21649
- if (!stream.readable) {
21650
- resolve(Buffer.concat(chunks).toString("utf-8"));
21651
- return;
21652
- }
21653
- stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
21654
- stream.on("error", reject);
21655
- });
21656
- }
21657
- function crossSpawn(command, options) {
21658
- const [cmd, ...args] = command;
21659
- const proc = nodeSpawn(cmd, args, {
21660
- stdio: [
21661
- options?.stdin ?? "ignore",
21662
- options?.stdout ?? "pipe",
21663
- options?.stderr ?? "pipe"
21664
- ],
21665
- cwd: options?.cwd,
21666
- env: options?.env
21667
- });
21668
- const stdoutCollector = collectStream(proc.stdout);
21669
- const stderrCollector = collectStream(proc.stderr);
21670
- const exited = new Promise((resolve, reject) => {
21671
- proc.on("error", reject);
21672
- proc.on("close", (code) => resolve(code ?? 1));
21673
- });
21674
- return {
21675
- proc,
21676
- stdout: stdoutCollector,
21677
- stderr: stderrCollector,
21678
- exited,
21679
- kill: (signal) => proc.kill(signal),
21680
- get exitCode() {
21681
- return proc.exitCode;
21682
- }
21683
- };
21684
- }
21685
- async function crossWrite(path5, data) {
21686
- await fsWriteFile(path5, Buffer.from(data));
21687
- }
21688
-
21689
22129
  // src/hooks/auto-update-checker/cache.ts
21690
22130
  import * as fs5 from "node:fs";
21691
- import * as path7 from "node:path";
22131
+ import * as path8 from "node:path";
21692
22132
  // src/hooks/auto-update-checker/checker.ts
21693
22133
  import * as fs4 from "node:fs";
21694
- import * as path6 from "node:path";
21695
- import { fileURLToPath } from "node:url";
22134
+ import * as path7 from "node:path";
22135
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
21696
22136
 
21697
22137
  // src/hooks/auto-update-checker/constants.ts
21698
- import * as os2 from "node:os";
21699
- import * as path5 from "node:path";
22138
+ import * as os3 from "node:os";
22139
+ import * as path6 from "node:path";
21700
22140
  var PACKAGE_NAME = "oh-my-opencode-slim";
21701
22141
  var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
21702
22142
  var NPM_FETCH_TIMEOUT = 5000;
21703
22143
  function getCacheDir() {
21704
22144
  if (process.platform === "win32") {
21705
- return path5.join(process.env.LOCALAPPDATA ?? os2.homedir(), "opencode");
22145
+ return path6.join(process.env.LOCALAPPDATA ?? os3.homedir(), "opencode");
21706
22146
  }
21707
- return path5.join(os2.homedir(), ".cache", "opencode");
22147
+ return path6.join(os3.homedir(), ".cache", "opencode");
21708
22148
  }
21709
22149
  var CACHE_DIR = getCacheDir();
21710
- var INSTALLED_PACKAGE_JSON = path5.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
22150
+ var INSTALLED_PACKAGE_JSON = path6.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
21711
22151
  var configPaths = getOpenCodeConfigPaths();
21712
22152
  var USER_OPENCODE_CONFIG = configPaths[0];
21713
22153
  var USER_OPENCODE_CONFIG_JSONC = configPaths[1];
@@ -21742,8 +22182,8 @@ function extractChannel(version) {
21742
22182
  }
21743
22183
  function getConfigPaths(directory) {
21744
22184
  return [
21745
- path6.join(directory, ".opencode", "opencode.json"),
21746
- path6.join(directory, ".opencode", "opencode.jsonc"),
22185
+ path7.join(directory, ".opencode", "opencode.json"),
22186
+ path7.join(directory, ".opencode", "opencode.jsonc"),
21747
22187
  USER_OPENCODE_CONFIG,
21748
22188
  USER_OPENCODE_CONFIG_JSONC
21749
22189
  ];
@@ -21759,7 +22199,7 @@ function getLocalDevPath(directory) {
21759
22199
  for (const entry of plugins) {
21760
22200
  if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) {
21761
22201
  try {
21762
- return fileURLToPath(entry);
22202
+ return fileURLToPath2(entry);
21763
22203
  } catch {
21764
22204
  return entry.replace("file://", "");
21765
22205
  }
@@ -21772,9 +22212,9 @@ function getLocalDevPath(directory) {
21772
22212
  function findPackageJsonUp(startPath) {
21773
22213
  try {
21774
22214
  const stat2 = fs4.statSync(startPath);
21775
- let dir = stat2.isDirectory() ? startPath : path6.dirname(startPath);
22215
+ let dir = stat2.isDirectory() ? startPath : path7.dirname(startPath);
21776
22216
  for (let i = 0;i < 10; i++) {
21777
- const pkgPath = path6.join(dir, "package.json");
22217
+ const pkgPath = path7.join(dir, "package.json");
21778
22218
  if (fs4.existsSync(pkgPath)) {
21779
22219
  try {
21780
22220
  const content = fs4.readFileSync(pkgPath, "utf-8");
@@ -21783,7 +22223,7 @@ function findPackageJsonUp(startPath) {
21783
22223
  return pkgPath;
21784
22224
  } catch {}
21785
22225
  }
21786
- const parent = path6.dirname(dir);
22226
+ const parent = path7.dirname(dir);
21787
22227
  if (parent === dir)
21788
22228
  break;
21789
22229
  dir = parent;
@@ -21808,7 +22248,7 @@ function getLocalDevVersion(directory) {
21808
22248
  }
21809
22249
  function getCurrentRuntimePackageJsonPath(currentModuleUrl = import.meta.url) {
21810
22250
  try {
21811
- const currentDir = path6.dirname(fileURLToPath(currentModuleUrl));
22251
+ const currentDir = path7.dirname(fileURLToPath2(currentModuleUrl));
21812
22252
  return findPackageJsonUp(currentDir);
21813
22253
  } catch (err) {
21814
22254
  log("[auto-update-checker] Failed to resolve runtime package path:", err);
@@ -21892,7 +22332,7 @@ async function getLatestVersion(channel = "latest") {
21892
22332
 
21893
22333
  // src/hooks/auto-update-checker/cache.ts
21894
22334
  function removeFromBunLock(installDir, packageName) {
21895
- const lockPath = path7.join(installDir, "bun.lock");
22335
+ const lockPath = path8.join(installDir, "bun.lock");
21896
22336
  if (!fs5.existsSync(lockPath))
21897
22337
  return false;
21898
22338
  try {
@@ -21943,7 +22383,7 @@ function ensureDependencyVersion(packageJsonPath, packageName, version) {
21943
22383
  }
21944
22384
  }
21945
22385
  function removeInstalledPackage(installDir, packageName) {
21946
- const pkgDir = path7.join(installDir, "node_modules", packageName);
22386
+ const pkgDir = path8.join(installDir, "node_modules", packageName);
21947
22387
  if (!fs5.existsSync(pkgDir))
21948
22388
  return false;
21949
22389
  fs5.rmSync(pkgDir, { recursive: true, force: true });
@@ -21952,18 +22392,18 @@ function removeInstalledPackage(installDir, packageName) {
21952
22392
  }
21953
22393
  function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
21954
22394
  if (runtimePackageJsonPath) {
21955
- const packageDir = path7.dirname(runtimePackageJsonPath);
21956
- const nodeModulesDir = path7.dirname(packageDir);
21957
- if (path7.basename(packageDir) === PACKAGE_NAME && path7.basename(nodeModulesDir) === "node_modules") {
21958
- const installDir = path7.dirname(nodeModulesDir);
21959
- const packageJsonPath = path7.join(installDir, "package.json");
22395
+ const packageDir = path8.dirname(runtimePackageJsonPath);
22396
+ const nodeModulesDir = path8.dirname(packageDir);
22397
+ if (path8.basename(packageDir) === PACKAGE_NAME && path8.basename(nodeModulesDir) === "node_modules") {
22398
+ const installDir = path8.dirname(nodeModulesDir);
22399
+ const packageJsonPath = path8.join(installDir, "package.json");
21960
22400
  if (fs5.existsSync(packageJsonPath)) {
21961
22401
  return { installDir, packageJsonPath };
21962
22402
  }
21963
22403
  }
21964
22404
  return null;
21965
22405
  }
21966
- const legacyPackageJsonPath = path7.join(CACHE_DIR, "package.json");
22406
+ const legacyPackageJsonPath = path8.join(CACHE_DIR, "package.json");
21967
22407
  if (fs5.existsSync(legacyPackageJsonPath)) {
21968
22408
  return { installDir: CACHE_DIR, packageJsonPath: legacyPackageJsonPath };
21969
22409
  }
@@ -22097,7 +22537,7 @@ function showToast(ctx, title, message, variant = "info", duration = 3000) {
22097
22537
  }).catch(() => {});
22098
22538
  }
22099
22539
  // src/utils/agent-variant.ts
22100
- function normalizeAgentName(agentName) {
22540
+ function normalizeAgentName2(agentName) {
22101
22541
  const trimmed = agentName.trim();
22102
22542
  return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
22103
22543
  }
@@ -22109,7 +22549,7 @@ function getRuntimeAgentNames(config) {
22109
22549
  return [...unique];
22110
22550
  }
22111
22551
  function resolveRuntimeAgentName(config, agentName) {
22112
- const normalized = normalizeAgentName(agentName);
22552
+ const normalized = normalizeAgentName2(agentName);
22113
22553
  if (!normalized) {
22114
22554
  return normalized;
22115
22555
  }
@@ -22121,7 +22561,7 @@ function resolveRuntimeAgentName(config, agentName) {
22121
22561
  if (!displayName) {
22122
22562
  continue;
22123
22563
  }
22124
- if (normalizeAgentName(displayName) === normalized) {
22564
+ if (normalizeAgentName2(displayName) === normalized) {
22125
22565
  return internalName;
22126
22566
  }
22127
22567
  }
@@ -22137,7 +22577,7 @@ function createDisplayNameMentionRewriter(config) {
22137
22577
  if (!displayName) {
22138
22578
  continue;
22139
22579
  }
22140
- const normalizedDisplayName = normalizeAgentName(displayName);
22580
+ const normalizedDisplayName = normalizeAgentName2(displayName);
22141
22581
  if (!normalizedDisplayName || normalizedDisplayName === internalName) {
22142
22582
  continue;
22143
22583
  }
@@ -22447,8 +22887,8 @@ function isPwshAvailable() {
22447
22887
  });
22448
22888
  return result.status === 0;
22449
22889
  }
22450
- function escapePowerShellPath(path8) {
22451
- return path8.replace(/'/g, "''");
22890
+ function escapePowerShellPath(path9) {
22891
+ return path9.replace(/'/g, "''");
22452
22892
  }
22453
22893
  function getWindowsZipExtractor() {
22454
22894
  const buildNumber = getWindowsBuildNumber();
@@ -22777,6 +23217,7 @@ function parseModel(model) {
22777
23217
  return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
22778
23218
  }
22779
23219
  var DEDUP_WINDOW_MS = 5000;
23220
+ var REPROMPT_DELAY_MS = 500;
22780
23221
 
22781
23222
  class ForegroundFallbackManager {
22782
23223
  client;
@@ -22909,11 +23350,20 @@ class ForegroundFallbackManager {
22909
23350
  log("[foreground-fallback] no user message found", { sessionID });
22910
23351
  return;
22911
23352
  }
22912
- try {
22913
- await this.client.session.abort({ path: { id: sessionID } });
22914
- } catch {}
22915
- await new Promise((r) => setTimeout(r, 500));
22916
23353
  const sessionClient = this.client.session;
23354
+ if (typeof sessionClient.promptAsync !== "function") {
23355
+ log("[foreground-fallback] promptAsync unavailable", { sessionID });
23356
+ return;
23357
+ }
23358
+ try {
23359
+ await abortSessionWithTimeout(this.client, sessionID);
23360
+ } catch (error) {
23361
+ log("[foreground-fallback] abort did not complete cleanly", {
23362
+ sessionID,
23363
+ error: error instanceof Error ? error.message : String(error)
23364
+ });
23365
+ }
23366
+ await new Promise((r) => setTimeout(r, REPROMPT_DELAY_MS));
22917
23367
  await sessionClient.promptAsync({
22918
23368
  path: { id: sessionID },
22919
23369
  body: { parts: lastUser.parts, model: ref }
@@ -22960,8 +23410,8 @@ class ForegroundFallbackManager {
22960
23410
  // src/hooks/image-hook.ts
22961
23411
  import { createHash } from "node:crypto";
22962
23412
  import {
22963
- existsSync as existsSync4,
22964
- mkdirSync as mkdirSync2,
23413
+ existsSync as existsSync5,
23414
+ mkdirSync as mkdirSync3,
22965
23415
  readdirSync as readdirSync2,
22966
23416
  rmdirSync,
22967
23417
  statSync as statSync3,
@@ -23056,7 +23506,7 @@ function writeUniqueFile(dir, name, data, log2) {
23056
23506
  const ext = extname(name);
23057
23507
  const base = basename2(name, ext) || name;
23058
23508
  let candidate = join7(dir, name);
23059
- if (existsSync4(candidate)) {
23509
+ if (existsSync5(candidate)) {
23060
23510
  return candidate;
23061
23511
  }
23062
23512
  let counter = 0;
@@ -23094,14 +23544,14 @@ function processImageAttachments(args) {
23094
23544
  }
23095
23545
  const saveDir = join7(workDir, ".opencode", "images");
23096
23546
  if (messagesWithImages.length === 0) {
23097
- if (existsSync4(saveDir))
23547
+ if (existsSync5(saveDir))
23098
23548
  cleanupAllSessions(saveDir);
23099
23549
  return;
23100
23550
  }
23101
23551
  const gitignorePath = join7(workDir, ".opencode", ".gitignore");
23102
23552
  try {
23103
- mkdirSync2(saveDir, { recursive: true });
23104
- if (!existsSync4(gitignorePath))
23553
+ mkdirSync3(saveDir, { recursive: true });
23554
+ if (!existsSync5(gitignorePath))
23105
23555
  writeFileSync3(gitignorePath, `*
23106
23556
  `);
23107
23557
  } catch (e) {
@@ -23112,7 +23562,7 @@ function processImageAttachments(args) {
23112
23562
  const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
23113
23563
  const targetDir = sessionSubdir ? join7(saveDir, sessionSubdir) : saveDir;
23114
23564
  try {
23115
- mkdirSync2(targetDir, { recursive: true });
23565
+ mkdirSync3(targetDir, { recursive: true });
23116
23566
  } catch (e) {
23117
23567
  log2(`[image-hook] failed to create target image directory: ${e}`);
23118
23568
  }
@@ -23271,7 +23721,7 @@ function createPostFileToolNudgeHook(options = {}) {
23271
23721
  };
23272
23722
  }
23273
23723
  // src/hooks/task-session-manager/index.ts
23274
- import path8 from "node:path";
23724
+ import path9 from "node:path";
23275
23725
  var AGENT_NAME_SET = new Set([
23276
23726
  "orchestrator",
23277
23727
  "oracle",
@@ -23296,8 +23746,8 @@ function extractPath(output) {
23296
23746
  return /<path>([^<]+)<\/path>/.exec(output)?.[1];
23297
23747
  }
23298
23748
  function normalizePath(root, file) {
23299
- const relative = path8.relative(root, file);
23300
- if (!relative || relative.startsWith("..") || path8.isAbsolute(relative)) {
23749
+ const relative = path9.relative(root, file);
23750
+ if (!relative || relative.startsWith("..") || path9.isAbsolute(relative)) {
23301
23751
  return file;
23302
23752
  }
23303
23753
  return relative;
@@ -23689,6 +24139,7 @@ function createTodoHygiene(options) {
23689
24139
  // src/hooks/todo-continuation/index.ts
23690
24140
  var HOOK_NAME = "todo-continuation";
23691
24141
  var COMMAND_NAME = "auto-continue";
24142
+ var TODO_STATE_TIMEOUT_MS = 500;
23692
24143
  var CONTINUATION_PROMPT = "[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]";
23693
24144
  var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
23694
24145
  var TODO_HYGIENE_INSTRUCTION_CLOSE = "</instruction>";
@@ -23776,12 +24227,15 @@ function createTodoContinuationHook(ctx, config) {
23776
24227
  notifyingSessionIds: new Set,
23777
24228
  notificationBusyUntilBySession: new Map
23778
24229
  };
24230
+ async function fetchTodos(sessionID) {
24231
+ const result = await withTimeout(ctx.client.session.todo({
24232
+ path: { id: sessionID }
24233
+ }), TODO_STATE_TIMEOUT_MS, `Todo state lookup timed out after ${TODO_STATE_TIMEOUT_MS}ms`);
24234
+ return result.data;
24235
+ }
23779
24236
  const hygiene = createTodoHygiene({
23780
24237
  getTodoState: async (sessionID) => {
23781
- const result = await ctx.client.session.todo({
23782
- path: { id: sessionID }
23783
- });
23784
- const todos = result.data;
24238
+ const todos = await fetchTodos(sessionID);
23785
24239
  const openTodos = todos.filter((todo) => !TERMINAL_TODO_STATUSES.includes(todo.status));
23786
24240
  return {
23787
24241
  hasOpenTodos: openTodos.length > 0,
@@ -23966,10 +24420,7 @@ function createTodoContinuationHook(ctx, config) {
23966
24420
  }
23967
24421
  if (autoEnable && !state.enabled) {
23968
24422
  try {
23969
- const todosResult = await ctx.client.session.todo({
23970
- path: { id: sessionID }
23971
- });
23972
- const todos = todosResult.data;
24423
+ const todos = await fetchTodos(sessionID);
23973
24424
  const incompleteCount2 = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
23974
24425
  if (incompleteCount2 >= autoEnableThreshold) {
23975
24426
  state.enabled = true;
@@ -23995,10 +24446,7 @@ function createTodoContinuationHook(ctx, config) {
23995
24446
  let hasIncompleteTodos = false;
23996
24447
  let incompleteCount = 0;
23997
24448
  try {
23998
- const todosResult = await ctx.client.session.todo({
23999
- path: { id: sessionID }
24000
- });
24001
- const todos = todosResult.data;
24449
+ const todos = await fetchTodos(sessionID);
24002
24450
  incompleteCount = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
24003
24451
  hasIncompleteTodos = incompleteCount > 0;
24004
24452
  log(`[${HOOK_NAME}] Fetched todos`, {
@@ -24209,10 +24657,7 @@ function createTodoContinuationHook(ctx, config) {
24209
24657
  });
24210
24658
  let hasIncompleteTodos = false;
24211
24659
  try {
24212
- const todosResult = await ctx.client.session.todo({
24213
- path: { id: input.sessionID }
24214
- });
24215
- const todos = todosResult.data;
24660
+ const todos = await fetchTodos(input.sessionID);
24216
24661
  hasIncompleteTodos = todos.some((t) => !TERMINAL_TODO_STATUSES.includes(t.status));
24217
24662
  } catch (error) {
24218
24663
  log(`[${HOOK_NAME}] Warning: failed to fetch todos in command hook`, {
@@ -24236,7 +24681,7 @@ function createTodoContinuationHook(ctx, config) {
24236
24681
  };
24237
24682
  }
24238
24683
  // src/interview/manager.ts
24239
- import path12 from "node:path";
24684
+ import path13 from "node:path";
24240
24685
 
24241
24686
  // src/interview/dashboard.ts
24242
24687
  import crypto from "node:crypto";
@@ -24245,28 +24690,28 @@ import fs7 from "node:fs/promises";
24245
24690
  import {
24246
24691
  createServer
24247
24692
  } from "node:http";
24248
- import os3 from "node:os";
24249
- import path10 from "node:path";
24693
+ import os4 from "node:os";
24694
+ import path11 from "node:path";
24250
24695
  import { URL as URL2 } from "node:url";
24251
24696
 
24252
24697
  // src/interview/document.ts
24253
24698
  import * as fsSync from "node:fs";
24254
24699
  import * as fs6 from "node:fs/promises";
24255
- import * as path9 from "node:path";
24700
+ import * as path10 from "node:path";
24256
24701
  var DEFAULT_OUTPUT_FOLDER = "interview";
24257
24702
  function normalizeOutputFolder(outputFolder) {
24258
24703
  const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
24259
24704
  return normalized || DEFAULT_OUTPUT_FOLDER;
24260
24705
  }
24261
24706
  function createInterviewDirectoryPath(directory, outputFolder) {
24262
- return path9.join(directory, normalizeOutputFolder(outputFolder));
24707
+ return path10.join(directory, normalizeOutputFolder(outputFolder));
24263
24708
  }
24264
24709
  function createInterviewFilePath(directory, outputFolder, idea) {
24265
24710
  const fileName = `${slugify(idea) || "interview"}.md`;
24266
- return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
24711
+ return path10.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
24267
24712
  }
24268
24713
  function relativeInterviewPath(directory, filePath) {
24269
- return path9.relative(directory, filePath) || path9.basename(filePath);
24714
+ return path10.relative(directory, filePath) || path10.basename(filePath);
24270
24715
  }
24271
24716
  function resolveExistingInterviewPath(directory, outputFolder, value) {
24272
24717
  const trimmed = value.trim();
@@ -24275,22 +24720,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
24275
24720
  }
24276
24721
  const outputDir = createInterviewDirectoryPath(directory, outputFolder);
24277
24722
  const candidates = new Set;
24278
- const resolvedRoot = path9.resolve(directory);
24279
- if (path9.isAbsolute(trimmed)) {
24723
+ const resolvedRoot = path10.resolve(directory);
24724
+ if (path10.isAbsolute(trimmed)) {
24280
24725
  candidates.add(trimmed);
24281
24726
  } else {
24282
- candidates.add(path9.resolve(directory, trimmed));
24283
- candidates.add(path9.join(outputDir, trimmed));
24727
+ candidates.add(path10.resolve(directory, trimmed));
24728
+ candidates.add(path10.join(outputDir, trimmed));
24284
24729
  if (!trimmed.endsWith(".md")) {
24285
- candidates.add(path9.join(outputDir, `${trimmed}.md`));
24730
+ candidates.add(path10.join(outputDir, `${trimmed}.md`));
24286
24731
  }
24287
24732
  }
24288
24733
  for (const candidate of candidates) {
24289
- if (path9.extname(candidate) !== ".md") {
24734
+ if (path10.extname(candidate) !== ".md") {
24290
24735
  continue;
24291
24736
  }
24292
- const resolved = path9.resolve(candidate);
24293
- if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
24737
+ const resolved = path10.resolve(candidate);
24738
+ if (!resolved.startsWith(resolvedRoot + path10.sep) && resolved !== resolvedRoot) {
24294
24739
  continue;
24295
24740
  }
24296
24741
  if (fsSync.existsSync(candidate)) {
@@ -24370,7 +24815,7 @@ function parseFrontmatter(content) {
24370
24815
  return result;
24371
24816
  }
24372
24817
  async function ensureInterviewFile(record) {
24373
- await fs6.mkdir(path9.dirname(record.markdownPath), { recursive: true });
24818
+ await fs6.mkdir(path10.dirname(record.markdownPath), { recursive: true });
24374
24819
  try {
24375
24820
  await fs6.access(record.markdownPath);
24376
24821
  } catch {
@@ -26040,12 +26485,12 @@ function renderInterviewPage(interviewId, resumeSlug) {
26040
26485
 
26041
26486
  // src/interview/dashboard.ts
26042
26487
  function getAuthFilePath(port) {
26043
- const dataHome = process.env.XDG_DATA_HOME || path10.join(os3.homedir(), ".local", "share");
26044
- return path10.join(dataHome, "opencode", `.dashboard-${port}.json`);
26488
+ const dataHome = process.env.XDG_DATA_HOME || path11.join(os4.homedir(), ".local", "share");
26489
+ return path11.join(dataHome, "opencode", `.dashboard-${port}.json`);
26045
26490
  }
26046
26491
  function writeAuthFile(port, token) {
26047
26492
  const filePath = getAuthFilePath(port);
26048
- const dir = path10.dirname(filePath);
26493
+ const dir = path11.dirname(filePath);
26049
26494
  try {
26050
26495
  fsSync2.mkdirSync(dir, { recursive: true });
26051
26496
  } catch {}
@@ -26142,7 +26587,7 @@ function createDashboardServer(config) {
26142
26587
  let scanDays = 30;
26143
26588
  function getKnownDirectories() {
26144
26589
  const dirs = new Set;
26145
- dirs.add(os3.homedir());
26590
+ dirs.add(os4.homedir());
26146
26591
  for (const session2 of sessions.values()) {
26147
26592
  if (session2.directory)
26148
26593
  dirs.add(session2.directory);
@@ -26182,7 +26627,7 @@ function createDashboardServer(config) {
26182
26627
  const directories = getKnownDirectories();
26183
26628
  const items = [];
26184
26629
  for (const dir of directories) {
26185
- const interviewDir = path10.join(dir, config.outputFolder);
26630
+ const interviewDir = path11.join(dir, config.outputFolder);
26186
26631
  let entries;
26187
26632
  try {
26188
26633
  entries = await fs7.readdir(interviewDir);
@@ -26194,7 +26639,7 @@ function createDashboardServer(config) {
26194
26639
  continue;
26195
26640
  let content;
26196
26641
  try {
26197
- content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
26642
+ content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
26198
26643
  } catch {
26199
26644
  continue;
26200
26645
  }
@@ -26220,7 +26665,7 @@ function createDashboardServer(config) {
26220
26665
  const directories = getKnownDirectories();
26221
26666
  let rebuilt = 0;
26222
26667
  for (const dir of directories) {
26223
- const interviewDir = path10.join(dir, config.outputFolder);
26668
+ const interviewDir = path11.join(dir, config.outputFolder);
26224
26669
  let entries;
26225
26670
  try {
26226
26671
  entries = await fs7.readdir(interviewDir);
@@ -26232,7 +26677,7 @@ function createDashboardServer(config) {
26232
26677
  continue;
26233
26678
  let content;
26234
26679
  try {
26235
- content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
26680
+ content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
26236
26681
  } catch {
26237
26682
  continue;
26238
26683
  }
@@ -26258,7 +26703,7 @@ function createDashboardServer(config) {
26258
26703
  questions: [],
26259
26704
  pendingAnswers: null,
26260
26705
  lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
26261
- filePath: path10.join(interviewDir, entry),
26706
+ filePath: path11.join(interviewDir, entry),
26262
26707
  nudgeAction: null
26263
26708
  });
26264
26709
  if (!sessions.has(fm.sessionID)) {
@@ -26522,7 +26967,7 @@ function createDashboardServer(config) {
26522
26967
  const dirs = getKnownDirectories();
26523
26968
  for (const dir of dirs) {
26524
26969
  const slug = extractResumeSlug(interviewId);
26525
- const candidate = path10.join(dir, config.outputFolder, `${slug}.md`);
26970
+ const candidate = path11.join(dir, config.outputFolder, `${slug}.md`);
26526
26971
  try {
26527
26972
  document = await fs7.readFile(candidate, "utf8");
26528
26973
  markdownPath = candidate;
@@ -27048,9 +27493,9 @@ function createInterviewServer(deps) {
27048
27493
  }
27049
27494
 
27050
27495
  // src/interview/service.ts
27051
- import { spawn } from "node:child_process";
27496
+ import { spawn as spawn2 } from "node:child_process";
27052
27497
  import * as fs8 from "node:fs/promises";
27053
- import * as path11 from "node:path";
27498
+ import * as path12 from "node:path";
27054
27499
 
27055
27500
  // src/interview/types.ts
27056
27501
  import { z as z3 } from "zod";
@@ -27252,7 +27697,7 @@ function openBrowser(url) {
27252
27697
  args = [url];
27253
27698
  }
27254
27699
  try {
27255
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
27700
+ const child = spawn2(command, args, { detached: true, stdio: "ignore" });
27256
27701
  child.on("error", (error) => {
27257
27702
  log("[interview] failed to open browser:", { error: error.message, url });
27258
27703
  });
@@ -27317,12 +27762,12 @@ function createInterviewService(ctx, config, deps) {
27317
27762
  if (!newSlug) {
27318
27763
  return;
27319
27764
  }
27320
- const currentFileName = path11.basename(interview.markdownPath, ".md");
27765
+ const currentFileName = path12.basename(interview.markdownPath, ".md");
27321
27766
  if (currentFileName === newSlug) {
27322
27767
  return;
27323
27768
  }
27324
- const dir = path11.dirname(interview.markdownPath);
27325
- const newPath = path11.join(dir, `${newSlug}.md`);
27769
+ const dir = path12.dirname(interview.markdownPath);
27770
+ const newPath = path12.join(dir, `${newSlug}.md`);
27326
27771
  try {
27327
27772
  await fs8.access(newPath);
27328
27773
  return;
@@ -27398,9 +27843,9 @@ function createInterviewService(ctx, config, deps) {
27398
27843
  const messages = await loadMessages(sessionID);
27399
27844
  const title = extractTitle(document);
27400
27845
  const record = {
27401
- id: `${Date.now()}-${++idCounter}-${slugify(path11.basename(markdownPath, ".md")) || "interview"}`,
27846
+ id: `${Date.now()}-${++idCounter}-${slugify(path12.basename(markdownPath, ".md")) || "interview"}`,
27402
27847
  sessionID,
27403
- idea: title || path11.basename(markdownPath, ".md"),
27848
+ idea: title || path12.basename(markdownPath, ".md"),
27404
27849
  markdownPath,
27405
27850
  createdAt: nowIso(),
27406
27851
  status: "active",
@@ -27627,7 +28072,7 @@ function createInterviewService(ctx, config, deps) {
27627
28072
  return fileCache.items;
27628
28073
  }
27629
28074
  const outputDir = createInterviewDirectoryPath(ctx.directory, outputFolder);
27630
- const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path11.resolve(i.markdownPath)));
28075
+ const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path12.resolve(i.markdownPath)));
27631
28076
  let entries;
27632
28077
  try {
27633
28078
  entries = await fs8.readdir(outputDir);
@@ -27638,8 +28083,8 @@ function createInterviewService(ctx, config, deps) {
27638
28083
  for (const entry of entries) {
27639
28084
  if (!entry.endsWith(".md"))
27640
28085
  continue;
27641
- const fullPath = path11.join(outputDir, entry);
27642
- if (activePaths.has(path11.resolve(fullPath)))
28086
+ const fullPath = path12.join(outputDir, entry);
28087
+ if (activePaths.has(path12.resolve(fullPath)))
27643
28088
  continue;
27644
28089
  let content;
27645
28090
  try {
@@ -27738,7 +28183,7 @@ function createInterviewManager(ctx, config) {
27738
28183
  const outputFolder = interviewConfig?.outputFolder ?? "interview";
27739
28184
  if (!dashboardEnabled) {
27740
28185
  const service2 = createInterviewService(ctx, interviewConfig);
27741
- const resolvedOutputPath = path12.join(ctx.directory, outputFolder);
28186
+ const resolvedOutputPath = path13.join(ctx.directory, outputFolder);
27742
28187
  const server = createInterviewServer({
27743
28188
  getState: async (interviewId) => service2.getInterviewState(interviewId),
27744
28189
  listInterviewFiles: async () => service2.listInterviewFiles(),
@@ -27843,7 +28288,7 @@ function createInterviewManager(ctx, config) {
27843
28288
  listInterviews: () => service.listInterviews(),
27844
28289
  submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
27845
28290
  handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
27846
- outputFolder: path12.join(ctx.directory, outputFolder),
28291
+ outputFolder: path13.join(ctx.directory, outputFolder),
27847
28292
  port: 0
27848
28293
  });
27849
28294
  service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
@@ -28111,6 +28556,8 @@ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
28111
28556
  }
28112
28557
 
28113
28558
  // src/multiplexer/tmux/index.ts
28559
+ var TMUX_LAYOUT_DEBOUNCE_MS = 150;
28560
+
28114
28561
  class TmuxMultiplexer {
28115
28562
  type = "tmux";
28116
28563
  binaryPath = null;
@@ -28118,6 +28565,8 @@ class TmuxMultiplexer {
28118
28565
  storedLayout;
28119
28566
  storedMainPaneSize;
28120
28567
  targetPane = process.env.TMUX_PANE;
28568
+ layoutTimer;
28569
+ layoutGeneration = 0;
28121
28570
  constructor(layout = "main-vertical", mainPaneSize = 60) {
28122
28571
  this.storedLayout = layout;
28123
28572
  this.storedMainPaneSize = mainPaneSize;
@@ -28179,7 +28628,7 @@ class TmuxMultiplexer {
28179
28628
  if (exitCode === 0 && paneId) {
28180
28629
  const renameProc = crossSpawn([tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)], { stdout: "ignore", stderr: "ignore" });
28181
28630
  await renameProc.exited;
28182
- await this.applyLayout(this.storedLayout, this.storedMainPaneSize);
28631
+ this.scheduleLayout();
28183
28632
  log("[tmux] spawnPane: SUCCESS", { paneId });
28184
28633
  return { success: true, paneId };
28185
28634
  }
@@ -28216,7 +28665,7 @@ class TmuxMultiplexer {
28216
28665
  const stderr = await proc.stderr();
28217
28666
  log("[tmux] closePane: result", { exitCode, stderr: stderr.trim() });
28218
28667
  if (exitCode === 0) {
28219
- await this.applyLayout(this.storedLayout, this.storedMainPaneSize);
28668
+ this.scheduleLayout();
28220
28669
  return true;
28221
28670
  }
28222
28671
  log("[tmux] closePane: failed (pane may already be closed)", { paneId });
@@ -28227,41 +28676,82 @@ class TmuxMultiplexer {
28227
28676
  }
28228
28677
  }
28229
28678
  async applyLayout(layout, mainPaneSize) {
28679
+ if (this.layoutTimer) {
28680
+ clearTimeout(this.layoutTimer);
28681
+ this.layoutTimer = undefined;
28682
+ }
28683
+ this.layoutGeneration++;
28684
+ await this.applyLayoutNow(layout, mainPaneSize);
28685
+ }
28686
+ scheduleLayout() {
28687
+ if (this.layoutTimer)
28688
+ clearTimeout(this.layoutTimer);
28689
+ const gen = ++this.layoutGeneration;
28690
+ this.layoutTimer = setTimeout(() => {
28691
+ this.layoutTimer = undefined;
28692
+ if (this.layoutGeneration === gen) {
28693
+ this.applyLayoutNow(this.storedLayout, this.storedMainPaneSize);
28694
+ }
28695
+ }, TMUX_LAYOUT_DEBOUNCE_MS);
28696
+ this.layoutTimer.unref?.();
28697
+ }
28698
+ async applyLayoutNow(layout, mainPaneSize) {
28230
28699
  const tmux = await this.getBinary();
28231
28700
  if (!tmux)
28232
28701
  return;
28233
28702
  this.storedLayout = layout;
28234
28703
  this.storedMainPaneSize = mainPaneSize;
28235
28704
  try {
28236
- const layoutProc = crossSpawn([tmux, "select-layout", ...this.targetArgs(), layout], {
28237
- stdout: "pipe",
28238
- stderr: "pipe"
28239
- });
28240
- await layoutProc.exited;
28705
+ const layoutResult = await this.runTmux(tmux, [
28706
+ "select-layout",
28707
+ ...this.targetArgs(),
28708
+ layout
28709
+ ]);
28710
+ if (layoutResult !== 0)
28711
+ return;
28241
28712
  if (layout === "main-horizontal" || layout === "main-vertical") {
28242
28713
  const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
28243
- const sizeProc = crossSpawn([
28244
- tmux,
28714
+ const sizeResult = await this.runTmux(tmux, [
28245
28715
  "set-window-option",
28246
28716
  ...this.targetArgs(),
28247
28717
  sizeOption,
28248
28718
  `${mainPaneSize}%`
28249
- ], {
28250
- stdout: "pipe",
28251
- stderr: "pipe"
28252
- });
28253
- await sizeProc.exited;
28254
- const reapplyProc = crossSpawn([tmux, "select-layout", ...this.targetArgs(), layout], {
28255
- stdout: "pipe",
28256
- stderr: "pipe"
28257
- });
28258
- await reapplyProc.exited;
28719
+ ]);
28720
+ if (sizeResult !== 0)
28721
+ return;
28722
+ const reapplyResult = await this.runTmux(tmux, [
28723
+ "select-layout",
28724
+ ...this.targetArgs(),
28725
+ layout
28726
+ ]);
28727
+ if (reapplyResult !== 0)
28728
+ return;
28259
28729
  }
28260
28730
  log("[tmux] applyLayout: applied", { layout, mainPaneSize });
28261
28731
  } catch (err) {
28262
28732
  log("[tmux] applyLayout: exception", { error: String(err) });
28263
28733
  }
28264
28734
  }
28735
+ async runTmux(tmux, args) {
28736
+ const proc = crossSpawn([tmux, ...args], {
28737
+ stdout: "pipe",
28738
+ stderr: "pipe"
28739
+ });
28740
+ const [exitCode, , stderr] = await Promise.all([
28741
+ proc.exited,
28742
+ proc.stdout(),
28743
+ proc.stderr()
28744
+ ]);
28745
+ if (exitCode !== 0) {
28746
+ log("[tmux] command failed", {
28747
+ command: args[0],
28748
+ args: [tmux, ...args],
28749
+ exitCode,
28750
+ stderr: stderr.trim()
28751
+ });
28752
+ }
28753
+ return exitCode;
28754
+ }
28265
28755
  async getBinary() {
28266
28756
  await this.isAvailable();
28267
28757
  return this.binaryPath;
@@ -28283,23 +28773,23 @@ class TmuxMultiplexer {
28283
28773
  return null;
28284
28774
  }
28285
28775
  const stdout = await proc.stdout();
28286
- const path13 = stdout.trim().split(`
28776
+ const path14 = stdout.trim().split(`
28287
28777
  `)[0];
28288
- if (!path13) {
28778
+ if (!path14) {
28289
28779
  log("[tmux] findBinary: no path in output");
28290
28780
  return null;
28291
28781
  }
28292
- const verifyProc = crossSpawn([path13, "-V"], {
28782
+ const verifyProc = crossSpawn([path14, "-V"], {
28293
28783
  stdout: "pipe",
28294
28784
  stderr: "pipe"
28295
28785
  });
28296
28786
  const verifyExit = await verifyProc.exited;
28297
28787
  if (verifyExit !== 0) {
28298
- log("[tmux] findBinary: tmux -V failed", { path: path13, verifyExit });
28788
+ log("[tmux] findBinary: tmux -V failed", { path: path14, verifyExit });
28299
28789
  return null;
28300
28790
  }
28301
- log("[tmux] findBinary: found", { path: path13 });
28302
- return path13;
28791
+ log("[tmux] findBinary: found", { path: path14 });
28792
+ return path14;
28303
28793
  } catch (err) {
28304
28794
  log("[tmux] findBinary: exception", { error: String(err) });
28305
28795
  return null;
@@ -29010,17 +29500,17 @@ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
29010
29500
  import { tool as tool2 } from "@opencode-ai/plugin/tool";
29011
29501
 
29012
29502
  // src/tools/ast-grep/cli.ts
29013
- import { existsSync as existsSync8 } from "node:fs";
29503
+ import { existsSync as existsSync9 } from "node:fs";
29014
29504
 
29015
29505
  // src/tools/ast-grep/constants.ts
29016
- import { existsSync as existsSync7, statSync as statSync4 } from "node:fs";
29506
+ import { existsSync as existsSync8, statSync as statSync4 } from "node:fs";
29017
29507
  import { createRequire as createRequire3 } from "node:module";
29018
29508
  import { dirname as dirname6, join as join11 } from "node:path";
29019
29509
 
29020
29510
  // src/tools/ast-grep/downloader.ts
29021
- import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4 } from "node:fs";
29511
+ import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "node:fs";
29022
29512
  import { createRequire as createRequire2 } from "node:module";
29023
- import { homedir as homedir4 } from "node:os";
29513
+ import { homedir as homedir5 } from "node:os";
29024
29514
  import { join as join10 } from "node:path";
29025
29515
  var REPO = "ast-grep/ast-grep";
29026
29516
  var DEFAULT_VERSION = "0.40.0";
@@ -29045,11 +29535,11 @@ var PLATFORM_MAP = {
29045
29535
  function getCacheDir2() {
29046
29536
  if (process.platform === "win32") {
29047
29537
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
29048
- const base2 = localAppData || join10(homedir4(), "AppData", "Local");
29538
+ const base2 = localAppData || join10(homedir5(), "AppData", "Local");
29049
29539
  return join10(base2, "oh-my-opencode-slim", "bin");
29050
29540
  }
29051
29541
  const xdgCache = process.env.XDG_CACHE_HOME;
29052
- const base = xdgCache || join10(homedir4(), ".cache");
29542
+ const base = xdgCache || join10(homedir5(), ".cache");
29053
29543
  return join10(base, "oh-my-opencode-slim", "bin");
29054
29544
  }
29055
29545
  function getBinaryName() {
@@ -29057,7 +29547,7 @@ function getBinaryName() {
29057
29547
  }
29058
29548
  function getCachedBinaryPath() {
29059
29549
  const binaryPath = join10(getCacheDir2(), getBinaryName());
29060
- return existsSync6(binaryPath) ? binaryPath : null;
29550
+ return existsSync7(binaryPath) ? binaryPath : null;
29061
29551
  }
29062
29552
  async function downloadAstGrep(version = DEFAULT_VERSION) {
29063
29553
  const platformKey = `${process.platform}-${process.arch}`;
@@ -29069,16 +29559,16 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
29069
29559
  const cacheDir = getCacheDir2();
29070
29560
  const binaryName = getBinaryName();
29071
29561
  const binaryPath = join10(cacheDir, binaryName);
29072
- if (existsSync6(binaryPath)) {
29562
+ if (existsSync7(binaryPath)) {
29073
29563
  return binaryPath;
29074
29564
  }
29075
- const { arch, os: os4 } = platformInfo;
29076
- const assetName = `app-${arch}-${os4}.zip`;
29565
+ const { arch, os: os5 } = platformInfo;
29566
+ const assetName = `app-${arch}-${os5}.zip`;
29077
29567
  const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
29078
29568
  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
29079
29569
  try {
29080
- if (!existsSync6(cacheDir)) {
29081
- mkdirSync4(cacheDir, { recursive: true });
29570
+ if (!existsSync7(cacheDir)) {
29571
+ mkdirSync5(cacheDir, { recursive: true });
29082
29572
  }
29083
29573
  const response = await fetch(downloadUrl, { redirect: "follow" });
29084
29574
  if (!response.ok) {
@@ -29088,10 +29578,10 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
29088
29578
  const arrayBuffer = await response.arrayBuffer();
29089
29579
  await crossWrite(archivePath, arrayBuffer);
29090
29580
  await extractZip(archivePath, cacheDir);
29091
- if (existsSync6(archivePath)) {
29581
+ if (existsSync7(archivePath)) {
29092
29582
  unlinkSync4(archivePath);
29093
29583
  }
29094
- if (process.platform !== "win32" && existsSync6(binaryPath)) {
29584
+ if (process.platform !== "win32" && existsSync7(binaryPath)) {
29095
29585
  chmodSync(binaryPath, 493);
29096
29586
  }
29097
29587
  console.log(`[oh-my-opencode-slim] ast-grep binary ready.`);
@@ -29174,7 +29664,7 @@ function findSgCliPathSync() {
29174
29664
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
29175
29665
  const cliDir = dirname6(cliPkgPath);
29176
29666
  const sgPath = join11(cliDir, binaryName);
29177
- if (existsSync7(sgPath) && isValidBinary(sgPath)) {
29667
+ if (existsSync8(sgPath) && isValidBinary(sgPath)) {
29178
29668
  return sgPath;
29179
29669
  }
29180
29670
  } catch {}
@@ -29186,16 +29676,16 @@ function findSgCliPathSync() {
29186
29676
  const pkgDir = dirname6(pkgPath);
29187
29677
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
29188
29678
  const binaryPath = join11(pkgDir, astGrepName);
29189
- if (existsSync7(binaryPath) && isValidBinary(binaryPath)) {
29679
+ if (existsSync8(binaryPath) && isValidBinary(binaryPath)) {
29190
29680
  return binaryPath;
29191
29681
  }
29192
29682
  } catch {}
29193
29683
  }
29194
29684
  if (process.platform === "darwin") {
29195
29685
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
29196
- for (const path13 of homebrewPaths) {
29197
- if (existsSync7(path13) && isValidBinary(path13)) {
29198
- return path13;
29686
+ for (const path14 of homebrewPaths) {
29687
+ if (existsSync8(path14) && isValidBinary(path14)) {
29688
+ return path14;
29199
29689
  }
29200
29690
  }
29201
29691
  }
@@ -29212,8 +29702,8 @@ function getSgCliPath() {
29212
29702
  }
29213
29703
  return "sg";
29214
29704
  }
29215
- function setSgCliPath(path13) {
29216
- resolvedCliPath = path13;
29705
+ function setSgCliPath(path14) {
29706
+ resolvedCliPath = path14;
29217
29707
  }
29218
29708
  var DEFAULT_TIMEOUT_MS2 = 300000;
29219
29709
  var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
@@ -29223,7 +29713,7 @@ var DEFAULT_MAX_MATCHES = 500;
29223
29713
  var initPromise = null;
29224
29714
  async function getAstGrepPath() {
29225
29715
  const currentPath = getSgCliPath();
29226
- if (currentPath !== "sg" && existsSync8(currentPath)) {
29716
+ if (currentPath !== "sg" && existsSync9(currentPath)) {
29227
29717
  return currentPath;
29228
29718
  }
29229
29719
  if (initPromise) {
@@ -29231,7 +29721,7 @@ async function getAstGrepPath() {
29231
29721
  }
29232
29722
  initPromise = (async () => {
29233
29723
  const syncPath = findSgCliPathSync();
29234
- if (syncPath && existsSync8(syncPath)) {
29724
+ if (syncPath && existsSync9(syncPath)) {
29235
29725
  setSgCliPath(syncPath);
29236
29726
  return syncPath;
29237
29727
  }
@@ -29270,7 +29760,7 @@ async function runSg(options) {
29270
29760
  const paths2 = options.paths && options.paths.length > 0 ? options.paths : ["."];
29271
29761
  args.push(...paths2);
29272
29762
  let cliPath = getSgCliPath();
29273
- if (!existsSync8(cliPath) && cliPath !== "sg") {
29763
+ if (!existsSync9(cliPath) && cliPath !== "sg") {
29274
29764
  const downloadedPath = await getAstGrepPath();
29275
29765
  if (downloadedPath) {
29276
29766
  cliPath = downloadedPath;
@@ -29620,15 +30110,15 @@ Returns the councillor responses with a summary footer.`,
29620
30110
  }
29621
30111
  // src/tui-state.ts
29622
30112
  import * as fs9 from "node:fs";
29623
- import * as os4 from "node:os";
29624
- import * as path13 from "node:path";
30113
+ import * as os5 from "node:os";
30114
+ import * as path14 from "node:path";
29625
30115
  var STATE_DIR = "oh-my-opencode-slim";
29626
30116
  var STATE_FILE = "tui-state.json";
29627
30117
  function dataDir() {
29628
- return process.env.XDG_DATA_HOME ?? path13.join(os4.homedir(), ".local", "share");
30118
+ return process.env.XDG_DATA_HOME ?? path14.join(os5.homedir(), ".local", "share");
29629
30119
  }
29630
30120
  function getTuiStatePath() {
29631
- return path13.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
30121
+ return path14.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
29632
30122
  }
29633
30123
  function emptySnapshot() {
29634
30124
  return {
@@ -29664,7 +30154,7 @@ async function readTuiSnapshotAsync() {
29664
30154
  function writeTuiSnapshot(snapshot) {
29665
30155
  try {
29666
30156
  const filePath = getTuiStatePath();
29667
- fs9.mkdirSync(path13.dirname(filePath), { recursive: true });
30157
+ fs9.mkdirSync(path14.dirname(filePath), { recursive: true });
29668
30158
  fs9.writeFileSync(filePath, `${JSON.stringify(snapshot)}
29669
30159
  `);
29670
30160
  } catch {}
@@ -29880,15 +30370,15 @@ var BINARY_PREFIXES = [
29880
30370
  ];
29881
30371
  var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs pages. Supports llms.txt probing, content-focused HTML extraction, metadata, redirects, and an optional prompt processed by a cheap secondary model.";
29882
30372
  // src/tools/smartfetch/tool.ts
29883
- import os5 from "node:os";
29884
- import path17 from "node:path";
30373
+ import os6 from "node:os";
30374
+ import path18 from "node:path";
29885
30375
  import {
29886
30376
  tool as tool4
29887
30377
  } from "@opencode-ai/plugin";
29888
30378
 
29889
30379
  // src/tools/smartfetch/binary.ts
29890
30380
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
29891
- import path14 from "node:path";
30381
+ import path15 from "node:path";
29892
30382
  function extensionForMime(contentType) {
29893
30383
  const mime = contentType.split(";")[0]?.trim().toLowerCase();
29894
30384
  const map = {
@@ -29909,10 +30399,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
29909
30399
  async function saveBinary(binaryDir, data, contentType, filename) {
29910
30400
  await mkdir2(binaryDir, { recursive: true });
29911
30401
  const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
29912
- const parsed = path14.parse(initialName);
30402
+ const parsed = path15.parse(initialName);
29913
30403
  for (let attempt = 0;attempt < 1000; attempt++) {
29914
30404
  const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
29915
- const file = path14.join(binaryDir, candidateName);
30405
+ const file = path15.join(binaryDir, candidateName);
29916
30406
  try {
29917
30407
  await writeFile2(file, data, { flag: "wx" });
29918
30408
  return file;
@@ -30566,7 +31056,7 @@ var L = class u2 {
30566
31056
  };
30567
31057
 
30568
31058
  // src/tools/smartfetch/network.ts
30569
- import path15 from "node:path";
31059
+ import path16 from "node:path";
30570
31060
 
30571
31061
  // src/tools/smartfetch/utils.ts
30572
31062
  var import_readability = __toESM(require_readability(), 1);
@@ -31291,7 +31781,7 @@ function inferFilenameFromUrl(url) {
31291
31781
  function truncateFilename(name, maxLength = 180) {
31292
31782
  if (name.length <= maxLength)
31293
31783
  return name;
31294
- const parsed = path15.parse(name);
31784
+ const parsed = path16.parse(name);
31295
31785
  const ext = parsed.ext || "";
31296
31786
  const baseLimit = Math.max(1, maxLength - ext.length);
31297
31787
  return `${parsed.name.slice(0, baseLimit)}${ext}`;
@@ -31461,9 +31951,9 @@ function isInvalidLlmsResult(fetchResult) {
31461
31951
  }
31462
31952
 
31463
31953
  // src/tools/smartfetch/secondary-model.ts
31464
- import { existsSync as existsSync9 } from "node:fs";
31954
+ import { existsSync as existsSync10 } from "node:fs";
31465
31955
  import { readFile as readFile4 } from "node:fs/promises";
31466
- import path16 from "node:path";
31956
+ import path17 from "node:path";
31467
31957
  function parseModelRef(value) {
31468
31958
  if (!value)
31469
31959
  return;
@@ -31489,8 +31979,8 @@ function pickAgentModelRef(value) {
31489
31979
  }
31490
31980
  function findPreferredOpenCodeConfigPath(baseDir) {
31491
31981
  for (const file of ["opencode.jsonc", "opencode.json"]) {
31492
- const fullPath = path16.join(baseDir, file);
31493
- if (existsSync9(fullPath))
31982
+ const fullPath = path17.join(baseDir, file);
31983
+ if (existsSync10(fullPath))
31494
31984
  return fullPath;
31495
31985
  }
31496
31986
  return;
@@ -31506,7 +31996,7 @@ async function readOpenCodeConfigFile(configPath) {
31506
31996
  }
31507
31997
  }
31508
31998
  async function readEffectiveOpenCodeConfig(directory) {
31509
- const projectDir = path16.join(directory, ".opencode");
31999
+ const projectDir = path17.join(directory, ".opencode");
31510
32000
  const userDirs = getConfigSearchDirs();
31511
32001
  const projectPath = findPreferredOpenCodeConfigPath(projectDir);
31512
32002
  const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
@@ -31667,7 +32157,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
31667
32157
  // src/tools/smartfetch/tool.ts
31668
32158
  var z5 = tool4.schema;
31669
32159
  function createWebfetchTool(pluginCtx, options = {}) {
31670
- const binaryDir = options.binaryDir || path17.join(os5.tmpdir(), "opencode-smartfetch");
32160
+ const binaryDir = options.binaryDir || path18.join(os6.tmpdir(), "opencode-smartfetch");
31671
32161
  return tool4({
31672
32162
  description: WEBFETCH_DESCRIPTION,
31673
32163
  args: {
@@ -32148,6 +32638,460 @@ function createWebfetchTool(pluginCtx, options = {}) {
32148
32638
  }
32149
32639
  });
32150
32640
  }
32641
+ // src/tools/subtask/command.ts
32642
+ var COMMAND_NAME4 = "subtask";
32643
+ var SUBTASK_COMMAND_TEMPLATE = `Start a focused subtask worker.
32644
+
32645
+ The user's request below is the full scope for the worker. Do not broaden it.
32646
+ Create a self-contained worker prompt that includes:
32647
+ - the exact objective
32648
+ - relevant context from this conversation
32649
+ - specific files/paths that matter
32650
+ - expected deliverables
32651
+ - validation the worker should run, if applicable
32652
+
32653
+ USER REQUEST:
32654
+ $ARGUMENTS
32655
+
32656
+ Then call the subtask tool:
32657
+ \`subtask(prompt="...", files=["src/foo.ts", "docs/bar.md"])\`
32658
+
32659
+ Only include files that are clearly relevant. If no files are needed, omit files.`;
32660
+ function createSubtaskCommandManager(_ctx, state) {
32661
+ function registerCommand(opencodeConfig) {
32662
+ const configCommand = opencodeConfig.command;
32663
+ if (!configCommand?.[COMMAND_NAME4]) {
32664
+ if (!opencodeConfig.command) {
32665
+ opencodeConfig.command = {};
32666
+ }
32667
+ opencodeConfig.command[COMMAND_NAME4] = {
32668
+ description: "Create a focused subtask prompt for a new session",
32669
+ template: SUBTASK_COMMAND_TEMPLATE
32670
+ };
32671
+ }
32672
+ }
32673
+ return {
32674
+ registerCommand,
32675
+ handleEvent(input) {
32676
+ if (input.event.type === "session.created") {
32677
+ const info = input.event.properties?.info;
32678
+ if (!info?.id || !info.parentID)
32679
+ return;
32680
+ const source = state.sourceFor(info.parentID);
32681
+ if (source)
32682
+ state.markSession(info.id, source);
32683
+ return;
32684
+ }
32685
+ if (input.event.type !== "session.deleted")
32686
+ return;
32687
+ const sessionID = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
32688
+ if (sessionID)
32689
+ state.unmarkSession(sessionID);
32690
+ }
32691
+ };
32692
+ }
32693
+ // src/tools/subtask/files.ts
32694
+ import * as fs11 from "node:fs/promises";
32695
+ import * as path20 from "node:path";
32696
+
32697
+ // src/tools/subtask/vendor.ts
32698
+ import * as fs10 from "node:fs/promises";
32699
+ import * as path19 from "node:path";
32700
+ var DEFAULT_READ_LIMIT = 2000;
32701
+ var MAX_LINE_LENGTH = 2000;
32702
+ var MAX_BYTES = 50 * 1024;
32703
+ var MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`;
32704
+ var MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`;
32705
+ var SAMPLE_BYTES = 4096;
32706
+ var BINARY_EXTENSIONS = new Set([
32707
+ ".zip",
32708
+ ".tar",
32709
+ ".gz",
32710
+ ".exe",
32711
+ ".dll",
32712
+ ".so",
32713
+ ".class",
32714
+ ".jar",
32715
+ ".war",
32716
+ ".7z",
32717
+ ".doc",
32718
+ ".docx",
32719
+ ".xls",
32720
+ ".xlsx",
32721
+ ".ppt",
32722
+ ".pptx",
32723
+ ".odt",
32724
+ ".ods",
32725
+ ".odp",
32726
+ ".bin",
32727
+ ".dat",
32728
+ ".obj",
32729
+ ".o",
32730
+ ".a",
32731
+ ".lib",
32732
+ ".wasm",
32733
+ ".pyc",
32734
+ ".pyo"
32735
+ ]);
32736
+ async function isBinaryFile(filepath) {
32737
+ const ext = path19.extname(filepath).toLowerCase();
32738
+ if (BINARY_EXTENSIONS.has(ext)) {
32739
+ return true;
32740
+ }
32741
+ try {
32742
+ const file = await fs10.open(filepath, "r");
32743
+ try {
32744
+ const buffer = Buffer.alloc(SAMPLE_BYTES);
32745
+ const result = await file.read(buffer, 0, SAMPLE_BYTES, 0);
32746
+ if (result.bytesRead === 0)
32747
+ return false;
32748
+ const bytes = buffer.subarray(0, result.bytesRead);
32749
+ let nonPrintableCount = 0;
32750
+ for (let i = 0;i < bytes.length; i++) {
32751
+ const byte = bytes[i];
32752
+ if (byte === undefined)
32753
+ continue;
32754
+ if (byte === 0)
32755
+ return true;
32756
+ if (byte < 9 || byte > 13 && byte < 32) {
32757
+ nonPrintableCount++;
32758
+ }
32759
+ }
32760
+ return nonPrintableCount / bytes.length > 0.3;
32761
+ } finally {
32762
+ await file.close();
32763
+ }
32764
+ } catch {
32765
+ return false;
32766
+ }
32767
+ }
32768
+ function formatFileContent(_filepath, content) {
32769
+ const cappedContent = Buffer.byteLength(content, "utf8") > MAX_BYTES;
32770
+ const contentToFormat = cappedContent ? content.slice(0, MAX_BYTES) : content;
32771
+ const lines = contentToFormat.split(`
32772
+ `);
32773
+ const limit = DEFAULT_READ_LIMIT;
32774
+ const offset = 0;
32775
+ const raw = lines.slice(offset, offset + limit).map((line) => {
32776
+ return line.length > MAX_LINE_LENGTH ? `${line.substring(0, MAX_LINE_LENGTH)}${MAX_LINE_SUFFIX}` : line;
32777
+ });
32778
+ const formatted = raw.map((line, index) => {
32779
+ return `${index + offset + 1}: ${line}`;
32780
+ });
32781
+ let output = [
32782
+ `<path>${_filepath}</path>`,
32783
+ "<type>file</type>",
32784
+ `<content>
32785
+ `
32786
+ ].join(`
32787
+ `);
32788
+ output += formatted.join(`
32789
+ `);
32790
+ const totalLines = lines.length;
32791
+ const lastReadLine = offset + formatted.length;
32792
+ const hasMoreLines = totalLines > lastReadLine;
32793
+ if (cappedContent) {
32794
+ output += `
32795
+
32796
+ (Output capped at ${MAX_BYTES_LABEL}. Showing lines 1-${lastReadLine}. Use offset=${lastReadLine + 1} to continue.)`;
32797
+ } else if (hasMoreLines) {
32798
+ output += `
32799
+
32800
+ (Showing lines 1-${lastReadLine} of ${totalLines}. Use offset=${lastReadLine + 1} to continue.)`;
32801
+ } else {
32802
+ output += `
32803
+
32804
+ (End of file - total ${totalLines} lines)`;
32805
+ }
32806
+ output += `
32807
+ </content>`;
32808
+ return output;
32809
+ }
32810
+
32811
+ // src/tools/subtask/files.ts
32812
+ var FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g;
32813
+ var TRAILING_PATH_PUNCTUATION = /[!?:;]+$/;
32814
+ function cleanFileReference(ref) {
32815
+ return ref.replace(/^@/, "").replace(TRAILING_PATH_PUNCTUATION, "");
32816
+ }
32817
+ function parseFileReferences(text) {
32818
+ const fileRefs = new Set;
32819
+ for (const match of text.matchAll(FILE_REGEX)) {
32820
+ if (match[1]) {
32821
+ fileRefs.add(cleanFileReference(match[1]));
32822
+ }
32823
+ }
32824
+ return fileRefs;
32825
+ }
32826
+ async function buildSyntheticFileParts(directory, refs) {
32827
+ const parts = [];
32828
+ const realDirectory = await fs11.realpath(directory);
32829
+ for (const ref of refs) {
32830
+ const filepath = path20.resolve(directory, ref);
32831
+ const relative3 = path20.relative(directory, filepath);
32832
+ if (relative3.startsWith("..") || path20.isAbsolute(relative3))
32833
+ continue;
32834
+ try {
32835
+ const realFilepath = await fs11.realpath(filepath);
32836
+ const realRelative = path20.relative(realDirectory, realFilepath);
32837
+ if (realRelative.startsWith("..") || path20.isAbsolute(realRelative)) {
32838
+ continue;
32839
+ }
32840
+ const stats = await fs11.stat(realFilepath);
32841
+ if (!stats.isFile())
32842
+ continue;
32843
+ if (await isBinaryFile(realFilepath))
32844
+ continue;
32845
+ const content = await fs11.readFile(realFilepath, "utf-8");
32846
+ parts.push({
32847
+ type: "text",
32848
+ synthetic: true,
32849
+ text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: realFilepath })}`
32850
+ });
32851
+ parts.push({
32852
+ type: "text",
32853
+ synthetic: true,
32854
+ text: formatFileContent(realFilepath, content)
32855
+ });
32856
+ } catch {}
32857
+ }
32858
+ return parts;
32859
+ }
32860
+ // src/tools/subtask/state.ts
32861
+ function createSubtaskState() {
32862
+ const sourceBySession = new Map;
32863
+ return {
32864
+ markSession(sessionID, sourceSessionID) {
32865
+ sourceBySession.set(sessionID, sourceSessionID);
32866
+ },
32867
+ unmarkSession(sessionID) {
32868
+ sourceBySession.delete(sessionID);
32869
+ },
32870
+ isSubtaskSession(sessionID) {
32871
+ return sourceBySession.has(sessionID);
32872
+ },
32873
+ sourceFor(sessionID) {
32874
+ return sourceBySession.get(sessionID);
32875
+ }
32876
+ };
32877
+ }
32878
+ // src/tools/subtask/tools.ts
32879
+ import { tool as tool5 } from "@opencode-ai/plugin";
32880
+ var SUBTASK_TIMEOUT_MS = 5 * 60 * 1000;
32881
+ var SUBTASK_SUMMARY_TAG_REGEX = /<\/?subtask_summary>/g;
32882
+ function normalizeSubtaskSummary(text) {
32883
+ return text.replace(SUBTASK_SUMMARY_TAG_REGEX, "").trim();
32884
+ }
32885
+ function getAbortSignal(context) {
32886
+ if (!context || typeof context !== "object" || !("abort" in context)) {
32887
+ return;
32888
+ }
32889
+ const signal = context.abort;
32890
+ return signal && typeof signal === "object" && "addEventListener" in signal && "removeEventListener" in signal && "aborted" in signal ? signal : undefined;
32891
+ }
32892
+ function createSubtaskTool(ctx, state, depthTracker) {
32893
+ const client = ctx.client;
32894
+ return tool5({
32895
+ description: "Run a child worker session and return its completion summary to the caller",
32896
+ args: {
32897
+ prompt: tool5.schema.string().describe("The generated subtask prompt"),
32898
+ files: tool5.schema.array(tool5.schema.string()).optional().describe("Array of file paths to load into the new session's context")
32899
+ },
32900
+ async execute(args, context) {
32901
+ const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : ctx.directory;
32902
+ const sessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : "unknown";
32903
+ const abortSignal = getAbortSignal(context);
32904
+ if (state.isSubtaskSession(sessionID)) {
32905
+ return "Nested subtask is disabled: this session is already a subtask worker. Finish this worker and return its summary to the parent session instead.";
32906
+ }
32907
+ if (sessionID !== "unknown" && depthTracker && depthTracker.getDepth(sessionID) + 1 > depthTracker.maxDepth) {
32908
+ return `Subtask worker blocked: max subagent depth ${depthTracker.maxDepth} would be exceeded.`;
32909
+ }
32910
+ const sessionReference = `You are a subtask worker spawned by parent session ${sessionID}.
32911
+
32912
+ Your job is bounded: complete only the task below. Do not expand scope.
32913
+ If needed context is missing, use read_session to inspect the parent session.
32914
+ Do not spawn another subtask.`;
32915
+ const files = new Set([
32916
+ ...parseFileReferences(args.prompt),
32917
+ ...(args.files ?? []).map(cleanFileReference)
32918
+ ]);
32919
+ const fileRefs = files.size > 0 ? [...files].map((f) => `@${f}`).join(" ") : "";
32920
+ const fullPrompt = fileRefs ? `${sessionReference}
32921
+
32922
+ TASK:
32923
+ ${args.prompt}
32924
+
32925
+ FILES PROVIDED:
32926
+ ${fileRefs}` : `${sessionReference}
32927
+
32928
+ TASK:
32929
+ ${args.prompt}`;
32930
+ let childSessionID;
32931
+ try {
32932
+ const session2 = await client.session.create({
32933
+ responseStyle: "data",
32934
+ throwOnError: true,
32935
+ query: { directory },
32936
+ body: {
32937
+ parentID: sessionID === "unknown" ? undefined : sessionID,
32938
+ title: `Subtask worker from ${sessionID}`
32939
+ }
32940
+ });
32941
+ childSessionID = session2?.data?.id ?? session2?.id;
32942
+ if (!childSessionID) {
32943
+ throw new Error("Subtask worker session did not return an id");
32944
+ }
32945
+ if (sessionID !== "unknown" && depthTracker) {
32946
+ const registered = depthTracker.registerChild(sessionID, childSessionID);
32947
+ if (!registered) {
32948
+ throw new Error("Subtask worker blocked: max subagent depth exceeded");
32949
+ }
32950
+ }
32951
+ state.markSession(childSessionID, sessionID);
32952
+ await promptWithTimeout(client, {
32953
+ responseStyle: "data",
32954
+ throwOnError: true,
32955
+ query: { directory },
32956
+ path: { id: childSessionID },
32957
+ body: {
32958
+ agent: "orchestrator",
32959
+ parts: [
32960
+ {
32961
+ type: "text",
32962
+ text: `${fullPrompt}
32963
+
32964
+ Instructions:
32965
+ 1. Understand the task and relevant file context.
32966
+ 2. Make only necessary changes.
32967
+ 3. Run the most relevant validation checks when practical.
32968
+ 4. Stop when the requested task is done.
32969
+
32970
+ Return your final response in this format:
32971
+
32972
+ <subtask_summary>
32973
+ Status: completed | blocked | partial
32974
+
32975
+ What changed:
32976
+ - ...
32977
+
32978
+ Files touched:
32979
+ - ...
32980
+
32981
+ Validation:
32982
+ - ...
32983
+
32984
+ Risks / follow-up:
32985
+ - ...
32986
+ </subtask_summary>`
32987
+ },
32988
+ ...await buildSyntheticFileParts(directory, files)
32989
+ ]
32990
+ }
32991
+ }, SUBTASK_TIMEOUT_MS, abortSignal);
32992
+ const extraction = await extractSessionResult(client, childSessionID, {
32993
+ directory,
32994
+ includeReasoning: false
32995
+ });
32996
+ if (extraction.empty) {
32997
+ throw new Error("Subtask worker returned no summary");
32998
+ }
32999
+ const summary = normalizeSubtaskSummary(extraction.text);
33000
+ return [
33001
+ `task_id: ${childSessionID}`,
33002
+ "",
33003
+ "<subtask_summary>",
33004
+ summary,
33005
+ "</subtask_summary>"
33006
+ ].join(`
33007
+ `);
33008
+ } finally {
33009
+ if (childSessionID) {
33010
+ try {
33011
+ await client.session.abort({
33012
+ path: { id: childSessionID },
33013
+ query: { directory }
33014
+ });
33015
+ state.unmarkSession(childSessionID);
33016
+ } catch {}
33017
+ }
33018
+ }
33019
+ }
33020
+ });
33021
+ }
33022
+ function formatTranscript(messages, limit) {
33023
+ const lines = [];
33024
+ for (const msg of messages) {
33025
+ const role = msg.info?.role;
33026
+ const parts = msg.parts;
33027
+ if (role === "user") {
33028
+ lines.push("## User");
33029
+ for (const part of parts) {
33030
+ if (part.type === "text" && !part.ignored && typeof part.text === "string") {
33031
+ lines.push(part.text);
33032
+ }
33033
+ if (part.type === "file") {
33034
+ lines.push(`[Attached: ${part.filename || "file"}]`);
33035
+ }
33036
+ }
33037
+ lines.push("");
33038
+ }
33039
+ if (role === "assistant") {
33040
+ lines.push("## Assistant");
33041
+ for (const part of parts) {
33042
+ if (part.type === "text" && typeof part.text === "string") {
33043
+ lines.push(part.text);
33044
+ }
33045
+ if (part.type === "tool" && part.state?.status === "completed" && part.tool) {
33046
+ lines.push(`[Tool: ${part.tool}] ${part.state.title ?? ""}`);
33047
+ }
33048
+ }
33049
+ lines.push("");
33050
+ }
33051
+ }
33052
+ const output = lines.join(`
33053
+ `).trim();
33054
+ if (messages.length >= (limit ?? 100)) {
33055
+ return output + `
33056
+
33057
+ (Showing ${messages.length} most recent messages. Use a higher 'limit' to see more.)`;
33058
+ }
33059
+ return `${output}
33060
+
33061
+ (End of session - ${messages.length} messages)`;
33062
+ }
33063
+ function createReadSessionTool(client, state) {
33064
+ return tool5({
33065
+ description: "Read the conversation transcript from a previous session. Use this when you need specific information from the source session that wasn't included in the subtask summary.",
33066
+ args: {
33067
+ sessionID: tool5.schema.string().describe("The full session ID (e.g., sess_01jxyz...)"),
33068
+ limit: tool5.schema.number().optional().describe("Maximum number of messages to read (defaults to 100, max 500)")
33069
+ },
33070
+ async execute(args, context) {
33071
+ const limit = Math.min(args.limit ?? 100, 500);
33072
+ const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : undefined;
33073
+ const callerSessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : undefined;
33074
+ if (!callerSessionID || !state.isSubtaskSession(callerSessionID)) {
33075
+ return "read_session is only available from subtask worker sessions.";
33076
+ }
33077
+ if (state.sourceFor(callerSessionID) !== args.sessionID) {
33078
+ return "read_session can only read the source session for this subtask worker.";
33079
+ }
33080
+ try {
33081
+ const response = await client.session.messages({
33082
+ path: { id: args.sessionID },
33083
+ query: { limit, ...directory ? { directory } : {} }
33084
+ });
33085
+ if (!response.data || response.data.length === 0) {
33086
+ return "Session has no messages or does not exist.";
33087
+ }
33088
+ return formatTranscript(response.data, limit);
33089
+ } catch (error) {
33090
+ return `Could not read session ${args.sessionID}: ${error instanceof Error ? error.message : "Unknown error"}`;
33091
+ }
33092
+ }
33093
+ });
33094
+ }
32151
33095
  // src/utils/subagent-depth.ts
32152
33096
  class SubagentDepthTracker {
32153
33097
  depthBySession = new Map;
@@ -32263,9 +33207,12 @@ var OhMyOpenCodeLite = async (ctx) => {
32263
33207
  let taskSessionManagerHook;
32264
33208
  let interviewManager;
32265
33209
  let presetManager;
33210
+ let divoomManager;
32266
33211
  let councilTools;
32267
33212
  let webfetch;
32268
33213
  let rewriteDisplayNameMentions;
33214
+ let subtaskCommandManager;
33215
+ let subtaskState;
32269
33216
  let toolCount = 0;
32270
33217
  try {
32271
33218
  config = loadPluginConfig(ctx.directory);
@@ -32357,7 +33304,10 @@ var OhMyOpenCodeLite = async (ctx) => {
32357
33304
  });
32358
33305
  interviewManager = createInterviewManager(ctx, config);
32359
33306
  presetManager = createPresetManager(ctx, config);
32360
- toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
33307
+ divoomManager = createDivoomManager(config.divoom);
33308
+ subtaskState = createSubtaskState();
33309
+ subtaskCommandManager = createSubtaskCommandManager(ctx, subtaskState);
33310
+ toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2 + 2;
32361
33311
  } catch (err) {
32362
33312
  log("[plugin] FATAL: init failed", String(err));
32363
33313
  await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
@@ -32393,6 +33343,7 @@ var OhMyOpenCodeLite = async (ctx) => {
32393
33343
  appLog(ctx, "warn", msg).catch(() => {});
32394
33344
  }
32395
33345
  });
33346
+ divoomManager.onPluginLoad();
32396
33347
  return {
32397
33348
  name: "oh-my-opencode-slim",
32398
33349
  agent: agents,
@@ -32401,7 +33352,9 @@ var OhMyOpenCodeLite = async (ctx) => {
32401
33352
  webfetch,
32402
33353
  ...todoContinuationHook.tool,
32403
33354
  ast_grep_search,
32404
- ast_grep_replace
33355
+ ast_grep_replace,
33356
+ subtask: createSubtaskTool(ctx, subtaskState, depthTracker),
33357
+ read_session: createReadSessionTool(ctx.client, subtaskState)
32405
33358
  },
32406
33359
  mcp: mcps,
32407
33360
  config: async (opencodeConfig) => {
@@ -32598,6 +33551,7 @@ var OhMyOpenCodeLite = async (ctx) => {
32598
33551
  }
32599
33552
  interviewManager.registerCommand(opencodeConfig);
32600
33553
  presetManager.registerCommand(opencodeConfig);
33554
+ subtaskCommandManager.registerCommand(opencodeConfig);
32601
33555
  },
32602
33556
  event: async (input) => {
32603
33557
  const event = input.event;
@@ -32625,6 +33579,38 @@ var OhMyOpenCodeLite = async (ctx) => {
32625
33579
  await multiplexerSessionManager.onSessionDeleted(event);
32626
33580
  await interviewManager.handleEvent(input);
32627
33581
  await taskSessionManagerHook.event(input);
33582
+ subtaskCommandManager.handleEvent(input);
33583
+ if (event.type === "permission.asked" || event.type === "question.asked") {
33584
+ const props = event.properties;
33585
+ divoomManager.onUserInputRequired({
33586
+ sessionId: props?.sessionID,
33587
+ requestId: props?.id ?? props?.requestID
33588
+ });
33589
+ }
33590
+ if (event.type === "permission.replied" || event.type === "question.replied" || event.type === "question.rejected") {
33591
+ const props = event.properties;
33592
+ divoomManager.onUserInputResolved({
33593
+ sessionId: props?.sessionID,
33594
+ requestId: props?.requestID ?? props?.id
33595
+ });
33596
+ }
33597
+ if (input.event.type === "session.status") {
33598
+ const props = input.event.properties;
33599
+ const sessionID = props?.sessionID;
33600
+ divoomManager.onOrchestratorStatus({
33601
+ sessionId: sessionID,
33602
+ status: props?.status?.type,
33603
+ isOrchestrator: sessionID ? sessionAgentMap.get(sessionID) === "orchestrator" : false
33604
+ });
33605
+ }
33606
+ if (input.event.type === "session.deleted") {
33607
+ const props = input.event.properties;
33608
+ const sessionID = props?.info?.id ?? props?.sessionID;
33609
+ divoomManager.onSessionDeleted({
33610
+ sessionId: sessionID,
33611
+ isOrchestrator: sessionID ? sessionAgentMap.get(sessionID) === "orchestrator" : false
33612
+ });
33613
+ }
32628
33614
  if (input.event.type === "session.deleted") {
32629
33615
  const props = input.event.properties;
32630
33616
  const sessionID = props?.info?.id ?? props?.sessionID;
@@ -32639,6 +33625,13 @@ var OhMyOpenCodeLite = async (ctx) => {
32639
33625
  "tool.execute.before": async (input, output) => {
32640
33626
  await applyPatchHook["tool.execute.before"](input, output);
32641
33627
  await taskSessionManagerHook["tool.execute.before"](input, output);
33628
+ if (input.tool.toLowerCase() === "task") {
33629
+ divoomManager.onTaskStart({
33630
+ parentSessionId: input.sessionID,
33631
+ callId: input.callID,
33632
+ args: output.args
33633
+ });
33634
+ }
32642
33635
  },
32643
33636
  "command.execute.before": async (input, output) => {
32644
33637
  await todoContinuationHook.handleCommandExecuteBefore(input, output);
@@ -32701,11 +33694,31 @@ ${output.system[0]}` : "");
32701
33694
  await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
32702
33695
  },
32703
33696
  "tool.execute.after": async (input, output) => {
32704
- await delegateTaskRetryHook["tool.execute.after"](input, output);
32705
- await jsonErrorRecoveryHook["tool.execute.after"](input, output);
32706
- await todoContinuationHook.handleToolExecuteAfter(input, output);
32707
- await postFileToolNudgeHook["tool.execute.after"](input, output);
32708
- await taskSessionManagerHook["tool.execute.after"](input, output);
33697
+ const meta = input;
33698
+ const runPostToolHook = async (name, fn) => {
33699
+ try {
33700
+ await fn();
33701
+ } catch (error) {
33702
+ log("[plugin] post-tool hook failed open", {
33703
+ hook: name,
33704
+ tool: meta.tool,
33705
+ sessionID: meta.sessionID,
33706
+ callID: meta.callID,
33707
+ error: error instanceof Error ? error.message : String(error)
33708
+ });
33709
+ }
33710
+ };
33711
+ await runPostToolHook("delegate-task-retry", () => delegateTaskRetryHook["tool.execute.after"](input, output));
33712
+ await runPostToolHook("json-error-recovery", () => jsonErrorRecoveryHook["tool.execute.after"](input, output));
33713
+ await runPostToolHook("todo-continuation", () => todoContinuationHook.handleToolExecuteAfter(input, output));
33714
+ await runPostToolHook("post-file-tool-nudge", () => postFileToolNudgeHook["tool.execute.after"](input, output));
33715
+ await runPostToolHook("task-session-manager", () => taskSessionManagerHook["tool.execute.after"](input, output));
33716
+ if (input.tool.toLowerCase() === "task") {
33717
+ divoomManager.onTaskEnd({
33718
+ parentSessionId: input.sessionID,
33719
+ callId: input.callID
33720
+ });
33721
+ }
32709
33722
  }
32710
33723
  };
32711
33724
  };