oh-my-opencode-slim 2.0.1 → 2.0.3

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 (49) hide show
  1. package/README.ja-JP.md +31 -1
  2. package/README.ko-KR.md +31 -1
  3. package/README.md +41 -2
  4. package/README.zh-CN.md +31 -1
  5. package/dist/agents/orchestrator.d.ts +0 -2
  6. package/dist/cli/companion.d.ts +2 -2
  7. package/dist/cli/index.js +326 -89
  8. package/dist/companion/manager.d.ts +1 -0
  9. package/dist/companion/updater.d.ts +36 -0
  10. package/dist/config/agent-mcps.d.ts +0 -4
  11. package/dist/config/constants.d.ts +1 -7
  12. package/dist/config/council-schema.d.ts +0 -15
  13. package/dist/config/index.d.ts +1 -1
  14. package/dist/config/runtime-preset.d.ts +0 -1
  15. package/dist/config/schema.d.ts +78 -68
  16. package/dist/config/utils.d.ts +1 -0
  17. package/dist/hooks/auto-update-checker/skill-sync.d.ts +9 -0
  18. package/dist/hooks/auto-update-checker/types.d.ts +2 -0
  19. package/dist/hooks/filter-available-skills/index.d.ts +1 -13
  20. package/dist/hooks/foreground-fallback/index.d.ts +1 -1
  21. package/dist/hooks/image-hook.d.ts +1 -13
  22. package/dist/hooks/index.d.ts +3 -2
  23. package/dist/hooks/phase-reminder/index.d.ts +10 -16
  24. package/dist/hooks/reflect/index.d.ts +13 -0
  25. package/dist/hooks/task-session-manager/index.d.ts +2 -16
  26. package/dist/hooks/types.d.ts +23 -0
  27. package/dist/index.js +1610 -585
  28. package/dist/tools/acp-run.d.ts +3 -0
  29. package/dist/tools/index.d.ts +1 -0
  30. package/dist/tools/smartfetch/secondary-model.d.ts +7 -0
  31. package/dist/tui.js +114 -76
  32. package/dist/utils/agent-variant.d.ts +0 -40
  33. package/dist/utils/compat.d.ts +0 -1
  34. package/dist/utils/guards.d.ts +4 -0
  35. package/dist/utils/index.d.ts +1 -2
  36. package/dist/utils/logger.d.ts +1 -1
  37. package/dist/utils/task.d.ts +0 -2
  38. package/oh-my-opencode-slim.schema.json +103 -249
  39. package/package.json +2 -1
  40. package/src/companion/companion-manifest.json +12 -0
  41. package/src/skills/codemap.md +4 -1
  42. package/src/skills/reflect/SKILL.md +193 -0
  43. package/src/skills/worktrees/SKILL.md +164 -0
  44. package/dist/config/fallback-chains.d.ts +0 -1
  45. package/dist/hooks/apply-patch/patch.d.ts +0 -2
  46. package/dist/hooks/delegate-task-retry/guidance.d.ts +0 -2
  47. package/dist/hooks/delegate-task-retry/index.d.ts +0 -4
  48. package/dist/hooks/json-error-recovery/index.d.ts +0 -1
  49. package/dist/utils/env.d.ts +0 -1
package/dist/index.js CHANGED
@@ -30,8 +30,160 @@ var __toESM = (mod, isNodeMode, target) => {
30
30
  return to;
31
31
  };
32
32
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
+ var __returnValue = (v) => v;
34
+ function __exportSetter(name, newValue) {
35
+ this[name] = __returnValue.bind(null, newValue);
36
+ }
37
+ var __export = (target, all) => {
38
+ for (var name in all)
39
+ __defProp(target, name, {
40
+ get: all[name],
41
+ enumerable: true,
42
+ configurable: true,
43
+ set: __exportSetter.bind(all, name)
44
+ });
45
+ };
46
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
33
47
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
34
48
 
49
+ // src/utils/compat.ts
50
+ import { spawn as nodeSpawn } from "node:child_process";
51
+ import { writeFile as fsWriteFile } from "node:fs/promises";
52
+ function collectStream(stream) {
53
+ if (!stream)
54
+ return () => Promise.resolve("");
55
+ const chunks = [];
56
+ stream.on("data", (chunk) => chunks.push(chunk));
57
+ return () => new Promise((resolve, reject) => {
58
+ if (!stream.readable) {
59
+ resolve(Buffer.concat(chunks).toString("utf-8"));
60
+ return;
61
+ }
62
+ stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
63
+ stream.on("error", reject);
64
+ });
65
+ }
66
+ function crossSpawn(command, options) {
67
+ const [cmd, ...args] = command;
68
+ const proc = nodeSpawn(cmd, args, {
69
+ stdio: [
70
+ options?.stdin ?? "ignore",
71
+ options?.stdout ?? "pipe",
72
+ options?.stderr ?? "pipe"
73
+ ],
74
+ cwd: options?.cwd,
75
+ env: options?.env
76
+ });
77
+ const stdoutCollector = collectStream(proc.stdout);
78
+ const stderrCollector = collectStream(proc.stderr);
79
+ const exited = new Promise((resolve, reject) => {
80
+ proc.on("error", reject);
81
+ proc.on("close", (code) => resolve(code ?? 1));
82
+ });
83
+ return {
84
+ proc,
85
+ stdout: stdoutCollector,
86
+ stderr: stderrCollector,
87
+ exited,
88
+ kill: (signal) => proc.kill(signal),
89
+ get exitCode() {
90
+ return proc.exitCode;
91
+ }
92
+ };
93
+ }
94
+ async function crossWrite(path, data) {
95
+ await fsWriteFile(path, Buffer.from(data));
96
+ }
97
+ var init_compat = () => {};
98
+
99
+ // src/utils/zip-extractor.ts
100
+ var exports_zip_extractor = {};
101
+ __export(exports_zip_extractor, {
102
+ extractZip: () => extractZip
103
+ });
104
+ import { spawnSync } from "node:child_process";
105
+ import { release } from "node:os";
106
+ function getWindowsBuildNumber() {
107
+ if (process.platform !== "win32")
108
+ return null;
109
+ const parts = release().split(".");
110
+ if (parts.length >= 3) {
111
+ const build = parseInt(parts[2], 10);
112
+ if (!Number.isNaN(build))
113
+ return build;
114
+ }
115
+ return null;
116
+ }
117
+ function isPwshAvailable() {
118
+ if (process.platform !== "win32")
119
+ return false;
120
+ const result = spawnSync("where", ["pwsh"], {
121
+ stdio: ["ignore", "pipe", "pipe"]
122
+ });
123
+ return result.status === 0;
124
+ }
125
+ function escapePowerShellPath(path4) {
126
+ return path4.replace(/'/g, "''");
127
+ }
128
+ function getWindowsZipExtractor() {
129
+ const buildNumber = getWindowsBuildNumber();
130
+ if (buildNumber !== null && buildNumber >= WINDOWS_BUILD_WITH_TAR) {
131
+ return "tar";
132
+ }
133
+ if (isPwshAvailable()) {
134
+ return "pwsh";
135
+ }
136
+ return "powershell";
137
+ }
138
+ async function extractZip(archivePath, destDir) {
139
+ let proc;
140
+ if (process.platform === "win32") {
141
+ const extractor = getWindowsZipExtractor();
142
+ switch (extractor) {
143
+ case "tar":
144
+ proc = crossSpawn(["tar", "-xf", archivePath, "-C", destDir], {
145
+ stdout: "ignore",
146
+ stderr: "pipe"
147
+ });
148
+ break;
149
+ case "pwsh":
150
+ proc = crossSpawn([
151
+ "pwsh",
152
+ "-Command",
153
+ `Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
154
+ ], {
155
+ stdout: "ignore",
156
+ stderr: "pipe"
157
+ });
158
+ break;
159
+ default:
160
+ proc = crossSpawn([
161
+ "powershell",
162
+ "-Command",
163
+ `Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
164
+ ], {
165
+ stdout: "ignore",
166
+ stderr: "pipe"
167
+ });
168
+ break;
169
+ }
170
+ } else {
171
+ proc = crossSpawn(["unzip", "-o", archivePath, "-d", destDir], {
172
+ stdout: "ignore",
173
+ stderr: "pipe"
174
+ });
175
+ }
176
+ const exitCode = await proc.exited;
177
+ if (exitCode !== 0) {
178
+ const stderr = await proc.stderr();
179
+ throw new Error(`zip extraction failed (exit ${exitCode}): ${stderr}`);
180
+ }
181
+ }
182
+ var WINDOWS_BUILD_WITH_TAR = 17134;
183
+ var init_zip_extractor = __esm(() => {
184
+ init_compat();
185
+ });
186
+
35
187
  // node_modules/.pnpm/@mozilla+readability@0.6.0/node_modules/@mozilla/readability/Readability.js
36
188
  var require_Readability = __commonJS((exports, module) => {
37
189
  function Readability(doc, options) {
@@ -6246,33 +6398,33 @@ var require_URL = __commonJS((exports, module) => {
6246
6398
  else
6247
6399
  return basepath.substring(0, lastslash + 1) + refpath;
6248
6400
  }
6249
- function remove_dot_segments(path16) {
6250
- if (!path16)
6251
- return path16;
6401
+ function remove_dot_segments(path19) {
6402
+ if (!path19)
6403
+ return path19;
6252
6404
  var output = "";
6253
- while (path16.length > 0) {
6254
- if (path16 === "." || path16 === "..") {
6255
- path16 = "";
6405
+ while (path19.length > 0) {
6406
+ if (path19 === "." || path19 === "..") {
6407
+ path19 = "";
6256
6408
  break;
6257
6409
  }
6258
- var twochars = path16.substring(0, 2);
6259
- var threechars = path16.substring(0, 3);
6260
- var fourchars = path16.substring(0, 4);
6410
+ var twochars = path19.substring(0, 2);
6411
+ var threechars = path19.substring(0, 3);
6412
+ var fourchars = path19.substring(0, 4);
6261
6413
  if (threechars === "../") {
6262
- path16 = path16.substring(3);
6414
+ path19 = path19.substring(3);
6263
6415
  } else if (twochars === "./") {
6264
- path16 = path16.substring(2);
6416
+ path19 = path19.substring(2);
6265
6417
  } else if (threechars === "/./") {
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);
6418
+ path19 = "/" + path19.substring(3);
6419
+ } else if (twochars === "/." && path19.length === 2) {
6420
+ path19 = "/";
6421
+ } else if (fourchars === "/../" || threechars === "/.." && path19.length === 3) {
6422
+ path19 = "/" + path19.substring(4);
6271
6423
  output = output.replace(/\/?[^\/]*$/, "");
6272
6424
  } else {
6273
- var segment = path16.match(/(\/?([^\/]*))/)[0];
6425
+ var segment = path19.match(/(\/?([^\/]*))/)[0];
6274
6426
  output += segment;
6275
- path16 = path16.substring(segment.length);
6427
+ path19 = path19.substring(segment.length);
6276
6428
  }
6277
6429
  }
6278
6430
  return output;
@@ -18150,14 +18302,14 @@ var require_turndown_cjs = __commonJS((exports, module) => {
18150
18302
  } else if (node.nodeType === 1) {
18151
18303
  replacement = replacementForNode.call(self, node);
18152
18304
  }
18153
- return join14(output, replacement);
18305
+ return join17(output, replacement);
18154
18306
  }, "");
18155
18307
  }
18156
18308
  function postProcess(output) {
18157
18309
  var self = this;
18158
18310
  this.rules.forEach(function(rule) {
18159
18311
  if (typeof rule.append === "function") {
18160
- output = join14(output, rule.append(self.options));
18312
+ output = join17(output, rule.append(self.options));
18161
18313
  }
18162
18314
  });
18163
18315
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -18170,7 +18322,7 @@ var require_turndown_cjs = __commonJS((exports, module) => {
18170
18322
  content = content.trim();
18171
18323
  return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
18172
18324
  }
18173
- function join14(output, replacement) {
18325
+ function join17(output, replacement) {
18174
18326
  var s1 = trimTrailingNewlines(output);
18175
18327
  var s2 = trimLeadingNewlines(replacement);
18176
18328
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -18240,11 +18392,23 @@ var CUSTOM_SKILLS = [
18240
18392
  allowedAgents: ["orchestrator"],
18241
18393
  sourcePath: "src/skills/deepwork"
18242
18394
  },
18395
+ {
18396
+ name: "reflect",
18397
+ description: "Review repeated work and suggest reusable workflow improvements",
18398
+ allowedAgents: ["orchestrator"],
18399
+ sourcePath: "src/skills/reflect"
18400
+ },
18243
18401
  {
18244
18402
  name: "oh-my-opencode-slim",
18245
18403
  description: "Configure, customize, and safely improve oh-my-opencode-slim setups",
18246
18404
  allowedAgents: ["orchestrator"],
18247
18405
  sourcePath: "src/skills/oh-my-opencode-slim"
18406
+ },
18407
+ {
18408
+ name: "worktrees",
18409
+ description: "Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work",
18410
+ allowedAgents: ["orchestrator"],
18411
+ sourcePath: "src/skills/worktrees"
18248
18412
  }
18249
18413
  ];
18250
18414
 
@@ -18303,8 +18467,7 @@ var SUBAGENT_NAMES = [
18303
18467
  "council",
18304
18468
  "councillor"
18305
18469
  ];
18306
- var ORCHESTRATOR_NAME = "orchestrator";
18307
- var ALL_AGENT_NAMES = [ORCHESTRATOR_NAME, ...SUBAGENT_NAMES];
18470
+ var ALL_AGENT_NAMES = ["orchestrator", ...SUBAGENT_NAMES];
18308
18471
  var PROTECTED_AGENTS = new Set(["orchestrator", "councillor"]);
18309
18472
  var DEFAULT_MODELS = {
18310
18473
  orchestrator: undefined,
@@ -18318,10 +18481,10 @@ var DEFAULT_MODELS = {
18318
18481
  councillor: "openai/gpt-5.4-mini"
18319
18482
  };
18320
18483
  var POLL_INTERVAL_BACKGROUND_MS = 2000;
18321
- var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
18322
18484
  var MAX_POLL_TIME_MS = 5 * 60 * 1000;
18323
18485
  var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
18324
18486
  var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion → reconcile terminal results → verify. Do not poll running jobs, consume running-job output, or advance dependent work. !END!`;
18487
+ var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
18325
18488
  var WRITABLE_FILE_OPERATIONS_RULES = `**File Operations Rules**:
18326
18489
  - Prefer dedicated file tools for normal code work: glob/grep/ast_grep_search for discovery, read for file contents, and edit/write/apply_patch for targeted source changes.
18327
18490
  - Use bash for execution and automation: git, package managers, tests, builds, scripts, diagnostics, and shell-native filesystem operations.
@@ -18386,17 +18549,11 @@ var CouncilConfigSchema = z.object({
18386
18549
  default_preset: z.string().default("default"),
18387
18550
  councillor_execution_mode: CouncillorExecutionModeSchema.describe('Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).'),
18388
18551
  councillor_retries: z.number().int().min(0).max(5).default(3).describe("Number of retry attempts for councillors that return empty responses " + "(e.g. due to provider rate limiting). Default: 3 retries."),
18389
- master: z.unknown().optional().describe("DEPRECATED — ignored. Council agent synthesizes directly."),
18390
- master_timeout: z.unknown().optional().describe('DEPRECATED — ignored. Use "timeout" instead.'),
18391
- master_fallback: z.unknown().optional().describe("DEPRECATED — ignored. No separate master session.")
18552
+ master: z.unknown().optional().describe("DEPRECATED — ignored. Council agent synthesizes directly.")
18392
18553
  }).transform((data) => {
18393
18554
  const deprecated = [];
18394
18555
  if (data.master !== undefined)
18395
18556
  deprecated.push("master");
18396
- if (data.master_timeout !== undefined)
18397
- deprecated.push("master_timeout");
18398
- if (data.master_fallback !== undefined)
18399
- deprecated.push("master_fallback");
18400
18557
  const legacyMasterModel = typeof data.master === "object" && data.master !== null && "model" in data.master && typeof data.master.model === "string" ? data.master.model : undefined;
18401
18558
  return {
18402
18559
  presets: data.presets,
@@ -18412,55 +18569,8 @@ var CouncilConfigSchema = z.object({
18412
18569
  import * as fs from "node:fs";
18413
18570
  import * as path from "node:path";
18414
18571
 
18415
- // src/utils/compat.ts
18416
- import { spawn as nodeSpawn } from "node:child_process";
18417
- import { writeFile as fsWriteFile } from "node:fs/promises";
18418
- var isBun = typeof globalThis.Bun !== "undefined";
18419
- function collectStream(stream) {
18420
- if (!stream)
18421
- return () => Promise.resolve("");
18422
- const chunks = [];
18423
- stream.on("data", (chunk) => chunks.push(chunk));
18424
- return () => new Promise((resolve, reject) => {
18425
- if (!stream.readable) {
18426
- resolve(Buffer.concat(chunks).toString("utf-8"));
18427
- return;
18428
- }
18429
- stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
18430
- stream.on("error", reject);
18431
- });
18432
- }
18433
- function crossSpawn(command, options) {
18434
- const [cmd, ...args] = command;
18435
- const proc = nodeSpawn(cmd, args, {
18436
- stdio: [
18437
- options?.stdin ?? "ignore",
18438
- options?.stdout ?? "pipe",
18439
- options?.stderr ?? "pipe"
18440
- ],
18441
- cwd: options?.cwd,
18442
- env: options?.env
18443
- });
18444
- const stdoutCollector = collectStream(proc.stdout);
18445
- const stderrCollector = collectStream(proc.stderr);
18446
- const exited = new Promise((resolve, reject) => {
18447
- proc.on("error", reject);
18448
- proc.on("close", (code) => resolve(code ?? 1));
18449
- });
18450
- return {
18451
- proc,
18452
- stdout: stdoutCollector,
18453
- stderr: stderrCollector,
18454
- exited,
18455
- kill: (signal) => proc.kill(signal),
18456
- get exitCode() {
18457
- return proc.exitCode;
18458
- }
18459
- };
18460
- }
18461
- async function crossWrite(path, data) {
18462
- await fsWriteFile(path, Buffer.from(data));
18463
- }
18572
+ // src/cli/config-io.ts
18573
+ init_compat();
18464
18574
 
18465
18575
  // src/config/agent-mcps.ts
18466
18576
  var DEFAULT_AGENT_MCPS = {
@@ -18534,15 +18644,6 @@ var ManualPlanSchema = z2.object({
18534
18644
  librarian: ManualAgentPlanSchema,
18535
18645
  fixer: ManualAgentPlanSchema
18536
18646
  }).strict();
18537
- var AgentModelChainSchema = z2.array(z2.string()).min(1);
18538
- var FallbackChainsSchema = z2.object({
18539
- orchestrator: AgentModelChainSchema.optional(),
18540
- oracle: AgentModelChainSchema.optional(),
18541
- designer: AgentModelChainSchema.optional(),
18542
- explorer: AgentModelChainSchema.optional(),
18543
- librarian: AgentModelChainSchema.optional(),
18544
- fixer: AgentModelChainSchema.optional()
18545
- }).catchall(AgentModelChainSchema);
18546
18647
  var AgentOverrideConfigSchema = z2.object({
18547
18648
  model: z2.union([
18548
18649
  z2.string(),
@@ -18605,14 +18706,32 @@ var FailoverConfigSchema = z2.object({
18605
18706
  enabled: z2.boolean().default(true),
18606
18707
  timeoutMs: z2.number().min(0).default(15000),
18607
18708
  retryDelayMs: z2.number().min(0).default(500),
18608
- chains: FallbackChainsSchema.default({}),
18609
18709
  retry_on_empty: z2.boolean().default(true).describe("When true (default), empty provider responses are treated as failures, " + "triggering fallback/retry. Set to false to treat them as successes.")
18610
- });
18710
+ }).strict();
18611
18711
  var CompanionConfigSchema = z2.object({
18612
18712
  enabled: z2.boolean().optional(),
18713
+ binaryPath: z2.string().min(1).optional().describe("Path to a custom companion binary to launch."),
18613
18714
  position: z2.enum(["bottom-right", "bottom-left", "top-right", "top-left"]).optional(),
18614
- size: z2.enum(["small", "medium", "large"]).optional()
18715
+ size: z2.enum(["small", "medium", "large"]).optional(),
18716
+ gifPack: z2.enum(["default"]).optional().describe("Bundled companion animation pack to use."),
18717
+ loopStyle: z2.enum(["classic", "smooth"]).optional().describe("Companion animation playback style: classic loops or smooth ping-pong playback."),
18718
+ speed: z2.number().min(0.25).max(4).optional().describe("Companion animation playback speed multiplier. Defaults to 1."),
18719
+ debug: z2.boolean().optional().describe("Enable verbose native companion debug logs.")
18615
18720
  });
18721
+ var AcpAgentPermissionModeSchema = z2.enum(["ask", "allow", "reject"]);
18722
+ var AcpAgentConfigSchema = z2.object({
18723
+ command: z2.string().min(1),
18724
+ args: z2.array(z2.string()).default([]),
18725
+ env: z2.record(z2.string(), z2.string()).default({}),
18726
+ cwd: z2.string().min(1).optional(),
18727
+ description: z2.string().min(1).optional(),
18728
+ prompt: z2.string().min(1).optional(),
18729
+ orchestratorPrompt: z2.string().min(1).optional(),
18730
+ wrapperModel: ProviderModelIdSchema.optional(),
18731
+ timeoutMs: z2.number().int().min(1000).max(900000).default(300000),
18732
+ permissionMode: AcpAgentPermissionModeSchema.default("ask")
18733
+ }).strict();
18734
+ var AcpAgentsConfigSchema = z2.record(z2.string(), AcpAgentConfigSchema);
18616
18735
  function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
18617
18736
  for (const [name, override] of Object.entries(overrides)) {
18618
18737
  const isBuiltInOrAlias = ALL_AGENT_NAMES.includes(name) || AGENT_ALIASES[name] !== undefined;
@@ -18638,10 +18757,7 @@ function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
18638
18757
  var PluginConfigSchema = z2.object({
18639
18758
  preset: z2.string().optional(),
18640
18759
  setDefaultAgent: z2.boolean().optional(),
18641
- scoringEngineVersion: z2.enum(["v1", "v2-shadow", "v2"]).optional(),
18642
- balanceProviderUsage: z2.boolean().optional(),
18643
18760
  autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
18644
- manualPlan: ManualPlanSchema.optional(),
18645
18761
  presets: z2.record(z2.string(), PresetSchema).optional(),
18646
18762
  agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
18647
18763
  disabled_agents: z2.array(z2.string()).optional().describe("Agent names to disable completely. " + "Disabled agents are not instantiated and cannot be delegated to. " + "Orchestrator and council internal agents (councillor) cannot be disabled. " + "By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable."),
@@ -18653,7 +18769,8 @@ var PluginConfigSchema = z2.object({
18653
18769
  backgroundJobs: BackgroundJobsConfigSchema.optional(),
18654
18770
  fallback: FailoverConfigSchema.optional(),
18655
18771
  council: CouncilConfigSchema.optional(),
18656
- companion: CompanionConfigSchema.optional()
18772
+ companion: CompanionConfigSchema.optional(),
18773
+ acpAgents: AcpAgentsConfigSchema.optional()
18657
18774
  }).superRefine((value, ctx) => {
18658
18775
  if (value.agents) {
18659
18776
  validateCustomOnlyPromptFields(value.agents, ctx, ["agents"]);
@@ -18753,6 +18870,7 @@ function mergePluginConfigs(base, override) {
18753
18870
  backgroundJobs: deepMerge(base.backgroundJobs, override.backgroundJobs),
18754
18871
  fallback: deepMerge(base.fallback, override.fallback),
18755
18872
  council: deepMerge(base.council, override.council),
18873
+ acpAgents: deepMerge(base.acpAgents, override.acpAgents),
18756
18874
  companion: deepMerge(base.companion, override.companion)
18757
18875
  };
18758
18876
  }
@@ -18806,8 +18924,13 @@ function loadPluginConfig(directory, options) {
18806
18924
  if (config.companion) {
18807
18925
  config.companion = {
18808
18926
  enabled: config.companion.enabled ?? false,
18927
+ binaryPath: config.companion.binaryPath,
18809
18928
  position: config.companion.position ?? "bottom-right",
18810
- size: config.companion.size ?? "medium"
18929
+ size: config.companion.size ?? "medium",
18930
+ gifPack: config.companion.gifPack ?? "default",
18931
+ loopStyle: config.companion.loopStyle ?? "classic",
18932
+ speed: config.companion.speed ?? 1,
18933
+ debug: config.companion.debug ?? false
18811
18934
  };
18812
18935
  }
18813
18936
  return config;
@@ -18868,6 +18991,9 @@ function getCustomAgentNames(config) {
18868
18991
  return !ALL_AGENT_NAMES.includes(name);
18869
18992
  });
18870
18993
  }
18994
+ function getAcpAgentNames(config) {
18995
+ return Object.keys(config?.acpAgents ?? {});
18996
+ }
18871
18997
  // src/utils/session.ts
18872
18998
  var SESSION_ABORT_TIMEOUT_MS = 1000;
18873
18999
 
@@ -19201,7 +19327,6 @@ When user's approach seems problematic:
19201
19327
  </Communication>
19202
19328
  `;
19203
19329
  }
19204
- var ORCHESTRATOR_PROMPT = buildOrchestratorPrompt();
19205
19330
  function createOrchestratorAgent(model, customPrompt, customAppendPrompt, disabledAgents) {
19206
19331
  const basePrompt = buildOrchestratorPrompt(disabledAgents);
19207
19332
  const prompt = resolvePrompt(basePrompt, customPrompt, customAppendPrompt);
@@ -19744,6 +19869,39 @@ function normalizeDisplayName(displayName) {
19744
19869
  const trimmed = displayName.trim();
19745
19870
  return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
19746
19871
  }
19872
+ function buildAcpAgentDefinition(name, config) {
19873
+ const description = config.description ?? `External ACP agent '${name}' via ${config.command}`;
19874
+ const prompt = config.prompt ?? [
19875
+ `You are the ${name} ACP wrapper agent.`,
19876
+ "",
19877
+ "Your only job is to send the user task to the configured external ACP agent using the acp_run tool, then return the ACP agent result.",
19878
+ `Always call acp_run with agent: ${JSON.stringify(name)} and pass the full user task as prompt.`,
19879
+ "Do not edit files yourself unless the ACP result explicitly asks you to report a local follow-up to the orchestrator."
19880
+ ].join(`
19881
+ `);
19882
+ return {
19883
+ name,
19884
+ description,
19885
+ config: {
19886
+ model: config.wrapperModel ?? DEFAULT_MODELS.fixer ?? DEFAULT_MODELS.librarian ?? DEFAULT_MODELS.orchestrator ?? DEFAULT_MODELS.oracle,
19887
+ temperature: 0,
19888
+ prompt,
19889
+ permission: {
19890
+ read: "deny",
19891
+ edit: "deny",
19892
+ bash: "deny",
19893
+ task: "deny",
19894
+ glob: "deny",
19895
+ grep: "deny",
19896
+ list: "deny",
19897
+ webfetch: "deny",
19898
+ question: "deny",
19899
+ skill: "deny",
19900
+ acp_run: "allow"
19901
+ }
19902
+ }
19903
+ };
19904
+ }
19747
19905
  function isSafeDisplayName(displayName) {
19748
19906
  return SAFE_AGENT_ALIAS_RE.test(displayName);
19749
19907
  }
@@ -19883,6 +20041,24 @@ function createAgents(config) {
19883
20041
  buildCustomAgentDefinition(name, override, customPrompts.prompt, customPrompts.appendPrompt)
19884
20042
  ];
19885
20043
  });
20044
+ const acpAgentNames = getAcpAgentNames(config).map(normalizeCustomAgentName).filter((name) => name.length > 0).filter((name) => {
20045
+ if (!SAFE_AGENT_ALIAS_RE.test(name)) {
20046
+ throw new Error(`ACP agent name '${name}' must match /^[a-z][a-z0-9_-]*$/i`);
20047
+ }
20048
+ if (isKnownAgentName(name) || AGENT_ALIASES[name] !== undefined) {
20049
+ throw new Error(`ACP agent '${name}' conflicts with a built-in agent name or alias`);
20050
+ }
20051
+ if (customAgentNames.includes(name)) {
20052
+ throw new Error(`ACP agent '${name}' conflicts with a custom agent of the same name`);
20053
+ }
20054
+ return !disabled.has(name);
20055
+ });
20056
+ const protoAcpAgents = acpAgentNames.map((name) => {
20057
+ const acp = config?.acpAgents?.[name];
20058
+ if (!acp)
20059
+ throw new Error(`ACP agent '${name}' is missing config`);
20060
+ return buildAcpAgentDefinition(name, acp);
20061
+ });
19886
20062
  const builtInSubAgents = protoSubAgents.map((agent) => {
19887
20063
  const override = getAgentOverride(config, agent.name);
19888
20064
  if (override) {
@@ -19906,7 +20082,15 @@ function createAgents(config) {
19906
20082
  applyDefaultPermissions(agent, override?.skills);
19907
20083
  return agent;
19908
20084
  });
19909
- const allSubAgents = [...builtInSubAgents, ...customSubAgents];
20085
+ const acpSubAgents = protoAcpAgents.map((agent) => {
20086
+ applyDefaultPermissions(agent);
20087
+ return agent;
20088
+ });
20089
+ const allSubAgents = [
20090
+ ...builtInSubAgents,
20091
+ ...customSubAgents,
20092
+ ...acpSubAgents
20093
+ ];
19910
20094
  const orchestratorOverride = getAgentOverride(config, "orchestrator");
19911
20095
  const orchestratorModel = orchestratorOverride?.model ?? DEFAULT_MODELS.orchestrator;
19912
20096
  const orchestratorPrompts = loadAgentPrompt("orchestrator", config?.preset);
@@ -19928,6 +20112,20 @@ function createAgents(config) {
19928
20112
  const override = getAgentOverride(config, agent.name);
19929
20113
  return override?.orchestratorPrompt;
19930
20114
  }).filter((prompt) => Boolean(prompt));
20115
+ const acpOrchestratorPrompts = acpSubAgents.map((agent) => {
20116
+ const acp = config?.acpAgents?.[agent.name];
20117
+ if (acp?.orchestratorPrompt)
20118
+ return acp.orchestratorPrompt;
20119
+ return [
20120
+ `@${agent.name}`,
20121
+ `- Lane: External ACP-connected agent (${acp?.command ?? "unknown command"})`,
20122
+ `- Role: ${agent.description ?? `External ACP agent ${agent.name}`}`,
20123
+ "- **Delegate when:** The user explicitly asks for this ACP-backed agent, or the task matches its role and benefits from software/subscription-specific capabilities outside OpenCode.",
20124
+ "- **Do not delegate when:** The built-in specialists can handle the task more directly or local file ownership would conflict with another writer lane.",
20125
+ "- **Result handling:** Treat returned output as external-agent work. Reconcile any reported file changes before continuing."
20126
+ ].join(`
20127
+ `);
20128
+ });
19931
20129
  const usedDisplayNames = new Set;
19932
20130
  for (const [, displayName] of displayNameMap) {
19933
20131
  const normalizedDisplayName = normalizeDisplayName(displayName);
@@ -19940,13 +20138,17 @@ function createAgents(config) {
19940
20138
  usedDisplayNames.add(normalizedDisplayName);
19941
20139
  }
19942
20140
  for (const displayName of usedDisplayNames) {
19943
- if (ALL_AGENT_NAMES.includes(displayName) || customAgentNames.includes(displayName)) {
20141
+ if (ALL_AGENT_NAMES.includes(displayName) || customAgentNames.includes(displayName) || acpAgentNames.includes(displayName)) {
19944
20142
  throw new Error(`displayName '${displayName}' conflicts with an agent name`);
19945
20143
  }
19946
20144
  }
19947
20145
  injectDisplayNames(orchestrator, displayNameMap);
19948
- if (customOrchestratorPrompts.length > 0) {
19949
- const rewrittenPrompts = customOrchestratorPrompts.map((promptText) => {
20146
+ const extraOrchestratorPrompts = [
20147
+ ...customOrchestratorPrompts,
20148
+ ...acpOrchestratorPrompts
20149
+ ];
20150
+ if (extraOrchestratorPrompts.length > 0) {
20151
+ const rewrittenPrompts = extraOrchestratorPrompts.map((promptText) => {
19950
20152
  let text = promptText;
19951
20153
  for (const [internalName, displayName] of displayNameMap) {
19952
20154
  text = text.replace(new RegExp(`@${escapeRegExp(internalName)}\\b`, "g"), `@${normalizeDisplayName(displayName)}`);
@@ -20018,6 +20220,7 @@ import {
20018
20220
  mkdirSync as mkdirSync2,
20019
20221
  readFileSync as readFileSync2,
20020
20222
  renameSync,
20223
+ rmSync,
20021
20224
  writeFileSync
20022
20225
  } from "node:fs";
20023
20226
  import * as os2 from "node:os";
@@ -20106,11 +20309,15 @@ function stateFilePath() {
20106
20309
  const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(os2.homedir(), ".local", "share");
20107
20310
  return path3.join(base, "opencode", "storage", "oh-my-opencode-slim", "companion-state.json");
20108
20311
  }
20109
- function binaryPath() {
20312
+ function defaultBinaryPath() {
20110
20313
  const xdg = process.env.XDG_DATA_HOME?.trim();
20111
20314
  const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(os2.homedir(), ".local", "share");
20112
20315
  const binaryName = os2.platform() === "win32" ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion";
20113
- const bin = path3.join(base, "opencode", "storage", "oh-my-opencode-slim", "bin", binaryName);
20316
+ return path3.join(base, "opencode", "storage", "oh-my-opencode-slim", "bin", binaryName);
20317
+ }
20318
+ function resolveCompanionBinaryPath(config) {
20319
+ const configured = config?.binaryPath?.trim();
20320
+ const bin = configured || defaultBinaryPath();
20114
20321
  return existsSync2(bin) ? bin : null;
20115
20322
  }
20116
20323
  function readState() {
@@ -20123,17 +20330,43 @@ function readState() {
20123
20330
  } catch {}
20124
20331
  return { version: 1, sessions: [] };
20125
20332
  }
20126
- function writeState(state) {
20333
+ function writeState(mutator) {
20127
20334
  const file = stateFilePath();
20128
20335
  try {
20129
20336
  mkdirSync2(path3.dirname(file), { recursive: true });
20130
- const tmp = `${file}.tmp`;
20131
- writeFileSync(tmp, JSON.stringify(state));
20132
- renameSync(tmp, file);
20337
+ const release = acquireStateLock(file);
20338
+ try {
20339
+ const state = readState();
20340
+ mutator(state);
20341
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
20342
+ writeFileSync(tmp, JSON.stringify(state));
20343
+ renameSync(tmp, file);
20344
+ } finally {
20345
+ release();
20346
+ }
20133
20347
  } catch (err) {
20134
20348
  log("[companion] write failed", String(err));
20135
20349
  }
20136
20350
  }
20351
+ function acquireStateLock(file) {
20352
+ const lock = `${file}.lock`;
20353
+ for (let attempt = 0;attempt < 40; attempt++) {
20354
+ try {
20355
+ mkdirSync2(lock);
20356
+ return () => {
20357
+ try {
20358
+ rmSync(lock, { recursive: true, force: true });
20359
+ } catch {}
20360
+ };
20361
+ } catch (err) {
20362
+ const code = err.code;
20363
+ if (code !== "EEXIST")
20364
+ throw err;
20365
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
20366
+ }
20367
+ }
20368
+ throw new Error("timed out waiting for companion state lock");
20369
+ }
20137
20370
 
20138
20371
  class CompanionManager {
20139
20372
  id;
@@ -20149,12 +20382,11 @@ class CompanionManager {
20149
20382
  onLoad() {
20150
20383
  if (this.config?.enabled !== true) {
20151
20384
  try {
20152
- const state = readState();
20153
- const filtered = state.sessions.filter((s) => s.session_id !== this.id);
20154
- if (filtered.length !== state.sessions.length) {
20155
- state.sessions = filtered;
20156
- writeState(state);
20157
- }
20385
+ if (!existsSync2(stateFilePath()))
20386
+ return;
20387
+ writeState((state) => {
20388
+ state.sessions = state.sessions.filter((s) => s.session_id !== this.id);
20389
+ });
20158
20390
  } catch {}
20159
20391
  return;
20160
20392
  }
@@ -20206,9 +20438,9 @@ class CompanionManager {
20206
20438
  onExit() {
20207
20439
  if (this.config?.enabled !== true)
20208
20440
  return;
20209
- const state = readState();
20210
- state.sessions = state.sessions.filter((s) => s.session_id !== this.id);
20211
- writeState(state);
20441
+ writeState((state) => {
20442
+ state.sessions = state.sessions.filter((s) => s.session_id !== this.id);
20443
+ });
20212
20444
  }
20213
20445
  activeAgents() {
20214
20446
  const agents = Array.from(this.busyAgentSessions.values());
@@ -20224,28 +20456,41 @@ class CompanionManager {
20224
20456
  if (this.config?.enabled !== true)
20225
20457
  return;
20226
20458
  try {
20227
- const state = readState();
20228
20459
  const entry = {
20229
20460
  session_id: this.id,
20230
20461
  cwd: this.cwd,
20231
20462
  active_agents: this.activeAgents(),
20232
20463
  status: this.status,
20233
- pid: process.pid
20234
- };
20235
- const idx = state.sessions.findIndex((s) => s.session_id === this.id);
20236
- if (idx >= 0) {
20237
- state.sessions[idx] = entry;
20238
- } else {
20239
- state.sessions.push(entry);
20240
- }
20241
- if (this.config) {
20242
- state.config = {
20464
+ pid: process.pid,
20465
+ config: this.config ? {
20243
20466
  enabled: this.config.enabled ?? false,
20244
20467
  position: this.config.position ?? "bottom-right",
20245
- size: this.config.size ?? "medium"
20246
- };
20247
- }
20248
- writeState(state);
20468
+ size: this.config.size ?? "medium",
20469
+ gifPack: this.config.gifPack ?? "default",
20470
+ loopStyle: this.config.loopStyle ?? "classic",
20471
+ speed: this.config.speed ?? 1,
20472
+ debug: this.config.debug ?? false
20473
+ } : undefined
20474
+ };
20475
+ writeState((state) => {
20476
+ const idx = state.sessions.findIndex((s) => s.session_id === this.id);
20477
+ if (idx >= 0) {
20478
+ state.sessions[idx] = entry;
20479
+ } else {
20480
+ state.sessions.push(entry);
20481
+ }
20482
+ if (this.config) {
20483
+ state.config = {
20484
+ enabled: this.config.enabled ?? false,
20485
+ position: this.config.position ?? "bottom-right",
20486
+ size: this.config.size ?? "medium",
20487
+ gifPack: this.config.gifPack ?? "default",
20488
+ loopStyle: this.config.loopStyle ?? "classic",
20489
+ speed: this.config.speed ?? 1,
20490
+ debug: this.config.debug ?? false
20491
+ };
20492
+ }
20493
+ });
20249
20494
  } catch (err) {
20250
20495
  log("[companion] flush failed", String(err));
20251
20496
  }
@@ -20253,51 +20498,356 @@ class CompanionManager {
20253
20498
  spawnIfAvailable() {
20254
20499
  if (this.config?.enabled !== true)
20255
20500
  return;
20256
- const bin = binaryPath();
20501
+ const bin = resolveCompanionBinaryPath(this.config);
20257
20502
  if (!bin) {
20258
- const xdg = process.env.XDG_DATA_HOME?.trim();
20259
- const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(os2.homedir(), ".local", "share");
20260
- const expected = path3.join(base, "o‍pencode", "storage", "oh-my-o‍pencode-slim", "bin", "oh-my-o‍pencode-slim-companion");
20503
+ const expected = this.config.binaryPath?.trim() || defaultBinaryPath();
20261
20504
  log(`[companion] enabled but companion binary not found at expected path: ${expected}. Please install/download the companion binary separately.`);
20262
20505
  return;
20263
20506
  }
20264
20507
  try {
20265
- const child = spawn(bin, [], { detached: true, stdio: "ignore" });
20508
+ const child = spawn(bin, [], {
20509
+ detached: true,
20510
+ env: {
20511
+ ...process.env,
20512
+ OH_MY_OPENCODE_SLIM_COMPANION_SESSION_ID: this.id,
20513
+ ...this.config.debug === true ? { OH_MY_OPENCODE_SLIM_COMPANION_DEBUG: "1" } : {}
20514
+ },
20515
+ stdio: "ignore"
20516
+ });
20266
20517
  child.unref();
20267
- log("[companion] spawned", bin);
20518
+ log("[companion] spawned", JSON.stringify({
20519
+ bin,
20520
+ sessionId: this.id,
20521
+ debug: this.config.debug === true
20522
+ }));
20268
20523
  } catch (err) {
20269
20524
  log("[companion] spawn failed", String(err));
20270
20525
  }
20271
20526
  }
20272
20527
  }
20273
20528
 
20274
- // src/config/fallback-chains.ts
20275
- function normalizeFallbackChainsForPreset(chains, presetName) {
20276
- const normalized = {};
20277
- for (const [rawKey, chainModels] of Object.entries(chains)) {
20278
- if (!chainModels?.length)
20279
- continue;
20280
- const separatorIndex = rawKey.indexOf(":");
20281
- const hasPresetScope = separatorIndex !== -1;
20282
- const scopedPreset = hasPresetScope ? rawKey.slice(0, separatorIndex) : "";
20283
- const agentName = hasPresetScope ? rawKey.slice(separatorIndex + 1) : rawKey;
20284
- if (!agentName)
20285
- continue;
20286
- if (hasPresetScope && scopedPreset !== presetName)
20287
- continue;
20288
- const existing = normalized[agentName] ?? [];
20289
- const seen = new Set(existing);
20290
- for (const chainModel of chainModels) {
20291
- if (seen.has(chainModel))
20292
- continue;
20293
- seen.add(chainModel);
20294
- existing.push(chainModel);
20529
+ // src/companion/updater.ts
20530
+ init_compat();
20531
+ import { createHash } from "node:crypto";
20532
+ import {
20533
+ chmodSync,
20534
+ copyFileSync,
20535
+ existsSync as existsSync3,
20536
+ mkdirSync as mkdirSync3,
20537
+ mkdtempSync,
20538
+ readFileSync as readFileSync3,
20539
+ renameSync as renameSync2,
20540
+ rmSync as rmSync2,
20541
+ statSync as statSync2,
20542
+ writeFileSync as writeFileSync2
20543
+ } from "node:fs";
20544
+ import { homedir as homedir4, platform as platform2, tmpdir } from "node:os";
20545
+ import * as path4 from "node:path";
20546
+ import { setTimeout as delay } from "node:timers/promises";
20547
+ var DOWNLOAD_TIMEOUT_MS = 30000;
20548
+ var LOCK_TIMEOUT_MS = 2000;
20549
+ var STALE_LOCK_MS = 5 * 60000;
20550
+ var FIRST_METADATA_VERSION = "0.1.2";
20551
+ var COMPANION_MANIFEST = {
20552
+ version: "0.1.3",
20553
+ tag: "companion-v0.1.3",
20554
+ repo: "alvinunreal/oh-my-opencode-slim",
20555
+ checksums: {
20556
+ "oh-my-opencode-slim-companion-v0.1.3-aarch64-apple-darwin.tar.gz": "b4885f9b1900c02376e5f8f5ae6f3b8a89d26f7514b03f836d7e3d618164a0ed",
20557
+ "oh-my-opencode-slim-companion-v0.1.3-aarch64-unknown-linux-gnu.tar.gz": "ed7cffc583e1eaa78c9bea702e6b6aa3bbc5bb4d881713fb2050237ba6b7aca5",
20558
+ "oh-my-opencode-slim-companion-v0.1.3-x86_64-apple-darwin.tar.gz": "98d8ea7c7bc4415b18e0d4c524adb4eb9a84c872919840fdc021f0f50c61f808",
20559
+ "oh-my-opencode-slim-companion-v0.1.3-x86_64-pc-windows-msvc.zip": "9316a49bf01f3b4fb1ce2d62edfc46094e73bb153d6ce023fb7df085afcf77bd",
20560
+ "oh-my-opencode-slim-companion-v0.1.3-x86_64-unknown-linux-gnu.tar.gz": "33f5fd4b6c80155a019391e5efb13904ca9531ba8dd8c6cba30a161f1b07b764"
20561
+ }
20562
+ };
20563
+ function getCompanionTarget() {
20564
+ const p = process.platform;
20565
+ const a = process.arch;
20566
+ if (p === "darwin") {
20567
+ if (a === "arm64")
20568
+ return "aarch64-apple-darwin";
20569
+ if (a === "x64")
20570
+ return "x86_64-apple-darwin";
20571
+ } else if (p === "linux") {
20572
+ if (a === "x64")
20573
+ return "x86_64-unknown-linux-gnu";
20574
+ if (a === "arm64")
20575
+ return "aarch64-unknown-linux-gnu";
20576
+ } else if (p === "win32") {
20577
+ if (a === "x64")
20578
+ return "x86_64-pc-windows-msvc";
20579
+ }
20580
+ return null;
20581
+ }
20582
+ function getCompanionBinaryPath() {
20583
+ const xdg = process.env.XDG_DATA_HOME?.trim();
20584
+ const base = xdg && path4.isAbsolute(xdg) ? xdg : path4.join(homedir4(), ".local", "share");
20585
+ return path4.join(base, "opencode", "storage", "oh-my-opencode-slim", "bin", platform2() === "win32" ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion");
20586
+ }
20587
+ function loadCompanionManifestFromPackageRoot(packageRoot) {
20588
+ const manifestPath = path4.join(packageRoot, "src", "companion", "companion-manifest.json");
20589
+ try {
20590
+ const parsed = JSON.parse(readFileSync3(manifestPath, "utf8"));
20591
+ if (parsed.version && parsed.tag && parsed.repo) {
20592
+ return {
20593
+ version: parsed.version,
20594
+ tag: parsed.tag,
20595
+ repo: parsed.repo,
20596
+ checksums: parsed.checksums
20597
+ };
20598
+ }
20599
+ } catch {}
20600
+ return null;
20601
+ }
20602
+ async function ensureCompanionVersion(options) {
20603
+ const { config, dryRun = false } = options;
20604
+ const manifest = options.manifest ?? COMPANION_MANIFEST;
20605
+ const binaryPath = getCompanionBinaryPath();
20606
+ if (config?.enabled !== true) {
20607
+ return { status: "skipped", reason: "disabled", binaryPath };
20608
+ }
20609
+ if (config.binaryPath?.trim()) {
20610
+ return { status: "skipped", reason: "custom-binary", binaryPath };
20611
+ }
20612
+ const target = getCompanionTarget();
20613
+ if (!target) {
20614
+ return {
20615
+ status: "failed",
20616
+ binaryPath,
20617
+ error: `Unsupported platform/architecture: ${process.platform} ${process.arch}`
20618
+ };
20619
+ }
20620
+ const current = readInstallMetadata(binaryPath);
20621
+ if (existsSync3(binaryPath) && !current && manifest.version === FIRST_METADATA_VERSION) {
20622
+ const archiveName = companionArchiveName(manifest.version, target);
20623
+ writeInstallMetadata(binaryPath, {
20624
+ version: manifest.version,
20625
+ tag: manifest.tag,
20626
+ target,
20627
+ installedAt: new Date().toISOString(),
20628
+ archiveName,
20629
+ checksum: manifest.checksums?.[archiveName]
20630
+ });
20631
+ return { status: "current", binaryPath, version: manifest.version };
20632
+ }
20633
+ if (existsSync3(binaryPath) && current?.target === target && compareSemver(current.version, manifest.version) >= 0) {
20634
+ return { status: "current", binaryPath, version: current.version };
20635
+ }
20636
+ if (dryRun) {
20637
+ return { status: "installed", binaryPath, version: manifest.version };
20638
+ }
20639
+ return withCompanionInstallLock(binaryPath, options.lockTimeoutMs, options.lockStaleMs, async () => {
20640
+ const lockedCurrent = readInstallMetadata(binaryPath);
20641
+ if (existsSync3(binaryPath) && !lockedCurrent && manifest.version === FIRST_METADATA_VERSION) {
20642
+ const archiveName = companionArchiveName(manifest.version, target);
20643
+ writeInstallMetadata(binaryPath, {
20644
+ version: manifest.version,
20645
+ tag: manifest.tag,
20646
+ target,
20647
+ installedAt: new Date().toISOString(),
20648
+ archiveName,
20649
+ checksum: manifest.checksums?.[archiveName]
20650
+ });
20651
+ return { status: "current", binaryPath, version: manifest.version };
20295
20652
  }
20296
- if (existing.length > 0) {
20297
- normalized[agentName] = existing;
20653
+ if (existsSync3(binaryPath) && lockedCurrent?.target === target && compareSemver(lockedCurrent.version, manifest.version) >= 0) {
20654
+ return {
20655
+ status: "current",
20656
+ binaryPath,
20657
+ version: lockedCurrent.version
20658
+ };
20298
20659
  }
20660
+ return installCompanionArchive(binaryPath, target, manifest, options.downloadTimeoutMs ?? DOWNLOAD_TIMEOUT_MS);
20661
+ });
20662
+ }
20663
+ async function installCompanionArchive(finalBinaryPath, target, manifest, downloadTimeoutMs) {
20664
+ const isWindows = process.platform === "win32";
20665
+ const archiveName = companionArchiveName(manifest.version, target, isWindows);
20666
+ const downloadUrl = `https://github.com/${manifest.repo}/releases/download/${manifest.tag}/${archiveName}`;
20667
+ const expectedChecksum = manifest.checksums?.[archiveName];
20668
+ if (!expectedChecksum) {
20669
+ return {
20670
+ status: "failed",
20671
+ binaryPath: finalBinaryPath,
20672
+ error: `Missing SHA256 checksum for companion archive: ${archiveName}`
20673
+ };
20299
20674
  }
20300
- return normalized;
20675
+ let buffer;
20676
+ const controller = new AbortController;
20677
+ const timeout = setTimeout(() => controller.abort(), downloadTimeoutMs);
20678
+ try {
20679
+ const res = await fetch(downloadUrl, { signal: controller.signal });
20680
+ if (!res.ok) {
20681
+ return {
20682
+ status: "failed",
20683
+ binaryPath: finalBinaryPath,
20684
+ error: `Failed to download companion binary (HTTP ${res.status}): ${res.statusText}`
20685
+ };
20686
+ }
20687
+ buffer = await res.arrayBuffer();
20688
+ } catch (err) {
20689
+ return {
20690
+ status: "failed",
20691
+ binaryPath: finalBinaryPath,
20692
+ error: `Failed to fetch companion archive: ${formatError(err)}`
20693
+ };
20694
+ } finally {
20695
+ clearTimeout(timeout);
20696
+ }
20697
+ const checksum = createHash("sha256").update(Buffer.from(buffer)).digest("hex");
20698
+ if (checksum !== expectedChecksum) {
20699
+ return {
20700
+ status: "failed",
20701
+ binaryPath: finalBinaryPath,
20702
+ error: "Companion archive checksum mismatch"
20703
+ };
20704
+ }
20705
+ let tempDir = "";
20706
+ try {
20707
+ tempDir = mkdtempSync(path4.join(tmpdir(), "companion-install-"));
20708
+ const archivePath = path4.join(tempDir, archiveName);
20709
+ writeFileSync2(archivePath, Buffer.from(buffer));
20710
+ const extractedDir = path4.join(tempDir, "extracted");
20711
+ mkdirSync3(extractedDir, { recursive: true });
20712
+ if (isWindows) {
20713
+ const { extractZip: extractZip2 } = await Promise.resolve().then(() => (init_zip_extractor(), exports_zip_extractor));
20714
+ await extractZip2(archivePath, extractedDir);
20715
+ } else {
20716
+ const proc = crossSpawn(["tar", "-xzf", archivePath, "-C", extractedDir]);
20717
+ const exitCode = await proc.exited;
20718
+ if (exitCode !== 0) {
20719
+ const stderr = await proc.stderr();
20720
+ return {
20721
+ status: "failed",
20722
+ binaryPath: finalBinaryPath,
20723
+ error: `Archive extraction failed (tar exited with ${exitCode}): ${stderr}`
20724
+ };
20725
+ }
20726
+ }
20727
+ const binaryName = isWindows ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion";
20728
+ const extractedBinaryPath = path4.join(extractedDir, binaryName);
20729
+ if (!existsSync3(extractedBinaryPath)) {
20730
+ return {
20731
+ status: "failed",
20732
+ binaryPath: finalBinaryPath,
20733
+ error: `Binary ${binaryName} not found in extracted archive`
20734
+ };
20735
+ }
20736
+ const binDir = path4.dirname(finalBinaryPath);
20737
+ mkdirSync3(binDir, { recursive: true });
20738
+ const tmpFinalPath = `${finalBinaryPath}.tmp`;
20739
+ copyFileSync(extractedBinaryPath, tmpFinalPath);
20740
+ if (!isWindows) {
20741
+ chmodSync(tmpFinalPath, 493);
20742
+ }
20743
+ renameSync2(tmpFinalPath, finalBinaryPath);
20744
+ writeInstallMetadata(finalBinaryPath, {
20745
+ version: manifest.version,
20746
+ tag: manifest.tag,
20747
+ target,
20748
+ installedAt: new Date().toISOString(),
20749
+ archiveName,
20750
+ checksum
20751
+ });
20752
+ return {
20753
+ status: "installed",
20754
+ binaryPath: finalBinaryPath,
20755
+ version: manifest.version
20756
+ };
20757
+ } catch (err) {
20758
+ return {
20759
+ status: "failed",
20760
+ binaryPath: finalBinaryPath,
20761
+ error: `Failed to install companion: ${formatError(err)}`
20762
+ };
20763
+ } finally {
20764
+ if (tempDir) {
20765
+ try {
20766
+ rmSync2(tempDir, { recursive: true, force: true });
20767
+ } catch {}
20768
+ }
20769
+ }
20770
+ }
20771
+ function readInstallMetadata(binaryPath) {
20772
+ try {
20773
+ const parsed = JSON.parse(readFileSync3(metadataPath(binaryPath), "utf8"));
20774
+ if (parsed?.version && parsed.tag && parsed.target) {
20775
+ return parsed;
20776
+ }
20777
+ } catch {}
20778
+ return null;
20779
+ }
20780
+ function writeInstallMetadata(binaryPath, metadata) {
20781
+ writeFileSync2(metadataPath(binaryPath), JSON.stringify(metadata, null, 2));
20782
+ }
20783
+ function metadataPath(binaryPath) {
20784
+ return `${binaryPath}.json`;
20785
+ }
20786
+ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
20787
+ const lock = `${binaryPath}.lock`;
20788
+ const deadline = Date.now() + (timeoutMs ?? LOCK_TIMEOUT_MS);
20789
+ const staleAfterMs = staleMs ?? STALE_LOCK_MS;
20790
+ mkdirSync3(path4.dirname(binaryPath), { recursive: true });
20791
+ while (Date.now() <= deadline) {
20792
+ try {
20793
+ mkdirSync3(lock);
20794
+ try {
20795
+ return await run();
20796
+ } finally {
20797
+ try {
20798
+ rmSync2(lock, { recursive: true, force: true });
20799
+ } catch {}
20800
+ }
20801
+ } catch (err) {
20802
+ const code = err.code;
20803
+ if (code !== "EEXIST")
20804
+ throw err;
20805
+ try {
20806
+ const ageMs = Date.now() - statSync2(lock).mtimeMs;
20807
+ if (ageMs > staleAfterMs) {
20808
+ rmSync2(lock, { recursive: true, force: true });
20809
+ log("[companion] removed stale install lock", lock);
20810
+ continue;
20811
+ }
20812
+ } catch (statErr) {
20813
+ const statCode = statErr.code;
20814
+ if (statCode !== "ENOENT")
20815
+ throw statErr;
20816
+ }
20817
+ await delay(25);
20818
+ }
20819
+ }
20820
+ log("[companion] install lock timed out", lock);
20821
+ return {
20822
+ status: "failed",
20823
+ binaryPath,
20824
+ error: "Timed out waiting for companion install lock"
20825
+ };
20826
+ }
20827
+ function companionArchiveName(version, target, isWindows = process.platform === "win32") {
20828
+ const ext = isWindows ? "zip" : "tar.gz";
20829
+ return `oh-my-opencode-slim-companion-v${version}-${target}.${ext}`;
20830
+ }
20831
+ function compareSemver(a, b) {
20832
+ const left = parseSemver(a);
20833
+ const right = parseSemver(b);
20834
+ if (!left || !right)
20835
+ return a.localeCompare(b);
20836
+ for (let i = 0;i < 3; i++) {
20837
+ const diff = left[i] - right[i];
20838
+ if (diff !== 0)
20839
+ return diff;
20840
+ }
20841
+ return 0;
20842
+ }
20843
+ function parseSemver(version) {
20844
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
20845
+ if (!match)
20846
+ return null;
20847
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
20848
+ }
20849
+ function formatError(err) {
20850
+ return err instanceof Error ? err.message : String(err);
20301
20851
  }
20302
20852
 
20303
20853
  // src/config/runtime-preset.ts
@@ -20636,7 +21186,7 @@ function ensureApplyPatchError(error, context) {
20636
21186
 
20637
21187
  // src/hooks/apply-patch/execution-context.ts
20638
21188
  import * as fs3 from "node:fs/promises";
20639
- import path4 from "node:path";
21189
+ import path5 from "node:path";
20640
21190
 
20641
21191
  // src/hooks/apply-patch/codec.ts
20642
21192
  function normalizeLineEndings(text) {
@@ -21505,7 +22055,7 @@ function isMissingPathError(error) {
21505
22055
  }
21506
22056
  async function real(target) {
21507
22057
  const parts = [];
21508
- let current = path4.resolve(target);
22058
+ let current = path5.resolve(target);
21509
22059
  while (true) {
21510
22060
  const exact = await fs3.realpath(current).catch((error) => {
21511
22061
  if (isMissingPathError(error)) {
@@ -21514,19 +22064,19 @@ async function real(target) {
21514
22064
  throw createApplyPatchInternalError(`Failed to resolve real path: ${current}`, error);
21515
22065
  });
21516
22066
  if (exact) {
21517
- return parts.length === 0 ? exact : path4.join(exact, ...parts.reverse());
22067
+ return parts.length === 0 ? exact : path5.join(exact, ...parts.reverse());
21518
22068
  }
21519
- const parent = path4.dirname(current);
22069
+ const parent = path5.dirname(current);
21520
22070
  if (parent === current) {
21521
- return parts.length === 0 ? current : path4.join(current, ...parts.reverse());
22071
+ return parts.length === 0 ? current : path5.join(current, ...parts.reverse());
21522
22072
  }
21523
- parts.push(path4.basename(current));
22073
+ parts.push(path5.basename(current));
21524
22074
  current = parent;
21525
22075
  }
21526
22076
  }
21527
22077
  function inside(root, target) {
21528
- const rel = path4.relative(root, target);
21529
- return rel === "" || !rel.startsWith("..") && !path4.isAbsolute(rel);
22078
+ const rel = path5.relative(root, target);
22079
+ return rel === "" || !rel.startsWith("..") && !path5.isAbsolute(rel);
21530
22080
  }
21531
22081
  function createPathGuardContext(root, worktree) {
21532
22082
  return {
@@ -21536,7 +22086,7 @@ function createPathGuardContext(root, worktree) {
21536
22086
  };
21537
22087
  }
21538
22088
  async function realCached(ctx, target) {
21539
- const resolvedTarget = path4.resolve(target);
22089
+ const resolvedTarget = path5.resolve(target);
21540
22090
  let pending = ctx.realCache.get(resolvedTarget);
21541
22091
  if (!pending) {
21542
22092
  pending = real(resolvedTarget);
@@ -21587,22 +22137,22 @@ async function assertRegularFile(ctx, filePath, verb) {
21587
22137
  function collectPatchTargets(root, hunks) {
21588
22138
  const targets = new Set;
21589
22139
  for (const hunk of hunks) {
21590
- targets.add(path4.resolve(root, hunk.path));
22140
+ targets.add(path5.resolve(root, hunk.path));
21591
22141
  if (hunk.type === "update" && hunk.move_path) {
21592
- targets.add(path4.resolve(root, hunk.move_path));
22142
+ targets.add(path5.resolve(root, hunk.move_path));
21593
22143
  }
21594
22144
  }
21595
22145
  return [...targets];
21596
22146
  }
21597
22147
  function toRelativePatchPath(root, target) {
21598
- const relative = path4.relative(root, target);
22148
+ const relative = path5.relative(root, target);
21599
22149
  return (relative.length === 0 ? "." : relative).replaceAll("\\", "/");
21600
22150
  }
21601
22151
  function normalizePatchPath(root, value) {
21602
- return path4.isAbsolute(value) ? toRelativePatchPath(root, path4.resolve(value)) : value;
22152
+ return path5.isAbsolute(value) ? toRelativePatchPath(root, path5.resolve(value)) : value;
21603
22153
  }
21604
22154
  function normalizePatchPaths(root, hunks) {
21605
- const resolvedRoot = path4.resolve(root);
22155
+ const resolvedRoot = path5.resolve(root);
21606
22156
  const normalized = [];
21607
22157
  let changed = false;
21608
22158
  for (const hunk of hunks) {
@@ -21726,7 +22276,7 @@ function stageAddedText(contents) {
21726
22276
  `;
21727
22277
  }
21728
22278
  // src/hooks/apply-patch/rewrite.ts
21729
- import path5 from "node:path";
22279
+ import path6 from "node:path";
21730
22280
  function normalizeTextLineEndings(text) {
21731
22281
  return text.replace(/\r\n/g, `
21732
22282
  `).replace(/\r/g, `
@@ -21883,7 +22433,7 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21883
22433
  const dependencyGroups = new Map;
21884
22434
  for (const hunk of hunks) {
21885
22435
  if (hunk.type === "add") {
21886
- const filePath2 = path5.resolve(root, hunk.path);
22436
+ const filePath2 = path6.resolve(root, hunk.path);
21887
22437
  await assertPreparedPathMissing(filePath2, "add");
21888
22438
  rewritten.push(hunk);
21889
22439
  clearDependencyGroup(filePath2);
@@ -21905,20 +22455,20 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21905
22455
  continue;
21906
22456
  }
21907
22457
  if (hunk.type === "delete") {
21908
- const filePath2 = path5.resolve(root, hunk.path);
22458
+ const filePath2 = path6.resolve(root, hunk.path);
21909
22459
  await getPreparedFileState(filePath2, "delete");
21910
22460
  clearDependencyGroup(filePath2);
21911
22461
  rewritten.push(hunk);
21912
22462
  staged.set(filePath2, { exists: false, derived: true });
21913
22463
  continue;
21914
22464
  }
21915
- const filePath = path5.resolve(root, hunk.path);
22465
+ const filePath = path6.resolve(root, hunk.path);
21916
22466
  const currentDependency = dependencyGroups.get(filePath);
21917
22467
  const current = await getPreparedFileState(filePath, "update");
21918
22468
  if (!current.exists) {
21919
22469
  throw createApplyPatchVerificationError(`Failed to read file to update: ${filePath}`);
21920
22470
  }
21921
- const movePath = hunk.move_path ? path5.resolve(root, hunk.move_path) : undefined;
22471
+ const movePath = hunk.move_path ? path6.resolve(root, hunk.move_path) : undefined;
21922
22472
  if (movePath && movePath !== filePath) {
21923
22473
  await assertPreparedPathMissing(movePath, "move");
21924
22474
  }
@@ -22083,29 +22633,35 @@ function createApplyPatchHook(ctx) {
22083
22633
  }
22084
22634
  };
22085
22635
  }
22636
+ // src/hooks/auto-update-checker/index.ts
22637
+ import * as path11 from "node:path";
22638
+ init_compat();
22639
+
22086
22640
  // src/hooks/auto-update-checker/cache.ts
22087
22641
  import * as fs5 from "node:fs";
22088
- import * as path8 from "node:path";
22642
+ import * as path9 from "node:path";
22643
+ // src/cli/system.ts
22644
+ init_compat();
22089
22645
  // src/hooks/auto-update-checker/checker.ts
22090
22646
  import * as fs4 from "node:fs";
22091
- import * as path7 from "node:path";
22647
+ import * as path8 from "node:path";
22092
22648
  import { fileURLToPath } from "node:url";
22093
22649
 
22094
22650
  // src/hooks/auto-update-checker/constants.ts
22095
22651
  import * as os3 from "node:os";
22096
- import * as path6 from "node:path";
22652
+ import * as path7 from "node:path";
22097
22653
  var PACKAGE_NAME = "oh-my-opencode-slim";
22098
22654
  var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
22099
22655
  var NPM_PACKAGE_URL = `https://registry.npmjs.org/${PACKAGE_NAME}`;
22100
22656
  var NPM_FETCH_TIMEOUT = 5000;
22101
22657
  function getCacheDir() {
22102
22658
  if (process.platform === "win32") {
22103
- return path6.join(process.env.LOCALAPPDATA ?? os3.homedir(), "opencode");
22659
+ return path7.join(process.env.LOCALAPPDATA ?? os3.homedir(), "opencode");
22104
22660
  }
22105
- return path6.join(os3.homedir(), ".cache", "opencode");
22661
+ return path7.join(os3.homedir(), ".cache", "opencode");
22106
22662
  }
22107
22663
  var CACHE_DIR = getCacheDir();
22108
- var INSTALLED_PACKAGE_JSON = path6.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
22664
+ var INSTALLED_PACKAGE_JSON = path7.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
22109
22665
  var configPaths = getOpenCodeConfigPaths();
22110
22666
  var USER_OPENCODE_CONFIG = configPaths[0];
22111
22667
  var USER_OPENCODE_CONFIG_JSONC = configPaths[1];
@@ -22217,8 +22773,8 @@ function extractChannel(version) {
22217
22773
  }
22218
22774
  function getConfigPaths(directory) {
22219
22775
  return [
22220
- path7.join(directory, ".opencode", "opencode.json"),
22221
- path7.join(directory, ".opencode", "opencode.jsonc"),
22776
+ path8.join(directory, ".opencode", "opencode.json"),
22777
+ path8.join(directory, ".opencode", "opencode.jsonc"),
22222
22778
  USER_OPENCODE_CONFIG,
22223
22779
  USER_OPENCODE_CONFIG_JSONC
22224
22780
  ];
@@ -22247,9 +22803,9 @@ function getLocalDevPath(directory) {
22247
22803
  function findPackageJsonUp(startPath) {
22248
22804
  try {
22249
22805
  const stat2 = fs4.statSync(startPath);
22250
- let dir = stat2.isDirectory() ? startPath : path7.dirname(startPath);
22806
+ let dir = stat2.isDirectory() ? startPath : path8.dirname(startPath);
22251
22807
  for (let i = 0;i < 10; i++) {
22252
- const pkgPath = path7.join(dir, "package.json");
22808
+ const pkgPath = path8.join(dir, "package.json");
22253
22809
  if (fs4.existsSync(pkgPath)) {
22254
22810
  try {
22255
22811
  const content = fs4.readFileSync(pkgPath, "utf-8");
@@ -22258,7 +22814,7 @@ function findPackageJsonUp(startPath) {
22258
22814
  return pkgPath;
22259
22815
  } catch {}
22260
22816
  }
22261
- const parent = path7.dirname(dir);
22817
+ const parent = path8.dirname(dir);
22262
22818
  if (parent === dir)
22263
22819
  break;
22264
22820
  dir = parent;
@@ -22283,7 +22839,7 @@ function getLocalDevVersion(directory) {
22283
22839
  }
22284
22840
  function getCurrentRuntimePackageJsonPath(currentModuleUrl = import.meta.url) {
22285
22841
  try {
22286
- const currentDir = path7.dirname(fileURLToPath(currentModuleUrl));
22842
+ const currentDir = path8.dirname(fileURLToPath(currentModuleUrl));
22287
22843
  return findPackageJsonUp(currentDir);
22288
22844
  } catch (err) {
22289
22845
  log("[auto-update-checker] Failed to resolve runtime package path:", err);
@@ -22442,7 +22998,7 @@ function getBlockingMajorVersion(current, candidates) {
22442
22998
 
22443
22999
  // src/hooks/auto-update-checker/cache.ts
22444
23000
  function removeFromBunLock(installDir, packageName) {
22445
- const lockPath = path8.join(installDir, "bun.lock");
23001
+ const lockPath = path9.join(installDir, "bun.lock");
22446
23002
  if (!fs5.existsSync(lockPath))
22447
23003
  return false;
22448
23004
  try {
@@ -22493,7 +23049,7 @@ function ensureDependencyVersion(packageJsonPath, packageName, version) {
22493
23049
  }
22494
23050
  }
22495
23051
  function removeInstalledPackage(installDir, packageName) {
22496
- const pkgDir = path8.join(installDir, "node_modules", packageName);
23052
+ const pkgDir = path9.join(installDir, "node_modules", packageName);
22497
23053
  if (!fs5.existsSync(pkgDir))
22498
23054
  return false;
22499
23055
  fs5.rmSync(pkgDir, { recursive: true, force: true });
@@ -22502,18 +23058,18 @@ function removeInstalledPackage(installDir, packageName) {
22502
23058
  }
22503
23059
  function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
22504
23060
  if (runtimePackageJsonPath) {
22505
- const packageDir = path8.dirname(runtimePackageJsonPath);
22506
- const nodeModulesDir = path8.dirname(packageDir);
22507
- if (path8.basename(packageDir) === PACKAGE_NAME && path8.basename(nodeModulesDir) === "node_modules") {
22508
- const installDir = path8.dirname(nodeModulesDir);
22509
- const packageJsonPath = path8.join(installDir, "package.json");
23061
+ const packageDir = path9.dirname(runtimePackageJsonPath);
23062
+ const nodeModulesDir = path9.dirname(packageDir);
23063
+ if (path9.basename(packageDir) === PACKAGE_NAME && path9.basename(nodeModulesDir) === "node_modules") {
23064
+ const installDir = path9.dirname(nodeModulesDir);
23065
+ const packageJsonPath = path9.join(installDir, "package.json");
22510
23066
  if (fs5.existsSync(packageJsonPath)) {
22511
23067
  return { installDir, packageJsonPath };
22512
23068
  }
22513
23069
  }
22514
23070
  return null;
22515
23071
  }
22516
- const legacyPackageJsonPath = path8.join(CACHE_DIR, "package.json");
23072
+ const legacyPackageJsonPath = path9.join(CACHE_DIR, "package.json");
22517
23073
  if (fs5.existsSync(legacyPackageJsonPath)) {
22518
23074
  return { installDir: CACHE_DIR, packageJsonPath: legacyPackageJsonPath };
22519
23075
  }
@@ -22542,9 +23098,136 @@ function preparePackageUpdate(version, packageName = PACKAGE_NAME, runtimePackag
22542
23098
  }
22543
23099
  }
22544
23100
 
23101
+ // src/hooks/auto-update-checker/skill-sync.ts
23102
+ import {
23103
+ copyFileSync as copyFileSync2,
23104
+ existsSync as existsSync6,
23105
+ lstatSync,
23106
+ mkdirSync as mkdirSync4,
23107
+ mkdtempSync as mkdtempSync2,
23108
+ readdirSync as readdirSync2,
23109
+ renameSync as renameSync3,
23110
+ rmSync as rmSync4
23111
+ } from "node:fs";
23112
+ import * as path10 from "node:path";
23113
+ function copyDirRecursive(src, dest) {
23114
+ const stat2 = lstatSync(src);
23115
+ if (stat2.isSymbolicLink()) {
23116
+ return;
23117
+ }
23118
+ if (stat2.isDirectory()) {
23119
+ mkdirSync4(dest, { recursive: true });
23120
+ const entries = readdirSync2(src);
23121
+ for (const entry of entries) {
23122
+ copyDirRecursive(path10.join(src, entry), path10.join(dest, entry));
23123
+ }
23124
+ } else if (stat2.isFile()) {
23125
+ const destDir = path10.dirname(dest);
23126
+ if (!existsSync6(destDir)) {
23127
+ mkdirSync4(destDir, { recursive: true });
23128
+ }
23129
+ copyFileSync2(src, dest);
23130
+ }
23131
+ }
23132
+ function syncBundledSkillsFromPackage(packageRoot) {
23133
+ const installed = [];
23134
+ const skippedExisting = [];
23135
+ const failed = [];
23136
+ const sourceSkillsDir = path10.join(packageRoot, "src", "skills");
23137
+ try {
23138
+ const stat2 = lstatSync(sourceSkillsDir);
23139
+ if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
23140
+ log(`[skill-sync] Source skills directory is not a valid directory: ${sourceSkillsDir}`);
23141
+ return { installed, skippedExisting, failed };
23142
+ }
23143
+ } catch {
23144
+ log(`[skill-sync] Source skills directory does not exist or is unreadable: ${sourceSkillsDir}`);
23145
+ return { installed, skippedExisting, failed };
23146
+ }
23147
+ const destSkillsDir = path10.join(getConfigDir(), "skills");
23148
+ try {
23149
+ if (!existsSync6(destSkillsDir)) {
23150
+ mkdirSync4(destSkillsDir, { recursive: true });
23151
+ }
23152
+ } catch (err) {
23153
+ log(`[skill-sync] Failed to create destination skills directory: ${destSkillsDir}`, err);
23154
+ }
23155
+ let entries = [];
23156
+ try {
23157
+ entries = readdirSync2(sourceSkillsDir);
23158
+ } catch (err) {
23159
+ log(`[skill-sync] Failed to read source skills directory: ${sourceSkillsDir}`, err);
23160
+ return { installed, skippedExisting, failed };
23161
+ }
23162
+ for (const entry of entries) {
23163
+ const entryPath = path10.join(sourceSkillsDir, entry);
23164
+ try {
23165
+ if (entry.startsWith(".")) {
23166
+ continue;
23167
+ }
23168
+ const entryStat = lstatSync(entryPath);
23169
+ if (entryStat.isSymbolicLink() || !entryStat.isDirectory()) {
23170
+ continue;
23171
+ }
23172
+ const skillMdPath = path10.join(entryPath, "SKILL.md");
23173
+ try {
23174
+ const skillMdStat = lstatSync(skillMdPath);
23175
+ if (skillMdStat.isSymbolicLink() || !skillMdStat.isFile()) {
23176
+ continue;
23177
+ }
23178
+ } catch {
23179
+ continue;
23180
+ }
23181
+ const destPath = path10.join(destSkillsDir, entry);
23182
+ let destExists = false;
23183
+ try {
23184
+ lstatSync(destPath);
23185
+ destExists = true;
23186
+ } catch {}
23187
+ if (destExists) {
23188
+ log(`[skill-sync] Skill already exists in destination: ${entry}`);
23189
+ skippedExisting.push(entry);
23190
+ continue;
23191
+ }
23192
+ const stagingDir = mkdtempSync2(path10.join(destSkillsDir, `.sync-staging-${entry}-`));
23193
+ try {
23194
+ copyDirRecursive(entryPath, stagingDir);
23195
+ let destExistsLate = false;
23196
+ try {
23197
+ lstatSync(destPath);
23198
+ destExistsLate = true;
23199
+ } catch {}
23200
+ if (destExistsLate) {
23201
+ log(`[skill-sync] Destination path was created during staging for ${entry}, skipping promotion.`);
23202
+ skippedExisting.push(entry);
23203
+ } else {
23204
+ renameSync3(stagingDir, destPath);
23205
+ installed.push(entry);
23206
+ log(`[skill-sync] Successfully synced skill: ${entry}`);
23207
+ }
23208
+ } catch (err) {
23209
+ log(`[skill-sync] Failed to sync skill ${entry}:`, err);
23210
+ failed.push(entry);
23211
+ } finally {
23212
+ try {
23213
+ if (existsSync6(stagingDir)) {
23214
+ rmSync4(stagingDir, { recursive: true, force: true });
23215
+ }
23216
+ } catch (err) {
23217
+ log(`[skill-sync] Failed to clean up staging directory ${stagingDir}:`, err);
23218
+ }
23219
+ }
23220
+ } catch (err) {
23221
+ log(`[skill-sync] Error processing source entry ${entry}:`, err);
23222
+ failed.push(entry);
23223
+ }
23224
+ }
23225
+ return { installed, skippedExisting, failed };
23226
+ }
23227
+
22545
23228
  // src/hooks/auto-update-checker/index.ts
22546
23229
  function createAutoUpdateCheckerHook(ctx, options = {}) {
22547
- const { autoUpdate = true } = options;
23230
+ const { autoUpdate = true, companion } = options;
22548
23231
  let hasChecked = false;
22549
23232
  return {
22550
23233
  event: ({ event }) => {
@@ -22562,14 +23245,14 @@ function createAutoUpdateCheckerHook(ctx, options = {}) {
22562
23245
  log("[auto-update-checker] Local development mode");
22563
23246
  return;
22564
23247
  }
22565
- runBackgroundUpdateCheck(ctx, autoUpdate).catch((err) => {
23248
+ runBackgroundUpdateCheck(ctx, autoUpdate, companion).catch((err) => {
22566
23249
  log("[auto-update-checker] Background update check failed:", err);
22567
23250
  });
22568
23251
  }, 0);
22569
23252
  }
22570
23253
  };
22571
23254
  }
22572
- async function runBackgroundUpdateCheck(ctx, autoUpdate) {
23255
+ async function runBackgroundUpdateCheck(ctx, autoUpdate, companion) {
22573
23256
  const pluginInfo = findPluginEntry(ctx.directory);
22574
23257
  if (!pluginInfo) {
22575
23258
  log("[auto-update-checker] Plugin not found in config");
@@ -22624,8 +23307,54 @@ Version is pinned. Update your plugin config to apply.`, "info", 8000);
22624
23307
  }
22625
23308
  const installSuccess = await runBunInstallSafe(installDir);
22626
23309
  if (installSuccess) {
22627
- showToast(ctx, "OMO-Slim Updated!", `v${currentVersion} → v${latestVersion}
22628
- Restart OpenCode to apply.`, "success", 8000);
23310
+ let installedSkills = [];
23311
+ let companionUpdated = false;
23312
+ let companionWillRetry = false;
23313
+ const packageRoot = path11.join(installDir, "node_modules", PACKAGE_NAME);
23314
+ try {
23315
+ const syncResult = syncBundledSkillsFromPackage(packageRoot);
23316
+ installedSkills = syncResult.installed;
23317
+ if (syncResult.failed.length > 0) {
23318
+ log(`[auto-update-checker] Skill sync warnings/failures: ${syncResult.failed.join(", ")}`);
23319
+ }
23320
+ if (syncResult.skippedExisting.length > 0) {
23321
+ log(`[auto-update-checker] Skill sync skipped existing: ${syncResult.skippedExisting.join(", ")}`);
23322
+ }
23323
+ } catch (err) {
23324
+ log("[auto-update-checker] Skill sync failed silently:", err);
23325
+ }
23326
+ if (companion?.enabled === true) {
23327
+ try {
23328
+ const manifest = loadCompanionManifestFromPackageRoot(packageRoot);
23329
+ const companionResult = await ensureCompanionVersion({
23330
+ config: companion,
23331
+ manifest: manifest ?? undefined
23332
+ });
23333
+ if (companionResult.status === "installed") {
23334
+ companionUpdated = true;
23335
+ } else if (companionResult.status === "failed") {
23336
+ companionWillRetry = true;
23337
+ log("[auto-update-checker] Companion update failed; will retry on restart:", companionResult.error);
23338
+ } else if (companionResult.status === "skipped") {
23339
+ log("[auto-update-checker] Companion update skipped:", companionResult.reason);
23340
+ }
23341
+ } catch (err) {
23342
+ companionWillRetry = true;
23343
+ log("[auto-update-checker] Companion update failed silently; will retry on restart:", err);
23344
+ }
23345
+ }
23346
+ const messageLines = [`v${currentVersion} → v${latestVersion}`];
23347
+ if (installedSkills.length > 0) {
23348
+ messageLines.push(`Added bundled skills: ${installedSkills.join(", ")}`);
23349
+ }
23350
+ if (companionUpdated) {
23351
+ messageLines.push("Companion updated.");
23352
+ } else if (companionWillRetry) {
23353
+ messageLines.push("Companion update will retry on restart.");
23354
+ }
23355
+ messageLines.push("Restart OpenCode to apply.");
23356
+ showToast(ctx, "OMO-Slim Updated!", messageLines.join(`
23357
+ `), "success", 8000);
22629
23358
  log(`[auto-update-checker] Update installed: ${currentVersion} → ${latestVersion}`);
22630
23359
  } else {
22631
23360
  showToast(ctx, `OMO-Slim ${latestVersion}`, `v${latestVersion} available, but auto-update failed to install it. Check logs or retry manually.`, "error", 8000);
@@ -22728,10 +23457,6 @@ function createDisplayNameMentionRewriter(config) {
22728
23457
  };
22729
23458
  }
22730
23459
  // src/utils/task.ts
22731
- var TRANSIENT_PROCESS_ERROR_TEXT = new Set([
22732
- "Task is not running in this process and has no final output.",
22733
- "Task is not running in this process and has not produced output."
22734
- ]);
22735
23460
  function parseTaskIdFromTaskOutput(output) {
22736
23461
  const xmlMatch = /<task\s+[^>]*\bid=["']([^"']+)["'][^>]*>/i.exec(output);
22737
23462
  if (xmlMatch)
@@ -23012,7 +23737,7 @@ class BackgroundJobBoard {
23012
23737
  existing.set(file.path, { ...file });
23013
23738
  }
23014
23739
  }
23015
- const contextFiles = [...existing.values()].filter((file) => file.lineCount >= this.readContextMinLines).sort((a, b) => b.lastReadAt - a.lastReadAt).slice(0, this.readContextMaxFiles + 1);
23740
+ const contextFiles = [...existing.values()].filter((file) => file.lineCount >= this.readContextMinLines).sort((a, b) => b.lineCount - a.lineCount || b.lastReadAt - a.lastReadAt || a.path.localeCompare(b.path)).slice(0, this.readContextMaxFiles + 1);
23016
23741
  this.jobs.set(taskID, { ...job, contextFiles });
23017
23742
  }
23018
23743
  list(parentSessionID) {
@@ -23134,11 +23859,13 @@ function normalizeCancelReason(reason) {
23134
23859
  const normalized = reason?.replace(/\s+/g, " ").trim();
23135
23860
  return normalized ? `cancelled: ${normalized}` : "cancelled";
23136
23861
  }
23137
- // src/utils/internal-initiator.ts
23138
- var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
23862
+ // src/utils/guards.ts
23139
23863
  function isRecord(value) {
23140
23864
  return typeof value === "object" && value !== null;
23141
23865
  }
23866
+
23867
+ // src/utils/internal-initiator.ts
23868
+ var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
23142
23869
  function createInternalAgentTextPart(text) {
23143
23870
  return {
23144
23871
  type: "text",
@@ -23155,86 +23882,10 @@ function hasInternalInitiatorMarker(part) {
23155
23882
  }
23156
23883
  return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
23157
23884
  }
23158
- // src/utils/zip-extractor.ts
23159
- import { spawnSync } from "node:child_process";
23160
- import { release } from "node:os";
23161
- var WINDOWS_BUILD_WITH_TAR = 17134;
23162
- function getWindowsBuildNumber() {
23163
- if (process.platform !== "win32")
23164
- return null;
23165
- const parts = release().split(".");
23166
- if (parts.length >= 3) {
23167
- const build = parseInt(parts[2], 10);
23168
- if (!Number.isNaN(build))
23169
- return build;
23170
- }
23171
- return null;
23172
- }
23173
- function isPwshAvailable() {
23174
- if (process.platform !== "win32")
23175
- return false;
23176
- const result = spawnSync("where", ["pwsh"], {
23177
- stdio: ["ignore", "pipe", "pipe"]
23178
- });
23179
- return result.status === 0;
23180
- }
23181
- function escapePowerShellPath(path9) {
23182
- return path9.replace(/'/g, "''");
23183
- }
23184
- function getWindowsZipExtractor() {
23185
- const buildNumber = getWindowsBuildNumber();
23186
- if (buildNumber !== null && buildNumber >= WINDOWS_BUILD_WITH_TAR) {
23187
- return "tar";
23188
- }
23189
- if (isPwshAvailable()) {
23190
- return "pwsh";
23191
- }
23192
- return "powershell";
23193
- }
23194
- async function extractZip(archivePath, destDir) {
23195
- let proc;
23196
- if (process.platform === "win32") {
23197
- const extractor = getWindowsZipExtractor();
23198
- switch (extractor) {
23199
- case "tar":
23200
- proc = crossSpawn(["tar", "-xf", archivePath, "-C", destDir], {
23201
- stdout: "ignore",
23202
- stderr: "pipe"
23203
- });
23204
- break;
23205
- case "pwsh":
23206
- proc = crossSpawn([
23207
- "pwsh",
23208
- "-Command",
23209
- `Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
23210
- ], {
23211
- stdout: "ignore",
23212
- stderr: "pipe"
23213
- });
23214
- break;
23215
- default:
23216
- proc = crossSpawn([
23217
- "powershell",
23218
- "-Command",
23219
- `Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
23220
- ], {
23221
- stdout: "ignore",
23222
- stderr: "pipe"
23223
- });
23224
- break;
23225
- }
23226
- } else {
23227
- proc = crossSpawn(["unzip", "-o", archivePath, "-d", destDir], {
23228
- stdout: "ignore",
23229
- stderr: "pipe"
23230
- });
23231
- }
23232
- const exitCode = await proc.exited;
23233
- if (exitCode !== 0) {
23234
- const stderr = await proc.stderr();
23235
- throw new Error(`zip extraction failed (exit ${exitCode}): ${stderr}`);
23236
- }
23237
- }
23885
+
23886
+ // src/utils/index.ts
23887
+ init_zip_extractor();
23888
+
23238
23889
  // src/hooks/chat-headers.ts
23239
23890
  var INTERNAL_MARKER_CACHE_LIMIT = 1000;
23240
23891
  var internalMarkerCache = new Map;
@@ -23392,7 +24043,7 @@ function detectDelegateTaskError(output) {
23392
24043
  return null;
23393
24044
  }
23394
24045
 
23395
- // src/hooks/delegate-task-retry/guidance.ts
24046
+ // src/hooks/delegate-task-retry/hook.ts
23396
24047
  function extractAvailableList(output) {
23397
24048
  const match = output.match(/Allowed agents:\s*(.+)$/m);
23398
24049
  if (match)
@@ -23422,7 +24073,6 @@ function buildRetryGuidance(errorInfo) {
23422
24073
  return lines.join(`
23423
24074
  `);
23424
24075
  }
23425
- // src/hooks/delegate-task-retry/hook.ts
23426
24076
  function createDelegateTaskRetryHook(_ctx) {
23427
24077
  return {
23428
24078
  "tool.execute.after": async (input, output) => {
@@ -23548,12 +24198,6 @@ function isRateLimitError(error) {
23548
24198
  ].join(" ");
23549
24199
  return RATE_LIMIT_PATTERNS.some((p) => p.test(text));
23550
24200
  }
23551
- function parseModel(model) {
23552
- const slash = model.indexOf("/");
23553
- if (slash <= 0 || slash >= model.length - 1)
23554
- return null;
23555
- return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
23556
- }
23557
24201
  var DEDUP_WINDOW_MS = 5000;
23558
24202
  var REPROMPT_DELAY_MS = 500;
23559
24203
 
@@ -23671,7 +24315,7 @@ class ForegroundFallbackManager {
23671
24315
  return;
23672
24316
  }
23673
24317
  tried.add(nextModel);
23674
- const ref = parseModel(nextModel);
24318
+ const ref = parseModelReference(nextModel);
23675
24319
  if (!ref) {
23676
24320
  log("[foreground-fallback] invalid model format", {
23677
24321
  sessionID,
@@ -23746,17 +24390,17 @@ class ForegroundFallbackManager {
23746
24390
  }
23747
24391
  }
23748
24392
  // src/hooks/image-hook.ts
23749
- import { createHash } from "node:crypto";
24393
+ import { createHash as createHash2 } from "node:crypto";
23750
24394
  import {
23751
- existsSync as existsSync5,
23752
- mkdirSync as mkdirSync3,
23753
- readdirSync as readdirSync2,
24395
+ existsSync as existsSync7,
24396
+ mkdirSync as mkdirSync5,
24397
+ readdirSync as readdirSync3,
23754
24398
  rmdirSync,
23755
- statSync as statSync3,
24399
+ statSync as statSync4,
23756
24400
  unlinkSync as unlinkSync2,
23757
- writeFileSync as writeFileSync4
24401
+ writeFileSync as writeFileSync5
23758
24402
  } from "node:fs";
23759
- import { basename as basename2, extname, join as join8 } from "node:path";
24403
+ import { basename as basename2, extname, join as join11 } from "node:path";
23760
24404
  var lastCleanupByDir = new Map;
23761
24405
  var CLEANUP_INTERVAL = 10 * 60 * 1000;
23762
24406
  function isImagePart(p) {
@@ -23803,13 +24447,13 @@ function cleanupAllSessions(saveDir) {
23803
24447
  const maxAge = 60 * 60 * 1000;
23804
24448
  const dirsToScan = [];
23805
24449
  try {
23806
- for (const entry of readdirSync2(saveDir, { withFileTypes: true })) {
23807
- const fp = join8(saveDir, entry.name);
24450
+ for (const entry of readdirSync3(saveDir, { withFileTypes: true })) {
24451
+ const fp = join11(saveDir, entry.name);
23808
24452
  if (entry.isDirectory()) {
23809
24453
  dirsToScan.push(fp);
23810
24454
  } else {
23811
24455
  try {
23812
- if (now - statSync3(fp).mtimeMs > maxAge)
24456
+ if (now - statSync4(fp).mtimeMs > maxAge)
23813
24457
  unlinkSync2(fp);
23814
24458
  } catch {}
23815
24459
  }
@@ -23819,11 +24463,11 @@ function cleanupAllSessions(saveDir) {
23819
24463
  try {
23820
24464
  let isEmpty = true;
23821
24465
  let allRemoved = true;
23822
- for (const f of readdirSync2(dir)) {
24466
+ for (const f of readdirSync3(dir)) {
23823
24467
  isEmpty = false;
23824
- const fp = join8(dir, f);
24468
+ const fp = join11(dir, f);
23825
24469
  try {
23826
- if (now - statSync3(fp).mtimeMs > maxAge) {
24470
+ if (now - statSync4(fp).mtimeMs > maxAge) {
23827
24471
  unlinkSync2(fp);
23828
24472
  } else {
23829
24473
  allRemoved = false;
@@ -23843,20 +24487,20 @@ function cleanupAllSessions(saveDir) {
23843
24487
  function writeUniqueFile(dir, name, data, log2) {
23844
24488
  const ext = extname(name);
23845
24489
  const base = basename2(name, ext) || name;
23846
- let candidate = join8(dir, name);
23847
- if (existsSync5(candidate)) {
24490
+ let candidate = join11(dir, name);
24491
+ if (existsSync7(candidate)) {
23848
24492
  return candidate;
23849
24493
  }
23850
24494
  let counter = 0;
23851
24495
  const MAX_ATTEMPTS = 1000;
23852
24496
  for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
23853
24497
  try {
23854
- writeFileSync4(candidate, data, { flag: "wx" });
24498
+ writeFileSync5(candidate, data, { flag: "wx" });
23855
24499
  return candidate;
23856
24500
  } catch (e) {
23857
24501
  if (e instanceof Error && e.code === "EEXIST") {
23858
24502
  counter += 1;
23859
- candidate = join8(dir, `${base}-${counter}${ext}`);
24503
+ candidate = join11(dir, `${base}-${counter}${ext}`);
23860
24504
  continue;
23861
24505
  }
23862
24506
  log2(`[image-hook] failed to save image: ${e}`);
@@ -23880,17 +24524,17 @@ function processImageAttachments(args) {
23880
24524
  messagesWithImages.push({ msg, imageParts });
23881
24525
  }
23882
24526
  }
23883
- const saveDir = join8(workDir, ".opencode", "images");
24527
+ const saveDir = join11(workDir, ".opencode", "images");
23884
24528
  if (messagesWithImages.length === 0) {
23885
- if (existsSync5(saveDir))
24529
+ if (existsSync7(saveDir))
23886
24530
  cleanupAllSessions(saveDir);
23887
24531
  return;
23888
24532
  }
23889
- const gitignorePath = join8(workDir, ".opencode", ".gitignore");
24533
+ const gitignorePath = join11(workDir, ".opencode", ".gitignore");
23890
24534
  try {
23891
- mkdirSync3(saveDir, { recursive: true });
23892
- if (!existsSync5(gitignorePath))
23893
- writeFileSync4(gitignorePath, `*
24535
+ mkdirSync5(saveDir, { recursive: true });
24536
+ if (!existsSync7(gitignorePath))
24537
+ writeFileSync5(gitignorePath, `*
23894
24538
  `);
23895
24539
  } catch (e) {
23896
24540
  log2(`[image-hook] failed to create image directory: ${e}`);
@@ -23898,9 +24542,9 @@ function processImageAttachments(args) {
23898
24542
  cleanupAllSessions(saveDir);
23899
24543
  for (const { msg, imageParts } of messagesWithImages) {
23900
24544
  const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
23901
- const targetDir = sessionSubdir ? join8(saveDir, sessionSubdir) : saveDir;
24545
+ const targetDir = sessionSubdir ? join11(saveDir, sessionSubdir) : saveDir;
23902
24546
  try {
23903
- mkdirSync3(targetDir, { recursive: true });
24547
+ mkdirSync5(targetDir, { recursive: true });
23904
24548
  } catch (e) {
23905
24549
  log2(`[image-hook] failed to create target image directory: ${e}`);
23906
24550
  }
@@ -23911,7 +24555,7 @@ function processImageAttachments(args) {
23911
24555
  if (url) {
23912
24556
  const decoded = decodeDataUrl(url);
23913
24557
  if (decoded) {
23914
- const hash = createHash("sha1").update(decoded.data).digest("hex").slice(0, 8);
24558
+ const hash = createHash2("sha1").update(decoded.data).digest("hex").slice(0, 8);
23915
24559
  const sanitizedFilename = filename ? sanitizeFilename(filename) : undefined;
23916
24560
  const baseName = sanitizedFilename ? sanitizedFilename.replace(/\.[^.]+$/, "") || "image" : "image";
23917
24561
  const ext = sanitizedFilename ? extname(sanitizedFilename) || extFromMime(decoded.mime) : extFromMime(decoded.mime);
@@ -23984,7 +24628,6 @@ ${JSON_ERROR_REMINDER}`;
23984
24628
  };
23985
24629
  }
23986
24630
  // src/hooks/phase-reminder/index.ts
23987
- var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
23988
24631
  function createPhaseReminderHook() {
23989
24632
  return {
23990
24633
  "experimental.chat.messages.transform": async (_input, output) => {
@@ -24026,24 +24669,18 @@ function createPhaseReminderHook() {
24026
24669
  };
24027
24670
  }
24028
24671
  // src/hooks/post-file-tool-nudge/index.ts
24029
- var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
24030
24672
  var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
24031
24673
  function createPostFileToolNudgeHook(options = {}) {
24032
24674
  function appendReminder(output) {
24033
24675
  if (typeof output.output !== "string") {
24034
24676
  return;
24035
24677
  }
24036
- if (output.output.includes(POST_FILE_TOOL_NUDGE)) {
24678
+ if (output.output.includes(PHASE_REMINDER)) {
24037
24679
  return;
24038
24680
  }
24039
- output.output = [
24040
- output.output,
24041
- "",
24042
- "<internal_reminder>",
24043
- POST_FILE_TOOL_NUDGE,
24044
- "</internal_reminder>"
24045
- ].join(`
24046
- `);
24681
+ output.output = `${output.output}
24682
+
24683
+ ${PHASE_REMINDER}`;
24047
24684
  }
24048
24685
  return {
24049
24686
  "tool.execute.after": async (input, output) => {
@@ -24057,8 +24694,59 @@ function createPostFileToolNudgeHook(options = {}) {
24057
24694
  }
24058
24695
  };
24059
24696
  }
24697
+ // src/hooks/reflect/index.ts
24698
+ var COMMAND_NAME2 = "reflect";
24699
+ function activationPrompt2(focus) {
24700
+ const focusBlock = focus ? ["Focus:", focus] : [
24701
+ "Focus:",
24702
+ "Review recent work broadly and identify repeated workflow friction worth improving."
24703
+ ];
24704
+ return [
24705
+ "Use the reflect skill for this request.",
24706
+ "",
24707
+ "Reflect requirements:",
24708
+ "- inspect existing skills, commands, agents, prompt overrides, MCP permissions, config, and project playbooks before suggesting anything new;",
24709
+ "- find repeated workflow patterns from the current conversation, project notes, local memories, logs, or session artifacts that are available and safe to inspect;",
24710
+ "- prefer evidence from repeated recent behavior over speculation;",
24711
+ "- recommend the smallest useful improvement: prompt/config rule, skill, command, custom agent, MCP/tool permission change, project playbook, or skip;",
24712
+ "- treat creating nothing as a valid result when evidence is weak;",
24713
+ "- ask before changing prompts, skills, commands, agents, MCP access, or config unless the user explicitly requested the exact edit;",
24714
+ "- return a compact report with findings, recommended changes, skipped candidates, and items needing more evidence.",
24715
+ "",
24716
+ ...focusBlock
24717
+ ].join(`
24718
+ `);
24719
+ }
24720
+ function createReflectCommandHook() {
24721
+ let shouldHandleCommand = false;
24722
+ return {
24723
+ registerCommand: (opencodeConfig) => {
24724
+ const commandConfig = opencodeConfig.command;
24725
+ if (commandConfig?.[COMMAND_NAME2]) {
24726
+ shouldHandleCommand = false;
24727
+ return;
24728
+ }
24729
+ if (!opencodeConfig.command)
24730
+ opencodeConfig.command = {};
24731
+ opencodeConfig.command[COMMAND_NAME2] = {
24732
+ template: "Review repeated work and suggest workflow improvements",
24733
+ description: "Use reflect to learn from repeated workflows and suggest reusable improvements"
24734
+ };
24735
+ shouldHandleCommand = true;
24736
+ },
24737
+ handleCommandExecuteBefore: async (input, output) => {
24738
+ if (input.command !== COMMAND_NAME2 || !shouldHandleCommand)
24739
+ return;
24740
+ output.parts.length = 0;
24741
+ output.parts.push({
24742
+ type: "text",
24743
+ text: activationPrompt2(input.arguments.trim())
24744
+ });
24745
+ }
24746
+ };
24747
+ }
24060
24748
  // src/hooks/task-session-manager/index.ts
24061
- import path9 from "node:path";
24749
+ import path12 from "node:path";
24062
24750
  var AGENT_NAME_SET = new Set([
24063
24751
  "orchestrator",
24064
24752
  "oracle",
@@ -24104,9 +24792,6 @@ function createOccurrenceId(part, message, partIndex) {
24104
24792
  function isAgentName(value) {
24105
24793
  return typeof value === "string" && AGENT_NAME_SET.has(value);
24106
24794
  }
24107
- function isObjectRecord(value) {
24108
- return typeof value === "object" && value !== null;
24109
- }
24110
24795
  function extractPath(output) {
24111
24796
  return /<path>([^<]+)<\/path>/.exec(output)?.[1];
24112
24797
  }
@@ -24115,8 +24800,8 @@ function extractTaskSummary(output) {
24115
24800
  return summary?.trim() || undefined;
24116
24801
  }
24117
24802
  function normalizePath(root, file) {
24118
- const relative = path9.relative(root, file);
24119
- if (!relative || relative.startsWith("..") || path9.isAbsolute(relative)) {
24803
+ const relative = path12.relative(root, file);
24804
+ if (!relative || relative.startsWith("..") || path12.isAbsolute(relative)) {
24120
24805
  return file;
24121
24806
  }
24122
24807
  return relative;
@@ -24384,7 +25069,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24384
25069
  if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
24385
25070
  return;
24386
25071
  }
24387
- if (!isObjectRecord(output.args))
25072
+ if (!isRecord(output.args))
24388
25073
  return;
24389
25074
  const args = output.args;
24390
25075
  if (!isAgentName(args.subagent_type)) {
@@ -24658,7 +25343,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24658
25343
  result: status.result
24659
25344
  });
24660
25345
  output.output = formatCancelledTaskStatusOutput(status.taskID, existing?.resultSummary);
24661
- if (isObjectRecord(output) && isObjectRecord(output.metadata)) {
25346
+ if (isRecord(output) && isRecord(output.metadata)) {
24662
25347
  output.metadata.state = "cancelled";
24663
25348
  }
24664
25349
  }
@@ -24682,7 +25367,7 @@ function formatCancelledTaskStatusOutput(taskID, summary = "cancelled") {
24682
25367
  `);
24683
25368
  }
24684
25369
  // src/interview/manager.ts
24685
- import path13 from "node:path";
25370
+ import path16 from "node:path";
24686
25371
 
24687
25372
  // src/interview/dashboard.ts
24688
25373
  import crypto from "node:crypto";
@@ -24692,27 +25377,27 @@ import {
24692
25377
  createServer
24693
25378
  } from "node:http";
24694
25379
  import os4 from "node:os";
24695
- import path11 from "node:path";
25380
+ import path14 from "node:path";
24696
25381
  import { URL as URL2 } from "node:url";
24697
25382
 
24698
25383
  // src/interview/document.ts
24699
25384
  import * as fsSync from "node:fs";
24700
25385
  import * as fs6 from "node:fs/promises";
24701
- import * as path10 from "node:path";
25386
+ import * as path13 from "node:path";
24702
25387
  var DEFAULT_OUTPUT_FOLDER = "interview";
24703
25388
  function normalizeOutputFolder(outputFolder) {
24704
25389
  const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
24705
25390
  return normalized || DEFAULT_OUTPUT_FOLDER;
24706
25391
  }
24707
25392
  function createInterviewDirectoryPath(directory, outputFolder) {
24708
- return path10.join(directory, normalizeOutputFolder(outputFolder));
25393
+ return path13.join(directory, normalizeOutputFolder(outputFolder));
24709
25394
  }
24710
25395
  function createInterviewFilePath(directory, outputFolder, idea) {
24711
25396
  const fileName = `${slugify(idea) || "interview"}.md`;
24712
- return path10.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
25397
+ return path13.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
24713
25398
  }
24714
25399
  function relativeInterviewPath(directory, filePath) {
24715
- return path10.relative(directory, filePath) || path10.basename(filePath);
25400
+ return path13.relative(directory, filePath) || path13.basename(filePath);
24716
25401
  }
24717
25402
  function resolveExistingInterviewPath(directory, outputFolder, value) {
24718
25403
  const trimmed = value.trim();
@@ -24721,22 +25406,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
24721
25406
  }
24722
25407
  const outputDir = createInterviewDirectoryPath(directory, outputFolder);
24723
25408
  const candidates = new Set;
24724
- const resolvedRoot = path10.resolve(directory);
24725
- if (path10.isAbsolute(trimmed)) {
25409
+ const resolvedRoot = path13.resolve(directory);
25410
+ if (path13.isAbsolute(trimmed)) {
24726
25411
  candidates.add(trimmed);
24727
25412
  } else {
24728
- candidates.add(path10.resolve(directory, trimmed));
24729
- candidates.add(path10.join(outputDir, trimmed));
25413
+ candidates.add(path13.resolve(directory, trimmed));
25414
+ candidates.add(path13.join(outputDir, trimmed));
24730
25415
  if (!trimmed.endsWith(".md")) {
24731
- candidates.add(path10.join(outputDir, `${trimmed}.md`));
25416
+ candidates.add(path13.join(outputDir, `${trimmed}.md`));
24732
25417
  }
24733
25418
  }
24734
25419
  for (const candidate of candidates) {
24735
- if (path10.extname(candidate) !== ".md") {
25420
+ if (path13.extname(candidate) !== ".md") {
24736
25421
  continue;
24737
25422
  }
24738
- const resolved = path10.resolve(candidate);
24739
- if (!resolved.startsWith(resolvedRoot + path10.sep) && resolved !== resolvedRoot) {
25423
+ const resolved = path13.resolve(candidate);
25424
+ if (!resolved.startsWith(resolvedRoot + path13.sep) && resolved !== resolvedRoot) {
24740
25425
  continue;
24741
25426
  }
24742
25427
  if (fsSync.existsSync(candidate)) {
@@ -24816,7 +25501,7 @@ function parseFrontmatter(content) {
24816
25501
  return result;
24817
25502
  }
24818
25503
  async function ensureInterviewFile(record) {
24819
- await fs6.mkdir(path10.dirname(record.markdownPath), { recursive: true });
25504
+ await fs6.mkdir(path13.dirname(record.markdownPath), { recursive: true });
24820
25505
  try {
24821
25506
  await fs6.access(record.markdownPath);
24822
25507
  } catch {
@@ -26486,12 +27171,12 @@ function renderInterviewPage(interviewId, resumeSlug) {
26486
27171
 
26487
27172
  // src/interview/dashboard.ts
26488
27173
  function getAuthFilePath(port) {
26489
- const dataHome = process.env.XDG_DATA_HOME || path11.join(os4.homedir(), ".local", "share");
26490
- return path11.join(dataHome, "opencode", `.dashboard-${port}.json`);
27174
+ const dataHome = process.env.XDG_DATA_HOME || path14.join(os4.homedir(), ".local", "share");
27175
+ return path14.join(dataHome, "opencode", `.dashboard-${port}.json`);
26491
27176
  }
26492
27177
  function writeAuthFile(port, token) {
26493
27178
  const filePath = getAuthFilePath(port);
26494
- const dir = path11.dirname(filePath);
27179
+ const dir = path14.dirname(filePath);
26495
27180
  try {
26496
27181
  fsSync2.mkdirSync(dir, { recursive: true });
26497
27182
  } catch {}
@@ -26628,7 +27313,7 @@ function createDashboardServer(config) {
26628
27313
  const directories = getKnownDirectories();
26629
27314
  const items = [];
26630
27315
  for (const dir of directories) {
26631
- const interviewDir = path11.join(dir, config.outputFolder);
27316
+ const interviewDir = path14.join(dir, config.outputFolder);
26632
27317
  let entries;
26633
27318
  try {
26634
27319
  entries = await fs7.readdir(interviewDir);
@@ -26640,7 +27325,7 @@ function createDashboardServer(config) {
26640
27325
  continue;
26641
27326
  let content;
26642
27327
  try {
26643
- content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
27328
+ content = await fs7.readFile(path14.join(interviewDir, entry), "utf8");
26644
27329
  } catch {
26645
27330
  continue;
26646
27331
  }
@@ -26666,7 +27351,7 @@ function createDashboardServer(config) {
26666
27351
  const directories = getKnownDirectories();
26667
27352
  let rebuilt = 0;
26668
27353
  for (const dir of directories) {
26669
- const interviewDir = path11.join(dir, config.outputFolder);
27354
+ const interviewDir = path14.join(dir, config.outputFolder);
26670
27355
  let entries;
26671
27356
  try {
26672
27357
  entries = await fs7.readdir(interviewDir);
@@ -26678,7 +27363,7 @@ function createDashboardServer(config) {
26678
27363
  continue;
26679
27364
  let content;
26680
27365
  try {
26681
- content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
27366
+ content = await fs7.readFile(path14.join(interviewDir, entry), "utf8");
26682
27367
  } catch {
26683
27368
  continue;
26684
27369
  }
@@ -26704,7 +27389,7 @@ function createDashboardServer(config) {
26704
27389
  questions: [],
26705
27390
  pendingAnswers: null,
26706
27391
  lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
26707
- filePath: path11.join(interviewDir, entry),
27392
+ filePath: path14.join(interviewDir, entry),
26708
27393
  nudgeAction: null
26709
27394
  });
26710
27395
  if (!sessions.has(fm.sessionID)) {
@@ -26968,7 +27653,7 @@ function createDashboardServer(config) {
26968
27653
  const dirs = getKnownDirectories();
26969
27654
  for (const dir of dirs) {
26970
27655
  const slug = extractResumeSlug(interviewId);
26971
- const candidate = path11.join(dir, config.outputFolder, `${slug}.md`);
27656
+ const candidate = path14.join(dir, config.outputFolder, `${slug}.md`);
26972
27657
  try {
26973
27658
  document = await fs7.readFile(candidate, "utf8");
26974
27659
  markdownPath = candidate;
@@ -27496,7 +28181,7 @@ function createInterviewServer(deps) {
27496
28181
  // src/interview/service.ts
27497
28182
  import { spawn as spawn2 } from "node:child_process";
27498
28183
  import * as fs8 from "node:fs/promises";
27499
- import * as path12 from "node:path";
28184
+ import * as path15 from "node:path";
27500
28185
 
27501
28186
  // src/interview/types.ts
27502
28187
  import { z as z3 } from "zod";
@@ -27668,7 +28353,7 @@ function buildAnswerPrompt(answers, questions, maxQuestions) {
27668
28353
  }
27669
28354
 
27670
28355
  // src/interview/service.ts
27671
- var COMMAND_NAME2 = "interview";
28356
+ var COMMAND_NAME3 = "interview";
27672
28357
  var DEFAULT_MAX_QUESTIONS = 2;
27673
28358
  function isTruthyEnvFlag(value) {
27674
28359
  if (!value) {
@@ -27676,21 +28361,21 @@ function isTruthyEnvFlag(value) {
27676
28361
  }
27677
28362
  return value !== "0" && value.toLowerCase() !== "false";
27678
28363
  }
27679
- function isAutomatedRuntime(env2) {
27680
- return env2.NODE_ENV === "test" || isTruthyEnvFlag(env2.CI) || isTruthyEnvFlag(env2.BUN_TEST) || isTruthyEnvFlag(env2.VITEST) || env2.JEST_WORKER_ID !== undefined;
28364
+ function isAutomatedRuntime(env) {
28365
+ return env.NODE_ENV === "test" || isTruthyEnvFlag(env.CI) || isTruthyEnvFlag(env.BUN_TEST) || isTruthyEnvFlag(env.VITEST) || env.JEST_WORKER_ID !== undefined;
27681
28366
  }
27682
- function shouldAutoOpenBrowser(config, env2) {
28367
+ function shouldAutoOpenBrowser(config, env) {
27683
28368
  const requested = config?.autoOpenBrowser ?? true;
27684
- return requested && !isAutomatedRuntime(env2);
28369
+ return requested && !isAutomatedRuntime(env);
27685
28370
  }
27686
28371
  function openBrowser(url) {
27687
- const platform2 = process.platform;
28372
+ const platform3 = process.platform;
27688
28373
  let command;
27689
28374
  let args;
27690
- if (platform2 === "darwin") {
28375
+ if (platform3 === "darwin") {
27691
28376
  command = "open";
27692
28377
  args = [url];
27693
- } else if (platform2 === "win32") {
28378
+ } else if (platform3 === "win32") {
27694
28379
  command = "cmd";
27695
28380
  args = ["/c", "start", "", url];
27696
28381
  } else {
@@ -27763,12 +28448,12 @@ function createInterviewService(ctx, config, deps) {
27763
28448
  if (!newSlug) {
27764
28449
  return;
27765
28450
  }
27766
- const currentFileName = path12.basename(interview.markdownPath, ".md");
28451
+ const currentFileName = path15.basename(interview.markdownPath, ".md");
27767
28452
  if (currentFileName === newSlug) {
27768
28453
  return;
27769
28454
  }
27770
- const dir = path12.dirname(interview.markdownPath);
27771
- const newPath = path12.join(dir, `${newSlug}.md`);
28455
+ const dir = path15.dirname(interview.markdownPath);
28456
+ const newPath = path15.join(dir, `${newSlug}.md`);
27772
28457
  try {
27773
28458
  await fs8.access(newPath);
27774
28459
  return;
@@ -27844,9 +28529,9 @@ function createInterviewService(ctx, config, deps) {
27844
28529
  const messages = await loadMessages(sessionID);
27845
28530
  const title = extractTitle(document);
27846
28531
  const record = {
27847
- id: `${Date.now()}-${++idCounter}-${slugify(path12.basename(markdownPath, ".md")) || "interview"}`,
28532
+ id: `${Date.now()}-${++idCounter}-${slugify(path15.basename(markdownPath, ".md")) || "interview"}`,
27848
28533
  sessionID,
27849
- idea: title || path12.basename(markdownPath, ".md"),
28534
+ idea: title || path15.basename(markdownPath, ".md"),
27850
28535
  markdownPath,
27851
28536
  createdAt: nowIso(),
27852
28537
  status: "active",
@@ -27915,11 +28600,11 @@ function createInterviewService(ctx, config, deps) {
27915
28600
  }
27916
28601
  function registerCommand(opencodeConfig) {
27917
28602
  const configCommand = opencodeConfig.command;
27918
- if (!configCommand?.[COMMAND_NAME2]) {
28603
+ if (!configCommand?.[COMMAND_NAME3]) {
27919
28604
  if (!opencodeConfig.command) {
27920
28605
  opencodeConfig.command = {};
27921
28606
  }
27922
- opencodeConfig.command[COMMAND_NAME2] = {
28607
+ opencodeConfig.command[COMMAND_NAME3] = {
27923
28608
  template: "Start an interview and write a live markdown spec",
27924
28609
  description: "Open a localhost interview UI linked to the current OpenCode session"
27925
28610
  };
@@ -27993,7 +28678,7 @@ function createInterviewService(ctx, config, deps) {
27993
28678
  }
27994
28679
  }
27995
28680
  async function handleCommandExecuteBefore(input, output) {
27996
- if (input.command !== COMMAND_NAME2) {
28681
+ if (input.command !== COMMAND_NAME3) {
27997
28682
  return;
27998
28683
  }
27999
28684
  const idea = input.arguments.trim();
@@ -28073,7 +28758,7 @@ function createInterviewService(ctx, config, deps) {
28073
28758
  return fileCache.items;
28074
28759
  }
28075
28760
  const outputDir = createInterviewDirectoryPath(ctx.directory, outputFolder);
28076
- const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path12.resolve(i.markdownPath)));
28761
+ const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path15.resolve(i.markdownPath)));
28077
28762
  let entries;
28078
28763
  try {
28079
28764
  entries = await fs8.readdir(outputDir);
@@ -28084,8 +28769,8 @@ function createInterviewService(ctx, config, deps) {
28084
28769
  for (const entry of entries) {
28085
28770
  if (!entry.endsWith(".md"))
28086
28771
  continue;
28087
- const fullPath = path12.join(outputDir, entry);
28088
- if (activePaths.has(path12.resolve(fullPath)))
28772
+ const fullPath = path15.join(outputDir, entry);
28773
+ if (activePaths.has(path15.resolve(fullPath)))
28089
28774
  continue;
28090
28775
  let content;
28091
28776
  try {
@@ -28184,7 +28869,7 @@ function createInterviewManager(ctx, config) {
28184
28869
  const outputFolder = interviewConfig?.outputFolder ?? "interview";
28185
28870
  if (!dashboardEnabled) {
28186
28871
  const service2 = createInterviewService(ctx, interviewConfig);
28187
- const resolvedOutputPath = path13.join(ctx.directory, outputFolder);
28872
+ const resolvedOutputPath = path16.join(ctx.directory, outputFolder);
28188
28873
  const server = createInterviewServer({
28189
28874
  getState: async (interviewId) => service2.getInterviewState(interviewId),
28190
28875
  listInterviewFiles: async () => service2.listInterviewFiles(),
@@ -28289,7 +28974,7 @@ function createInterviewManager(ctx, config) {
28289
28974
  listInterviews: () => service.listInterviews(),
28290
28975
  submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
28291
28976
  handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
28292
- outputFolder: path13.join(ctx.directory, outputFolder),
28977
+ outputFolder: path16.join(ctx.directory, outputFolder),
28293
28978
  port: 0
28294
28979
  });
28295
28980
  service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
@@ -28557,6 +29242,7 @@ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
28557
29242
  }
28558
29243
 
28559
29244
  // src/multiplexer/tmux/index.ts
29245
+ init_compat();
28560
29246
  var TMUX_LAYOUT_DEBOUNCE_MS = 150;
28561
29247
 
28562
29248
  class TmuxMultiplexer {
@@ -28774,23 +29460,23 @@ class TmuxMultiplexer {
28774
29460
  return null;
28775
29461
  }
28776
29462
  const stdout = await proc.stdout();
28777
- const path14 = stdout.trim().split(`
29463
+ const path17 = stdout.trim().split(`
28778
29464
  `)[0];
28779
- if (!path14) {
29465
+ if (!path17) {
28780
29466
  log("[tmux] findBinary: no path in output");
28781
29467
  return null;
28782
29468
  }
28783
- const verifyProc = crossSpawn([path14, "-V"], {
29469
+ const verifyProc = crossSpawn([path17, "-V"], {
28784
29470
  stdout: "pipe",
28785
29471
  stderr: "pipe"
28786
29472
  });
28787
29473
  const verifyExit = await verifyProc.exited;
28788
29474
  if (verifyExit !== 0) {
28789
- log("[tmux] findBinary: tmux -V failed", { path: path14, verifyExit });
29475
+ log("[tmux] findBinary: tmux -V failed", { path: path17, verifyExit });
28790
29476
  return null;
28791
29477
  }
28792
- log("[tmux] findBinary: found", { path: path14 });
28793
- return path14;
29478
+ log("[tmux] findBinary: found", { path: path17 });
29479
+ return path17;
28794
29480
  } catch (err) {
28795
29481
  log("[tmux] findBinary: exception", { error: String(err) });
28796
29482
  return null;
@@ -28802,6 +29488,8 @@ function quoteShellArg(value) {
28802
29488
  }
28803
29489
 
28804
29490
  // src/multiplexer/zellij/index.ts
29491
+ init_compat();
29492
+
28805
29493
  class ZellijMultiplexer {
28806
29494
  paneMode;
28807
29495
  type = "zellij";
@@ -29741,22 +30429,359 @@ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
29741
30429
  }
29742
30430
  return false;
29743
30431
  }
29744
- // src/tools/ast-grep/tools.ts
30432
+ // src/tools/acp-run.ts
30433
+ import { spawn as spawn3 } from "node:child_process";
30434
+ import { createInterface } from "node:readline";
29745
30435
  import { tool } from "@opencode-ai/plugin";
30436
+ var z4 = tool.schema;
30437
+
30438
+ class AcpClient {
30439
+ name;
30440
+ config;
30441
+ cwd;
30442
+ ask;
30443
+ child;
30444
+ next = 1;
30445
+ pending = new Map;
30446
+ chunks = [];
30447
+ errors = [];
30448
+ sessionId;
30449
+ lastUpdate = Date.now();
30450
+ authMethods = [];
30451
+ active = false;
30452
+ activeRequests = 0;
30453
+ constructor(name, config, cwd, ask) {
30454
+ this.name = name;
30455
+ this.config = config;
30456
+ this.cwd = cwd;
30457
+ this.ask = ask;
30458
+ this.child = spawn3(config.command, config.args, {
30459
+ cwd,
30460
+ env: { ...process.env, ...config.env },
30461
+ stdio: "pipe"
30462
+ });
30463
+ this.child.stderr.on("data", (chunk) => {
30464
+ this.errors.push(String(chunk));
30465
+ });
30466
+ this.child.stdin.on("error", (error) => {
30467
+ this.errors.push(String(error));
30468
+ this.rejectPending(error);
30469
+ });
30470
+ this.child.on("error", (error) => {
30471
+ this.rejectPending(error);
30472
+ });
30473
+ this.child.on("exit", (code, signal) => {
30474
+ if (this.pending.size === 0)
30475
+ return;
30476
+ this.rejectPending(new Error(`ACP agent '${name}' exited before replying (code ${code ?? "null"}, signal ${signal ?? "null"})`));
30477
+ });
30478
+ createInterface({ input: this.child.stdout }).on("line", (line) => {
30479
+ this.receive(line).catch((error) => {
30480
+ this.errors.push(String(error));
30481
+ });
30482
+ });
30483
+ }
30484
+ async run(prompt) {
30485
+ const init = await this.request("initialize", {
30486
+ protocolVersion: 1,
30487
+ clientCapabilities: {},
30488
+ clientInfo: {
30489
+ name: "oh-my-opencode-slim",
30490
+ title: "oh-my-opencode-slim ACP bridge"
30491
+ }
30492
+ });
30493
+ this.authMethods = readAuthMethods(init);
30494
+ const created = await this.newSession();
30495
+ const sessionId = readSessionId(created);
30496
+ this.sessionId = sessionId;
30497
+ this.active = true;
30498
+ await this.request("session/prompt", {
30499
+ sessionId,
30500
+ prompt: [{ type: "text", text: prompt }]
30501
+ });
30502
+ await this.drain();
30503
+ this.active = false;
30504
+ return this.output();
30505
+ }
30506
+ async newSession() {
30507
+ try {
30508
+ return await this.request("session/new", {
30509
+ cwd: this.cwd,
30510
+ mcpServers: []
30511
+ });
30512
+ } catch (error) {
30513
+ if (!isAuthError(error) || this.authMethods.length === 0)
30514
+ throw error;
30515
+ const method = this.authMethods[0];
30516
+ if (typeof method.id !== "string")
30517
+ throw error;
30518
+ await this.request("authenticate", { methodId: method.id });
30519
+ return await this.request("session/new", {
30520
+ cwd: this.cwd,
30521
+ mcpServers: []
30522
+ });
30523
+ }
30524
+ }
30525
+ close() {
30526
+ if (this.active && this.sessionId && !this.child.killed) {
30527
+ this.notify("session/cancel", { sessionId: this.sessionId });
30528
+ }
30529
+ if (!this.child.killed)
30530
+ this.child.kill("SIGTERM");
30531
+ }
30532
+ request(method, params) {
30533
+ const id = this.next++;
30534
+ const payload = { jsonrpc: "2.0", id, method, params };
30535
+ return new Promise((resolve3, reject) => {
30536
+ this.pending.set(id, { resolve: resolve3, reject });
30537
+ this.child.stdin.write(`${JSON.stringify(payload)}
30538
+ `, (error) => {
30539
+ if (!error)
30540
+ return;
30541
+ this.pending.delete(id);
30542
+ reject(error);
30543
+ });
30544
+ });
30545
+ }
30546
+ notify(method, params) {
30547
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
30548
+ `);
30549
+ }
30550
+ async drain() {
30551
+ this.lastUpdate = Date.now();
30552
+ while (this.activeRequests > 0 || Date.now() - this.lastUpdate < 100) {
30553
+ await new Promise((resolve3) => setTimeout(resolve3, 25));
30554
+ }
30555
+ }
30556
+ async receive(line) {
30557
+ if (!line.trim())
30558
+ return;
30559
+ let message;
30560
+ try {
30561
+ message = JSON.parse(line);
30562
+ } catch {
30563
+ const error = new Error(`ACP agent '${this.name}' wrote non-JSON stdout: ${line.slice(0, 200)}`);
30564
+ this.errors.push(error.message);
30565
+ this.rejectPending(error);
30566
+ this.close();
30567
+ return;
30568
+ }
30569
+ if ("id" in message && (("result" in message) || ("error" in message))) {
30570
+ const pending = this.pending.get(message.id);
30571
+ if (!pending)
30572
+ return;
30573
+ this.pending.delete(message.id);
30574
+ if (message.error) {
30575
+ pending.reject(rpcError(message.error));
30576
+ return;
30577
+ }
30578
+ pending.resolve(message.result);
30579
+ return;
30580
+ }
30581
+ if ("id" in message && "method" in message) {
30582
+ this.activeRequests++;
30583
+ try {
30584
+ await this.handleRequest(message);
30585
+ } finally {
30586
+ this.activeRequests--;
30587
+ this.lastUpdate = Date.now();
30588
+ }
30589
+ return;
30590
+ }
30591
+ if ("method" in message)
30592
+ this.handleNotification(message);
30593
+ }
30594
+ rejectPending(error) {
30595
+ for (const item of this.pending.values())
30596
+ item.reject(error);
30597
+ this.pending.clear();
30598
+ }
30599
+ async handleRequest(message) {
30600
+ if (message.method === "session/request_permission") {
30601
+ const title = readPermissionTitle(message.params);
30602
+ try {
30603
+ if (this.config.permissionMode === "ask") {
30604
+ await this.ask(title, message.params ?? {});
30605
+ }
30606
+ const optionId = selectPermissionOption(message.params, this.config.permissionMode);
30607
+ if (!optionId)
30608
+ throw new Error("ACP permission request had no usable option");
30609
+ this.reply(message.id, {
30610
+ outcome: { outcome: "selected", optionId }
30611
+ });
30612
+ } catch {
30613
+ const optionId = selectPermissionOption(message.params, "reject");
30614
+ if (optionId) {
30615
+ this.reply(message.id, {
30616
+ outcome: { outcome: "selected", optionId }
30617
+ });
30618
+ return;
30619
+ }
30620
+ this.reply(message.id, { outcome: { outcome: "cancelled" } });
30621
+ }
30622
+ return;
30623
+ }
30624
+ this.replyError(message.id, `Unsupported ACP client method: ${message.method}`);
30625
+ }
30626
+ handleNotification(message) {
30627
+ if (message.method !== "session/update")
30628
+ return;
30629
+ this.lastUpdate = Date.now();
30630
+ const update = message.params?.update;
30631
+ if (!isRecord2(update))
30632
+ return;
30633
+ collectText(update, this.chunks);
30634
+ }
30635
+ reply(id, result) {
30636
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}
30637
+ `);
30638
+ }
30639
+ replyError(id, message) {
30640
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, error: { code: -32601, message } })}
30641
+ `);
30642
+ }
30643
+ output() {
30644
+ const text = this.chunks.join("").trim();
30645
+ if (text)
30646
+ return text;
30647
+ const err = this.errors.join("").trim();
30648
+ return err ? `ACP agent '${this.name}' completed without text output. stderr:
30649
+ ${err}` : `ACP agent '${this.name}' completed without text output.`;
30650
+ }
30651
+ }
30652
+ function createAcpRunTool(agents = {}) {
30653
+ return tool({
30654
+ description: "Run a configured external ACP-compatible coding agent and return its streamed result. Use for configured ACP agents such as Claude Code ACP, Gemini ACP, or custom ACP servers.",
30655
+ args: {
30656
+ agent: z4.string().describe("Configured ACP agent name"),
30657
+ prompt: z4.string().describe("Task or question to send to the ACP agent"),
30658
+ cwd: z4.string().optional().describe("Optional absolute working directory override"),
30659
+ timeout_ms: z4.number().int().min(1000).max(900000).optional().describe("Optional timeout override in milliseconds")
30660
+ },
30661
+ async execute(args, ctx) {
30662
+ if (ctx.agent !== args.agent) {
30663
+ throw new Error(`acp_run for '${args.agent}' can only be used by @${args.agent}`);
30664
+ }
30665
+ const config = agents[args.agent];
30666
+ if (!config) {
30667
+ throw new Error(`Unknown ACP agent '${args.agent}'. Configured agents: ${Object.keys(agents).join(", ") || "(none)"}`);
30668
+ }
30669
+ const cwd = args.cwd ?? config.cwd ?? ctx.directory;
30670
+ if (!cwd)
30671
+ throw new Error("acp_run requires a working directory");
30672
+ await ctx.ask({
30673
+ permission: "bash",
30674
+ patterns: [`${config.command} ${config.args.join(" ")}`.trim()],
30675
+ always: [],
30676
+ metadata: {
30677
+ agent: args.agent,
30678
+ cwd,
30679
+ command: config.command,
30680
+ args: config.args
30681
+ }
30682
+ });
30683
+ const client = new AcpClient(args.agent, config, cwd, async (title, metadata) => {
30684
+ if (config.permissionMode === "reject")
30685
+ return;
30686
+ await ctx.ask({
30687
+ permission: "bash",
30688
+ patterns: [`acp:${args.agent}:${title}`],
30689
+ always: [],
30690
+ metadata
30691
+ });
30692
+ });
30693
+ const timeoutMs = args.timeout_ms ?? config.timeoutMs;
30694
+ let timer;
30695
+ const timeout = new Promise((_, reject) => timer = setTimeout(() => reject(new Error(`ACP agent '${args.agent}' timed out after ${timeoutMs}ms`)), timeoutMs));
30696
+ const abort = () => client.close();
30697
+ ctx.abort.addEventListener("abort", abort, { once: true });
30698
+ try {
30699
+ return await Promise.race([client.run(args.prompt), timeout]);
30700
+ } finally {
30701
+ if (timer)
30702
+ clearTimeout(timer);
30703
+ ctx.abort.removeEventListener("abort", abort);
30704
+ client.close();
30705
+ }
30706
+ }
30707
+ });
30708
+ }
30709
+ function readSessionId(value) {
30710
+ if (!isRecord2(value) || typeof value.sessionId !== "string") {
30711
+ throw new Error("ACP agent did not return a sessionId");
30712
+ }
30713
+ return value.sessionId;
30714
+ }
30715
+ function readAuthMethods(value) {
30716
+ if (!isRecord2(value) || !Array.isArray(value.authMethods))
30717
+ return [];
30718
+ const methods = value.authMethods;
30719
+ return methods.filter(isRecord2);
30720
+ }
30721
+ function rpcError(error) {
30722
+ const err = new Error(error.message ?? "ACP request failed");
30723
+ err.code = error.code;
30724
+ err.data = error.data;
30725
+ return err;
30726
+ }
30727
+ function isAuthError(error) {
30728
+ if (!(error instanceof Error))
30729
+ return false;
30730
+ const meta = error;
30731
+ return meta.code === -32001 || error.message.toLowerCase().includes("auth_required") || error.message.toLowerCase().includes("auth required");
30732
+ }
30733
+ function isRecord2(value) {
30734
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30735
+ }
30736
+ function readPermissionTitle(params) {
30737
+ const tool2 = isRecord2(params?.toolCall) ? params.toolCall : undefined;
30738
+ if (typeof tool2?.title === "string")
30739
+ return tool2.title;
30740
+ if (typeof params?.permission === "string")
30741
+ return params.permission;
30742
+ return "ACP permission request";
30743
+ }
30744
+ function selectPermissionOption(params, mode) {
30745
+ const options = Array.isArray(params?.options) ? params.options : [];
30746
+ const choices = options.filter(isRecord2).filter((item) => typeof item.optionId === "string");
30747
+ const reject = choices.find((item) => typeof item.kind === "string" && item.kind.startsWith("reject"));
30748
+ if (mode === "reject")
30749
+ return reject?.optionId;
30750
+ const allow = choices.find((item) => typeof item.kind === "string" && item.kind.startsWith("allow"));
30751
+ return allow?.optionId ?? reject?.optionId;
30752
+ }
30753
+ function collectText(update, chunks) {
30754
+ if (update.sessionUpdate !== "agent_message_chunk")
30755
+ return;
30756
+ const text = readText(update.delta) ?? readText(update.content);
30757
+ if (text)
30758
+ chunks.push(text);
30759
+ }
30760
+ function readText(value) {
30761
+ if (typeof value === "string")
30762
+ return value;
30763
+ if (isRecord2(value) && typeof value.text === "string")
30764
+ return value.text;
30765
+ return;
30766
+ }
30767
+ // src/tools/ast-grep/tools.ts
30768
+ import { tool as tool2 } from "@opencode-ai/plugin";
29746
30769
 
29747
30770
  // src/tools/ast-grep/cli.ts
29748
- import { existsSync as existsSync9 } from "node:fs";
30771
+ init_compat();
30772
+ import { existsSync as existsSync11 } from "node:fs";
29749
30773
 
29750
30774
  // src/tools/ast-grep/constants.ts
29751
- import { existsSync as existsSync8, statSync as statSync4 } from "node:fs";
30775
+ import { existsSync as existsSync10, statSync as statSync5 } from "node:fs";
29752
30776
  import { createRequire as createRequire3 } from "node:module";
29753
- import { dirname as dirname7, join as join12 } from "node:path";
30777
+ import { dirname as dirname9, join as join15 } from "node:path";
29754
30778
 
29755
30779
  // src/tools/ast-grep/downloader.ts
29756
- import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "node:fs";
30780
+ import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "node:fs";
29757
30781
  import { createRequire as createRequire2 } from "node:module";
29758
- import { homedir as homedir5 } from "node:os";
29759
- import { join as join11 } from "node:path";
30782
+ import { homedir as homedir6 } from "node:os";
30783
+ import { join as join14 } from "node:path";
30784
+ init_compat();
29760
30785
  var REPO = "ast-grep/ast-grep";
29761
30786
  var DEFAULT_VERSION = "0.40.0";
29762
30787
  function getAstGrepVersion() {
@@ -29780,19 +30805,19 @@ var PLATFORM_MAP = {
29780
30805
  function getCacheDir2() {
29781
30806
  if (process.platform === "win32") {
29782
30807
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
29783
- const base2 = localAppData || join11(homedir5(), "AppData", "Local");
29784
- return join11(base2, "oh-my-opencode-slim", "bin");
30808
+ const base2 = localAppData || join14(homedir6(), "AppData", "Local");
30809
+ return join14(base2, "oh-my-opencode-slim", "bin");
29785
30810
  }
29786
30811
  const xdgCache = process.env.XDG_CACHE_HOME;
29787
- const base = xdgCache || join11(homedir5(), ".cache");
29788
- return join11(base, "oh-my-opencode-slim", "bin");
30812
+ const base = xdgCache || join14(homedir6(), ".cache");
30813
+ return join14(base, "oh-my-opencode-slim", "bin");
29789
30814
  }
29790
30815
  function getBinaryName() {
29791
30816
  return process.platform === "win32" ? "sg.exe" : "sg";
29792
30817
  }
29793
30818
  function getCachedBinaryPath() {
29794
- const binaryPath2 = join11(getCacheDir2(), getBinaryName());
29795
- return existsSync7(binaryPath2) ? binaryPath2 : null;
30819
+ const binaryPath = join14(getCacheDir2(), getBinaryName());
30820
+ return existsSync9(binaryPath) ? binaryPath : null;
29796
30821
  }
29797
30822
  async function downloadAstGrep(version = DEFAULT_VERSION) {
29798
30823
  const platformKey = `${process.platform}-${process.arch}`;
@@ -29803,34 +30828,34 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
29803
30828
  }
29804
30829
  const cacheDir = getCacheDir2();
29805
30830
  const binaryName = getBinaryName();
29806
- const binaryPath2 = join11(cacheDir, binaryName);
29807
- if (existsSync7(binaryPath2)) {
29808
- return binaryPath2;
30831
+ const binaryPath = join14(cacheDir, binaryName);
30832
+ if (existsSync9(binaryPath)) {
30833
+ return binaryPath;
29809
30834
  }
29810
30835
  const { arch, os: os5 } = platformInfo;
29811
30836
  const assetName = `app-${arch}-${os5}.zip`;
29812
30837
  const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
29813
30838
  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
29814
30839
  try {
29815
- if (!existsSync7(cacheDir)) {
29816
- mkdirSync5(cacheDir, { recursive: true });
30840
+ if (!existsSync9(cacheDir)) {
30841
+ mkdirSync7(cacheDir, { recursive: true });
29817
30842
  }
29818
30843
  const response = await fetch(downloadUrl, { redirect: "follow" });
29819
30844
  if (!response.ok) {
29820
30845
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
29821
30846
  }
29822
- const archivePath = join11(cacheDir, assetName);
30847
+ const archivePath = join14(cacheDir, assetName);
29823
30848
  const arrayBuffer = await response.arrayBuffer();
29824
30849
  await crossWrite(archivePath, arrayBuffer);
29825
30850
  await extractZip(archivePath, cacheDir);
29826
- if (existsSync7(archivePath)) {
30851
+ if (existsSync9(archivePath)) {
29827
30852
  unlinkSync4(archivePath);
29828
30853
  }
29829
- if (process.platform !== "win32" && existsSync7(binaryPath2)) {
29830
- chmodSync(binaryPath2, 493);
30854
+ if (process.platform !== "win32" && existsSync9(binaryPath)) {
30855
+ chmodSync2(binaryPath, 493);
29831
30856
  }
29832
30857
  console.log(`[oh-my-opencode-slim] ast-grep binary ready.`);
29833
- return binaryPath2;
30858
+ return binaryPath;
29834
30859
  } catch (err) {
29835
30860
  console.error(`[oh-my-opencode-slim] Failed to download ast-grep: ${err instanceof Error ? err.message : err}`);
29836
30861
  return null;
@@ -29878,13 +30903,13 @@ var CLI_LANGUAGES = [
29878
30903
  var MIN_BINARY_SIZE = 1e4;
29879
30904
  function isValidBinary(filePath) {
29880
30905
  try {
29881
- return statSync4(filePath).size > MIN_BINARY_SIZE;
30906
+ return statSync5(filePath).size > MIN_BINARY_SIZE;
29882
30907
  } catch {
29883
30908
  return false;
29884
30909
  }
29885
30910
  }
29886
30911
  function getPlatformPackageName() {
29887
- const platform2 = process.platform;
30912
+ const platform3 = process.platform;
29888
30913
  const arch = process.arch;
29889
30914
  const platformMap = {
29890
30915
  "darwin-arm64": "@ast-grep/cli-darwin-arm64",
@@ -29895,7 +30920,7 @@ function getPlatformPackageName() {
29895
30920
  "win32-arm64": "@ast-grep/cli-win32-arm64-msvc",
29896
30921
  "win32-ia32": "@ast-grep/cli-win32-ia32-msvc"
29897
30922
  };
29898
- return platformMap[`${platform2}-${arch}`] ?? null;
30923
+ return platformMap[`${platform3}-${arch}`] ?? null;
29899
30924
  }
29900
30925
  var resolvedCliPath = null;
29901
30926
  function findSgCliPathSync() {
@@ -29907,9 +30932,9 @@ function findSgCliPathSync() {
29907
30932
  try {
29908
30933
  const require2 = createRequire3(import.meta.url);
29909
30934
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
29910
- const cliDir = dirname7(cliPkgPath);
29911
- const sgPath = join12(cliDir, binaryName);
29912
- if (existsSync8(sgPath) && isValidBinary(sgPath)) {
30935
+ const cliDir = dirname9(cliPkgPath);
30936
+ const sgPath = join15(cliDir, binaryName);
30937
+ if (existsSync10(sgPath) && isValidBinary(sgPath)) {
29913
30938
  return sgPath;
29914
30939
  }
29915
30940
  } catch {}
@@ -29918,19 +30943,19 @@ function findSgCliPathSync() {
29918
30943
  try {
29919
30944
  const require2 = createRequire3(import.meta.url);
29920
30945
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
29921
- const pkgDir = dirname7(pkgPath);
30946
+ const pkgDir = dirname9(pkgPath);
29922
30947
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
29923
- const binaryPath2 = join12(pkgDir, astGrepName);
29924
- if (existsSync8(binaryPath2) && isValidBinary(binaryPath2)) {
29925
- return binaryPath2;
30948
+ const binaryPath = join15(pkgDir, astGrepName);
30949
+ if (existsSync10(binaryPath) && isValidBinary(binaryPath)) {
30950
+ return binaryPath;
29926
30951
  }
29927
30952
  } catch {}
29928
30953
  }
29929
30954
  if (process.platform === "darwin") {
29930
30955
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
29931
- for (const path14 of homebrewPaths) {
29932
- if (existsSync8(path14) && isValidBinary(path14)) {
29933
- return path14;
30956
+ for (const path17 of homebrewPaths) {
30957
+ if (existsSync10(path17) && isValidBinary(path17)) {
30958
+ return path17;
29934
30959
  }
29935
30960
  }
29936
30961
  }
@@ -29947,10 +30972,10 @@ function getSgCliPath() {
29947
30972
  }
29948
30973
  return "sg";
29949
30974
  }
29950
- function setSgCliPath(path14) {
29951
- resolvedCliPath = path14;
30975
+ function setSgCliPath(path17) {
30976
+ resolvedCliPath = path17;
29952
30977
  }
29953
- var DEFAULT_TIMEOUT_MS2 = 300000;
30978
+ var DEFAULT_TIMEOUT_MS = 300000;
29954
30979
  var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
29955
30980
  var DEFAULT_MAX_MATCHES = 500;
29956
30981
 
@@ -29958,7 +30983,7 @@ var DEFAULT_MAX_MATCHES = 500;
29958
30983
  var initPromise = null;
29959
30984
  async function getAstGrepPath() {
29960
30985
  const currentPath = getSgCliPath();
29961
- if (currentPath !== "sg" && existsSync9(currentPath)) {
30986
+ if (currentPath !== "sg" && existsSync11(currentPath)) {
29962
30987
  return currentPath;
29963
30988
  }
29964
30989
  if (initPromise) {
@@ -29966,7 +30991,7 @@ async function getAstGrepPath() {
29966
30991
  }
29967
30992
  initPromise = (async () => {
29968
30993
  const syncPath = findSgCliPathSync();
29969
- if (syncPath && existsSync9(syncPath)) {
30994
+ if (syncPath && existsSync11(syncPath)) {
29970
30995
  setSgCliPath(syncPath);
29971
30996
  return syncPath;
29972
30997
  }
@@ -30005,13 +31030,13 @@ async function runSg(options) {
30005
31030
  const paths2 = options.paths && options.paths.length > 0 ? options.paths : ["."];
30006
31031
  args.push(...paths2);
30007
31032
  let cliPath = getSgCliPath();
30008
- if (!existsSync9(cliPath) && cliPath !== "sg") {
31033
+ if (!existsSync11(cliPath) && cliPath !== "sg") {
30009
31034
  const downloadedPath = await getAstGrepPath();
30010
31035
  if (downloadedPath) {
30011
31036
  cliPath = downloadedPath;
30012
31037
  }
30013
31038
  }
30014
- const timeout = DEFAULT_TIMEOUT_MS2;
31039
+ const timeout = DEFAULT_TIMEOUT_MS;
30015
31040
  const proc = crossSpawn([cliPath, ...args], {
30016
31041
  stdout: "pipe",
30017
31042
  stderr: "pipe"
@@ -30221,14 +31246,14 @@ function showOutputToUser(context, output) {
30221
31246
  const ctx = context;
30222
31247
  ctx.metadata?.({ metadata: { output } });
30223
31248
  }
30224
- var ast_grep_search = tool({
31249
+ var ast_grep_search = tool2({
30225
31250
  description: "Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " + "Use meta-variables: $VAR (single node), $$$ (multiple nodes). " + "IMPORTANT: Patterns must be complete AST nodes (valid code). " + "For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. " + "Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'",
30226
31251
  args: {
30227
- pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
30228
- lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
30229
- paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"),
30230
- globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
30231
- context: tool.schema.number().optional().describe("Context lines around match")
31252
+ pattern: tool2.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
31253
+ lang: tool2.schema.enum(CLI_LANGUAGES).describe("Target language"),
31254
+ paths: tool2.schema.array(tool2.schema.string()).optional().describe("Paths to search (default: ['.'])"),
31255
+ globs: tool2.schema.array(tool2.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
31256
+ context: tool2.schema.number().optional().describe("Context lines around match")
30232
31257
  },
30233
31258
  execute: async (args, context) => {
30234
31259
  try {
@@ -30257,15 +31282,15 @@ ${hint}`;
30257
31282
  }
30258
31283
  }
30259
31284
  });
30260
- var ast_grep_replace = tool({
31285
+ var ast_grep_replace = tool2({
30261
31286
  description: "Replace code patterns across filesystem with AST-aware rewriting. " + "Dry-run by default. Use meta-variables in rewrite to preserve matched content. " + "Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'",
30262
31287
  args: {
30263
- pattern: tool.schema.string().describe("AST pattern to match"),
30264
- rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
30265
- lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
30266
- paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search"),
30267
- globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs"),
30268
- dryRun: tool.schema.boolean().optional().describe("Preview changes without applying (default: true)")
31288
+ pattern: tool2.schema.string().describe("AST pattern to match"),
31289
+ rewrite: tool2.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
31290
+ lang: tool2.schema.enum(CLI_LANGUAGES).describe("Target language"),
31291
+ paths: tool2.schema.array(tool2.schema.string()).optional().describe("Paths to search"),
31292
+ globs: tool2.schema.array(tool2.schema.string()).optional().describe("Include/exclude globs"),
31293
+ dryRun: tool2.schema.boolean().optional().describe("Preview changes without applying (default: true)")
30269
31294
  },
30270
31295
  execute: async (args, context) => {
30271
31296
  try {
@@ -30289,20 +31314,20 @@ var ast_grep_replace = tool({
30289
31314
  });
30290
31315
  // src/tools/cancel-task.ts
30291
31316
  import {
30292
- tool as tool2
31317
+ tool as tool3
30293
31318
  } from "@opencode-ai/plugin";
30294
- var z4 = tool2.schema;
31319
+ var z5 = tool3.schema;
30295
31320
 
30296
31321
  class SessionStillRunningError extends Error {
30297
31322
  }
30298
31323
  function createCancelTaskTool(options) {
30299
- const cancel_task = tool2({
31324
+ const cancel_task = tool3({
30300
31325
  description: `Cancel a tracked background specialist task.
30301
31326
 
30302
31327
  Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accepts either the native task_id/session ID or the parent-scoped alias shown in the Background Job Board. Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before replacing the lane.`,
30303
31328
  args: {
30304
- task_id: z4.string().describe("Tracked background task ID or Background Job Board alias"),
30305
- reason: z4.string().optional().describe("Short cancellation reason")
31329
+ task_id: z5.string().describe("Tracked background task ID or Background Job Board alias"),
31330
+ reason: z5.string().optional().describe("Short cancellation reason")
30306
31331
  },
30307
31332
  async execute(args, toolContext) {
30308
31333
  const parentSessionID = toolContext?.sessionID;
@@ -30515,7 +31540,7 @@ async function abortAndVerifySession(options, taskID) {
30515
31540
  attempts,
30516
31541
  status: statusSnapshot.status
30517
31542
  });
30518
- await delay(retryIntervalMs);
31543
+ await delay2(retryIntervalMs);
30519
31544
  continue;
30520
31545
  }
30521
31546
  stableStoppedSince ??= Date.now();
@@ -30528,7 +31553,7 @@ async function abortAndVerifySession(options, taskID) {
30528
31553
  });
30529
31554
  return;
30530
31555
  }
30531
- await delay(retryIntervalMs);
31556
+ await delay2(retryIntervalMs);
30532
31557
  }
30533
31558
  log("[cancel-task] abort verification timed out", {
30534
31559
  taskID,
@@ -30596,13 +31621,13 @@ async function deleteAndVerifySession(options, taskID, reason) {
30596
31621
  });
30597
31622
  if (status.status === "busy" || status.status === "retry") {
30598
31623
  stableStoppedSince = undefined;
30599
- await delay(retryIntervalMs);
31624
+ await delay2(retryIntervalMs);
30600
31625
  continue;
30601
31626
  }
30602
31627
  stableStoppedSince ??= Date.now();
30603
31628
  if (Date.now() - stableStoppedSince >= stableStoppedMs)
30604
31629
  return;
30605
- await delay(retryIntervalMs);
31630
+ await delay2(retryIntervalMs);
30606
31631
  }
30607
31632
  throw new SessionStillRunningError(`Session delete returned but task did not stay stopped: ${taskID} (${lastStatus ?? "unknown"})`);
30608
31633
  }
@@ -30614,7 +31639,7 @@ async function getSessionStatus(client, taskID) {
30614
31639
  try {
30615
31640
  const result = await client.session.status();
30616
31641
  const data = result.data;
30617
- if (!isObjectRecord2(data)) {
31642
+ if (!isRecord(data)) {
30618
31643
  return { status: undefined, source: "invalid-data", keys: [] };
30619
31644
  }
30620
31645
  const keys = Object.keys(data).slice(0, 20);
@@ -30622,14 +31647,14 @@ async function getSessionStatus(client, taskID) {
30622
31647
  if (item === undefined) {
30623
31648
  return { status: "idle", source: "missing-from-map", keys };
30624
31649
  }
30625
- if (isObjectRecord2(item) && typeof item.type === "string") {
31650
+ if (isRecord(item) && typeof item.type === "string") {
30626
31651
  return { status: item.type, source: "task-map-entry", keys };
30627
31652
  }
30628
31653
  if (typeof data.type === "string") {
30629
31654
  return { status: data.type, source: "legacy-data-type", keys };
30630
31655
  }
30631
31656
  const nested = data.status;
30632
- if (isObjectRecord2(nested) && typeof nested.type === "string") {
31657
+ if (isRecord(nested) && typeof nested.type === "string") {
30633
31658
  return { status: nested.type, source: "legacy-data-status", keys };
30634
31659
  }
30635
31660
  return { status: undefined, source: "unknown-shape", keys };
@@ -30641,12 +31666,9 @@ async function getSessionStatus(client, taskID) {
30641
31666
  return { status: undefined, source: "lookup-error", keys: [] };
30642
31667
  }
30643
31668
  }
30644
- function delay(ms) {
31669
+ function delay2(ms) {
30645
31670
  return new Promise((resolve3) => setTimeout(resolve3, ms));
30646
31671
  }
30647
- function isObjectRecord2(value) {
30648
- return typeof value === "object" && value !== null;
30649
- }
30650
31672
  function isSessionID(value) {
30651
31673
  return /^ses_[\w-]+$/.test(value);
30652
31674
  }
@@ -30661,7 +31683,7 @@ async function getSessionParentID(client, taskID) {
30661
31683
  try {
30662
31684
  const response = await session2.get({ path: { id: taskID } });
30663
31685
  const data = response.data;
30664
- if (!isObjectRecord2(data))
31686
+ if (!isRecord(data))
30665
31687
  return;
30666
31688
  const parentID = data.parentID;
30667
31689
  return typeof parentID === "string" ? parentID : undefined;
@@ -30686,9 +31708,9 @@ function unknownTaskOutput(taskID, message) {
30686
31708
  }
30687
31709
  // src/tools/council.ts
30688
31710
  import {
30689
- tool as tool3
31711
+ tool as tool4
30690
31712
  } from "@opencode-ai/plugin";
30691
- var z5 = tool3.schema;
31713
+ var z6 = tool4.schema;
30692
31714
  function formatModelComposition(councillorResults) {
30693
31715
  return councillorResults.map((cr) => {
30694
31716
  const shortModel = shortModelLabel(cr.model);
@@ -30696,15 +31718,15 @@ function formatModelComposition(councillorResults) {
30696
31718
  }).join(", ");
30697
31719
  }
30698
31720
  function createCouncilTool(_ctx, councilManager) {
30699
- const council_session = tool3({
31721
+ const council_session = tool4({
30700
31722
  description: `Launch a multi-LLM council session for consensus-based analysis.
30701
31723
 
30702
31724
  Sends the prompt to multiple models (councillors) in parallel and returns their formatted responses for you to synthesize.
30703
31725
 
30704
31726
  Returns the councillor responses with a summary footer.`,
30705
31727
  args: {
30706
- prompt: z5.string().describe("The prompt to send to all councillors"),
30707
- preset: z5.string().optional().describe('Council preset to use (default: "default"). Must match a preset in the council config.')
31728
+ prompt: z6.string().describe("The prompt to send to all councillors"),
31729
+ preset: z6.string().optional().describe('Council preset to use (default: "default"). Must match a preset in the council config.')
30708
31730
  },
30709
31731
  async execute(args, toolContext) {
30710
31732
  if (!toolContext || typeof toolContext !== "object" || !("sessionID" in toolContext)) {
@@ -30756,14 +31778,14 @@ import * as fs10 from "node:fs";
30756
31778
  // src/tui-state.ts
30757
31779
  import * as fs9 from "node:fs";
30758
31780
  import * as os5 from "node:os";
30759
- import * as path14 from "node:path";
31781
+ import * as path17 from "node:path";
30760
31782
  var STATE_DIR = "oh-my-opencode-slim";
30761
31783
  var STATE_FILE = "tui-state.json";
30762
31784
  function dataDir() {
30763
- return process.env.XDG_DATA_HOME ?? path14.join(os5.homedir(), ".local", "share");
31785
+ return process.env.XDG_DATA_HOME ?? path17.join(os5.homedir(), ".local", "share");
30764
31786
  }
30765
31787
  function getTuiStatePath() {
30766
- return path14.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
31788
+ return path17.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
30767
31789
  }
30768
31790
  function emptySnapshot() {
30769
31791
  return {
@@ -30799,7 +31821,7 @@ async function readTuiSnapshotAsync() {
30799
31821
  function writeTuiSnapshot(snapshot) {
30800
31822
  try {
30801
31823
  const filePath = getTuiStatePath();
30802
- fs9.mkdirSync(path14.dirname(filePath), { recursive: true });
31824
+ fs9.mkdirSync(path17.dirname(filePath), { recursive: true });
30803
31825
  fs9.writeFileSync(filePath, `${JSON.stringify(snapshot)}
30804
31826
  `);
30805
31827
  } catch {}
@@ -30822,11 +31844,11 @@ function recordTuiAgentModel(input) {
30822
31844
  }
30823
31845
 
30824
31846
  // src/tools/preset-manager.ts
30825
- var COMMAND_NAME3 = "preset";
31847
+ var COMMAND_NAME4 = "preset";
30826
31848
  function createPresetManager(ctx, config) {
30827
31849
  let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
30828
31850
  async function handleCommandExecuteBefore(input, output) {
30829
- if (input.command !== COMMAND_NAME3) {
31851
+ if (input.command !== COMMAND_NAME4) {
30830
31852
  return;
30831
31853
  }
30832
31854
  output.parts.length = 0;
@@ -30845,11 +31867,11 @@ function createPresetManager(ctx, config) {
30845
31867
  }
30846
31868
  function registerCommand(opencodeConfig) {
30847
31869
  const configCommand = opencodeConfig.command;
30848
- if (!configCommand?.[COMMAND_NAME3]) {
31870
+ if (!configCommand?.[COMMAND_NAME4]) {
30849
31871
  if (!opencodeConfig.command) {
30850
31872
  opencodeConfig.command = {};
30851
31873
  }
30852
- opencodeConfig.command[COMMAND_NAME3] = {
31874
+ opencodeConfig.command[COMMAND_NAME4] = {
30853
31875
  template: "List available presets and switch between them",
30854
31876
  description: "Switch agent presets at runtime (e.g., /preset cheap, /preset powerful)"
30855
31877
  };
@@ -30999,14 +32021,14 @@ var BINARY_PREFIXES = [
30999
32021
  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.";
31000
32022
  // src/tools/smartfetch/tool.ts
31001
32023
  import os6 from "node:os";
31002
- import path18 from "node:path";
32024
+ import path21 from "node:path";
31003
32025
  import {
31004
- tool as tool4
32026
+ tool as tool5
31005
32027
  } from "@opencode-ai/plugin";
31006
32028
 
31007
32029
  // src/tools/smartfetch/binary.ts
31008
32030
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
31009
- import path15 from "node:path";
32031
+ import path18 from "node:path";
31010
32032
  function extensionForMime(contentType) {
31011
32033
  const mime = contentType.split(";")[0]?.trim().toLowerCase();
31012
32034
  const map = {
@@ -31027,10 +32049,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
31027
32049
  async function saveBinary(binaryDir, data, contentType, filename) {
31028
32050
  await mkdir2(binaryDir, { recursive: true });
31029
32051
  const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
31030
- const parsed = path15.parse(initialName);
32052
+ const parsed = path18.parse(initialName);
31031
32053
  for (let attempt = 0;attempt < 1000; attempt++) {
31032
32054
  const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
31033
- const file = path15.join(binaryDir, candidateName);
32055
+ const file = path18.join(binaryDir, candidateName);
31034
32056
  try {
31035
32057
  await writeFile2(file, data, { flag: "wx" });
31036
32058
  return file;
@@ -31169,7 +32191,7 @@ var M = class u2 {
31169
32191
  return this.#S;
31170
32192
  }
31171
32193
  constructor(e) {
31172
- let { max: t = 0, ttl: i, ttlResolution: s = 1, ttlAutopurge: n, updateAgeOnGet: o, updateAgeOnHas: r, allowStale: h, dispose: l, onInsert: c, disposeAfter: f, noDisposeOnSet: g, noUpdateTTL: p, maxSize: T = 0, maxEntrySize: w = 0, sizeCalculation: y, fetchMethod: a, memoMethod: m, noDeleteOnFetchRejection: _, noDeleteOnStaleGet: b, allowStaleOnFetchRejection: d, allowStaleOnFetchAbort: A, ignoreFetchAbort: z6, perf: x } = e;
32194
+ let { max: t = 0, ttl: i, ttlResolution: s = 1, ttlAutopurge: n, updateAgeOnGet: o, updateAgeOnHas: r, allowStale: h, dispose: l, onInsert: c, disposeAfter: f, noDisposeOnSet: g, noUpdateTTL: p, maxSize: T = 0, maxEntrySize: w = 0, sizeCalculation: y, fetchMethod: a, memoMethod: m, noDeleteOnFetchRejection: _, noDeleteOnStaleGet: b, allowStaleOnFetchRejection: d, allowStaleOnFetchAbort: A, ignoreFetchAbort: z7, perf: x } = e;
31173
32195
  if (x !== undefined && typeof x?.now != "function")
31174
32196
  throw new TypeError("perf option must have a now() method if specified");
31175
32197
  if (this.#m = x ?? C, t !== 0 && !F(t))
@@ -31187,7 +32209,7 @@ var M = class u2 {
31187
32209
  throw new TypeError("memoMethod must be a function if defined");
31188
32210
  if (this.#U = m, a !== undefined && typeof a != "function")
31189
32211
  throw new TypeError("fetchMethod must be a function if specified");
31190
- if (this.#M = a, this.#W = !!a, this.#s = new Map, this.#i = Array.from({ length: t }).fill(undefined), this.#t = Array.from({ length: t }).fill(undefined), this.#a = new v(t), this.#c = new v(t), this.#l = 0, this.#h = 0, this.#y = R.create(t), this.#n = 0, this.#b = 0, typeof l == "function" && (this.#w = l), typeof c == "function" && (this.#x = c), typeof f == "function" ? (this.#S = f, this.#r = []) : (this.#S = undefined, this.#r = undefined), this.#T = !!this.#w, this.#j = !!this.#x, this.#f = !!this.#S, this.noDisposeOnSet = !!g, this.noUpdateTTL = !!p, this.noDeleteOnFetchRejection = !!_, this.allowStaleOnFetchRejection = !!d, this.allowStaleOnFetchAbort = !!A, this.ignoreFetchAbort = !!z6, this.maxEntrySize !== 0) {
32212
+ if (this.#M = a, this.#W = !!a, this.#s = new Map, this.#i = Array.from({ length: t }).fill(undefined), this.#t = Array.from({ length: t }).fill(undefined), this.#a = new v(t), this.#c = new v(t), this.#l = 0, this.#h = 0, this.#y = R.create(t), this.#n = 0, this.#b = 0, typeof l == "function" && (this.#w = l), typeof c == "function" && (this.#x = c), typeof f == "function" ? (this.#S = f, this.#r = []) : (this.#S = undefined, this.#r = undefined), this.#T = !!this.#w, this.#j = !!this.#x, this.#f = !!this.#S, this.noDisposeOnSet = !!g, this.noUpdateTTL = !!p, this.noDeleteOnFetchRejection = !!_, this.allowStaleOnFetchRejection = !!d, this.allowStaleOnFetchAbort = !!A, this.ignoreFetchAbort = !!z7, this.maxEntrySize !== 0) {
31191
32213
  if (this.#u !== 0 && !F(this.#u))
31192
32214
  throw new TypeError("maxSize must be a positive integer if specified");
31193
32215
  if (!F(this.maxEntrySize))
@@ -31567,8 +32589,8 @@ var M = class u2 {
31567
32589
  let A = this.#p(b);
31568
32590
  if (!y && !A)
31569
32591
  return a && (a.fetch = "hit"), this.#L(b), s && this.#D(b), a && this.#E(a, b), d;
31570
- let z6 = this.#P(e, b, _, w), v = z6.__staleWhileFetching !== undefined && i;
31571
- return a && (a.fetch = A ? "stale" : "refresh", v && A && (a.returnedStale = true)), v ? z6.__staleWhileFetching : z6.__returned = z6;
32592
+ let z7 = this.#P(e, b, _, w), v = z7.__staleWhileFetching !== undefined && i;
32593
+ return a && (a.fetch = A ? "stale" : "refresh", v && A && (a.returnedStale = true)), v ? z7.__staleWhileFetching : z7.__returned = z7;
31572
32594
  }
31573
32595
  }
31574
32596
  forceFetch(e, t = {}) {
@@ -31684,7 +32706,7 @@ var M = class u2 {
31684
32706
  };
31685
32707
 
31686
32708
  // src/tools/smartfetch/network.ts
31687
- import path16 from "node:path";
32709
+ import path19 from "node:path";
31688
32710
 
31689
32711
  // src/tools/smartfetch/utils.ts
31690
32712
  var import_readability = __toESM(require_readability(), 1);
@@ -32409,7 +33431,7 @@ function inferFilenameFromUrl(url) {
32409
33431
  function truncateFilename(name, maxLength = 180) {
32410
33432
  if (name.length <= maxLength)
32411
33433
  return name;
32412
- const parsed = path16.parse(name);
33434
+ const parsed = path19.parse(name);
32413
33435
  const ext = parsed.ext || "";
32414
33436
  const baseLimit = Math.max(1, maxLength - ext.length);
32415
33437
  return `${parsed.name.slice(0, baseLimit)}${ext}`;
@@ -32579,9 +33601,9 @@ function isInvalidLlmsResult(fetchResult) {
32579
33601
  }
32580
33602
 
32581
33603
  // src/tools/smartfetch/secondary-model.ts
32582
- import { existsSync as existsSync10 } from "node:fs";
33604
+ import { existsSync as existsSync12 } from "node:fs";
32583
33605
  import { readFile as readFile4 } from "node:fs/promises";
32584
- import path17 from "node:path";
33606
+ import path20 from "node:path";
32585
33607
  function parseModelRef(value) {
32586
33608
  if (!value)
32587
33609
  return;
@@ -32607,8 +33629,8 @@ function pickAgentModelRef(value) {
32607
33629
  }
32608
33630
  function findPreferredOpenCodeConfigPath(baseDir) {
32609
33631
  for (const file of ["opencode.jsonc", "opencode.json"]) {
32610
- const fullPath = path17.join(baseDir, file);
32611
- if (existsSync10(fullPath))
33632
+ const fullPath = path20.join(baseDir, file);
33633
+ if (existsSync12(fullPath))
32612
33634
  return fullPath;
32613
33635
  }
32614
33636
  return;
@@ -32624,7 +33646,7 @@ async function readOpenCodeConfigFile(configPath) {
32624
33646
  }
32625
33647
  }
32626
33648
  async function readEffectiveOpenCodeConfig(directory) {
32627
- const projectDir = path17.join(directory, ".opencode");
33649
+ const projectDir = path20.join(directory, ".opencode");
32628
33650
  const userDirs = getConfigSearchDirs();
32629
33651
  const projectPath = findPreferredOpenCodeConfigPath(projectDir);
32630
33652
  const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
@@ -32705,6 +33727,29 @@ function isUsableSecondaryText(text) {
32705
33727
  return false;
32706
33728
  return true;
32707
33729
  }
33730
+ var SESSION_DELETE_RETRIES = 3;
33731
+ var SESSION_DELETE_RETRY_DELAY_MS = 500;
33732
+ var SECONDARY_MODEL_TIMEOUT_MS = 30000;
33733
+ var _testConfig = {
33734
+ deleteRetryDelayMs: SESSION_DELETE_RETRY_DELAY_MS
33735
+ };
33736
+ async function deleteSessionSafely(client, sessionId, directory) {
33737
+ for (let attempt = 1;attempt <= SESSION_DELETE_RETRIES; attempt++) {
33738
+ try {
33739
+ await client.session.delete({
33740
+ path: { id: sessionId },
33741
+ query: { directory }
33742
+ });
33743
+ return;
33744
+ } catch (error) {
33745
+ if (attempt >= SESSION_DELETE_RETRIES) {
33746
+ console.warn(`[smartfetch] Failed to clean up secondary session ${sessionId} ` + `after ${SESSION_DELETE_RETRIES} attempts: ` + (error instanceof Error ? error.message : String(error)));
33747
+ return;
33748
+ }
33749
+ await new Promise((resolve3) => setTimeout(resolve3, _testConfig.deleteRetryDelayMs));
33750
+ }
33751
+ }
33752
+ }
32708
33753
  async function runSecondaryModel(client, directory, model, prompt, content) {
32709
33754
  const session2 = await client.session.create({
32710
33755
  responseStyle: "data",
@@ -32731,23 +33776,26 @@ Note: only the first ${inputChars} characters of a longer fetched document were
32731
33776
  const toolIDsData = toolIDsResponse;
32732
33777
  const toolIDs = Array.isArray(toolIDsData.data) ? toolIDsData.data : Array.isArray(toolIDsResponse) ? toolIDsResponse : [];
32733
33778
  const disabledTools = Object.fromEntries((toolIDs || []).map((id) => [id, false]));
32734
- const result = await client.session.prompt({
32735
- responseStyle: "data",
32736
- throwOnError: true,
32737
- path: { id: sessionId },
32738
- query: { directory },
32739
- body: {
32740
- model,
32741
- system: "Answer only from the supplied content. Do not use tools or outside knowledge.",
32742
- tools: disabledTools,
32743
- parts: [
32744
- {
32745
- type: "text",
32746
- text: buildPrompt(truncatedContent, effectivePrompt)
32747
- }
32748
- ]
32749
- }
32750
- });
33779
+ const result = await Promise.race([
33780
+ client.session.prompt({
33781
+ responseStyle: "data",
33782
+ throwOnError: true,
33783
+ path: { id: sessionId },
33784
+ query: { directory },
33785
+ body: {
33786
+ model,
33787
+ system: "Answer only from the supplied content. Do not use tools or outside knowledge.",
33788
+ tools: disabledTools,
33789
+ parts: [
33790
+ {
33791
+ type: "text",
33792
+ text: buildPrompt(truncatedContent, effectivePrompt)
33793
+ }
33794
+ ]
33795
+ }
33796
+ }),
33797
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Secondary model timed out")), SECONDARY_MODEL_TIMEOUT_MS))
33798
+ ]);
32751
33799
  const parts = result?.data?.parts ?? result?.parts ?? [];
32752
33800
  const text = parts.map((part) => part?.type === "text" ? part.text || "" : "").join("").trim();
32753
33801
  return {
@@ -32757,12 +33805,7 @@ Note: only the first ${inputChars} characters of a longer fetched document were
32757
33805
  sourceChars
32758
33806
  };
32759
33807
  } finally {
32760
- await client.session.delete({
32761
- path: { id: sessionId },
32762
- query: { directory }
32763
- }).catch(() => {
32764
- return;
32765
- });
33808
+ await deleteSessionSafely(client, sessionId, directory);
32766
33809
  }
32767
33810
  }
32768
33811
  async function runSecondaryModelWithFallback(client, directory, models, prompt, content) {
@@ -32783,20 +33826,20 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
32783
33826
  }
32784
33827
 
32785
33828
  // src/tools/smartfetch/tool.ts
32786
- var z6 = tool4.schema;
33829
+ var z7 = tool5.schema;
32787
33830
  function createWebfetchTool(pluginCtx, options = {}) {
32788
- const binaryDir = options.binaryDir || path18.join(os6.tmpdir(), "opencode-smartfetch");
32789
- return tool4({
33831
+ const binaryDir = options.binaryDir || path21.join(os6.tmpdir(), "opencode-smartfetch");
33832
+ return tool5({
32790
33833
  description: WEBFETCH_DESCRIPTION,
32791
33834
  args: {
32792
- url: z6.httpUrl(),
32793
- format: z6.enum(["text", "markdown", "html"]).default("markdown"),
32794
- timeout: z6.number().positive().max(MAX_TIMEOUT_SECONDS).optional().describe("Timeout in seconds, max 120."),
32795
- prompt: z6.string().optional().describe("Optional extraction task to run on the fetched content using a cheap secondary model."),
32796
- extract_main: z6.boolean().default(true),
32797
- prefer_llms_txt: z6.enum(["auto", "always", "never"]).default("auto"),
32798
- include_metadata: z6.boolean().default(true),
32799
- save_binary: z6.boolean().default(false).describe("Save binary payload to disk when it fits within the active download limit.")
33835
+ url: z7.httpUrl(),
33836
+ format: z7.enum(["text", "markdown", "html"]).default("markdown"),
33837
+ timeout: z7.number().positive().max(MAX_TIMEOUT_SECONDS).optional().describe("Timeout in seconds, max 120."),
33838
+ prompt: z7.string().optional().describe("Optional extraction task to run on the fetched content using a cheap secondary model."),
33839
+ extract_main: z7.boolean().default(true),
33840
+ prefer_llms_txt: z7.enum(["auto", "always", "never"]).default("auto"),
33841
+ include_metadata: z7.boolean().default(true),
33842
+ save_binary: z7.boolean().default(false).describe("Save binary payload to disk when it fits within the active download limit.")
32800
33843
  },
32801
33844
  async execute(args, ctx) {
32802
33845
  const secondaryModels = await readSecondaryModelFromConfig(ctx.directory || pluginCtx.directory);
@@ -33341,7 +34384,7 @@ async function appLog(ctx, level, message) {
33341
34384
  }
33342
34385
  var HEALTH_CHECK = {
33343
34386
  minAgents: 5,
33344
- minTools: 5,
34387
+ minTools: 4,
33345
34388
  minMcps: 1
33346
34389
  };
33347
34390
  async function probeJSDOM() {
@@ -33378,6 +34421,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33378
34421
  let jsonErrorRecoveryHook;
33379
34422
  let foregroundFallback;
33380
34423
  let deepworkCommandHook;
34424
+ let reflectCommandHook;
33381
34425
  let taskSessionManagerHook;
33382
34426
  let backgroundJobBoard;
33383
34427
  let interviewManager;
@@ -33385,6 +34429,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33385
34429
  let companionManager;
33386
34430
  let councilTools;
33387
34431
  let cancelTaskTools;
34432
+ let acpRunTools;
33388
34433
  let webfetch;
33389
34434
  let rewriteDisplayNameMentions;
33390
34435
  let toolCount = 0;
@@ -33403,32 +34448,13 @@ var OhMyOpenCodeLite = async (ctx) => {
33403
34448
  agentDefs = createAgents(config);
33404
34449
  agents = getAgentConfigs(config);
33405
34450
  modelArrayMap = {};
33406
- for (const agentDef of agentDefs) {
33407
- if (agentDef._modelArray && agentDef._modelArray.length > 0) {
33408
- modelArrayMap[agentDef.name] = agentDef._modelArray;
33409
- }
33410
- }
33411
34451
  runtimeChains = {};
33412
34452
  for (const agentDef of agentDefs) {
33413
34453
  if (agentDef._modelArray?.length) {
34454
+ modelArrayMap[agentDef.name] = agentDef._modelArray;
33414
34455
  runtimeChains[agentDef.name] = agentDef._modelArray.map((m) => m.id);
33415
34456
  }
33416
34457
  }
33417
- const activePresetForFallback = getActiveRuntimePreset() ?? config.preset ?? null;
33418
- if (config.fallback?.enabled !== false) {
33419
- const chains = normalizeFallbackChainsForPreset(config.fallback?.chains ?? {}, activePresetForFallback);
33420
- for (const [agentName, chainModels] of Object.entries(chains)) {
33421
- const existing = runtimeChains[agentName] ?? [];
33422
- const seen = new Set(existing);
33423
- for (const m of chainModels) {
33424
- if (!seen.has(m)) {
33425
- seen.add(m);
33426
- existing.push(m);
33427
- }
33428
- }
33429
- runtimeChains[agentName] = existing;
33430
- }
33431
- }
33432
34458
  multiplexerConfig = {
33433
34459
  type: config.multiplexer?.type ?? "none",
33434
34460
  layout: config.multiplexer?.layout ?? "main-vertical",
@@ -33448,6 +34474,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33448
34474
  depthTracker = new SubagentDepthTracker;
33449
34475
  councilTools = config.council ? createCouncilTool(ctx, new CouncilManager(ctx, config, depthTracker, multiplexerEnabled)) : {};
33450
34476
  mcps = createBuiltinMcps(config.disabled_mcps, config.websearch);
34477
+ acpRunTools = Object.keys(config.acpAgents ?? {}).length > 0 ? { acp_run: createAcpRunTool(config.acpAgents) } : {};
33451
34478
  webfetch = createWebfetchTool(ctx);
33452
34479
  backgroundJobBoard = new BackgroundJobBoard({
33453
34480
  maxReusablePerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
@@ -33456,7 +34483,8 @@ var OhMyOpenCodeLite = async (ctx) => {
33456
34483
  });
33457
34484
  multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig, backgroundJobBoard);
33458
34485
  autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
33459
- autoUpdate: config.autoUpdate ?? true
34486
+ autoUpdate: config.autoUpdate ?? true,
34487
+ companion: config.companion
33460
34488
  });
33461
34489
  phaseReminderHook = createPhaseReminderHook();
33462
34490
  filterAvailableSkillsHook = createFilterAvailableSkillsHook(ctx, config);
@@ -33470,6 +34498,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33470
34498
  jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
33471
34499
  foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
33472
34500
  deepworkCommandHook = createDeepworkCommandHook();
34501
+ reflectCommandHook = createReflectCommandHook();
33473
34502
  taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
33474
34503
  maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
33475
34504
  readContextMinLines: config.backgroundJobs?.readContextMinLines ?? 10,
@@ -33485,7 +34514,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33485
34514
  backgroundJobBoard,
33486
34515
  shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
33487
34516
  });
33488
- toolCount = Object.keys(councilTools).length + Object.keys(cancelTaskTools).length + 1 + 2;
34517
+ toolCount = Object.keys(councilTools).length + Object.keys(cancelTaskTools).length + Object.keys(acpRunTools).length + 1 + 2;
33489
34518
  } catch (err) {
33490
34519
  log("[plugin] FATAL: init failed", String(err));
33491
34520
  await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
@@ -33521,6 +34550,22 @@ var OhMyOpenCodeLite = async (ctx) => {
33521
34550
  appLog(ctx, "warn", msg).catch(() => {});
33522
34551
  }
33523
34552
  });
34553
+ if (config.companion?.enabled === true) {
34554
+ try {
34555
+ const companionResult = await ensureCompanionVersion({
34556
+ config: config.companion,
34557
+ downloadTimeoutMs: 3000,
34558
+ lockTimeoutMs: 500
34559
+ });
34560
+ if (companionResult.status === "installed") {
34561
+ log("[companion] updated before startup", companionResult.version);
34562
+ } else if (companionResult.status === "failed") {
34563
+ log("[companion] startup update failed", companionResult.error);
34564
+ }
34565
+ } catch (err) {
34566
+ log("[companion] startup update failed", String(err));
34567
+ }
34568
+ }
33524
34569
  companionManager.onLoad();
33525
34570
  return {
33526
34571
  name: "oh-my-opencode-slim",
@@ -33528,6 +34573,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33528
34573
  tool: {
33529
34574
  ...councilTools,
33530
34575
  ...cancelTaskTools,
34576
+ ...acpRunTools,
33531
34577
  webfetch,
33532
34578
  ast_grep_search,
33533
34579
  ast_grep_replace
@@ -33555,34 +34601,11 @@ var OhMyOpenCodeLite = async (ctx) => {
33555
34601
  }
33556
34602
  }
33557
34603
  const configAgent = opencodeConfig.agent;
33558
- const activePresetForFallback = getActiveRuntimePreset() ?? config.preset ?? null;
33559
- const fallbackChainsEnabled = config.fallback?.enabled !== false;
33560
- const fallbackChains = fallbackChainsEnabled ? normalizeFallbackChainsForPreset(config.fallback?.chains ?? {}, activePresetForFallback) : {};
33561
- const effectiveArrays = {};
33562
- for (const [agentName, models] of Object.entries(modelArrayMap)) {
33563
- effectiveArrays[agentName] = [...models];
33564
- }
33565
- for (const [agentName, chainModels] of Object.entries(fallbackChains)) {
33566
- if (!chainModels || chainModels.length === 0)
33567
- continue;
33568
- if (!effectiveArrays[agentName]) {
33569
- const entry = configAgent[agentName];
33570
- const currentModel = typeof entry?.model === "string" ? entry.model : undefined;
33571
- effectiveArrays[agentName] = currentModel ? [{ id: currentModel }] : [];
33572
- }
33573
- const seen = new Set(effectiveArrays[agentName].map((m) => m.id));
33574
- for (const chainModel of chainModels) {
33575
- if (!seen.has(chainModel)) {
33576
- seen.add(chainModel);
33577
- effectiveArrays[agentName].push({ id: chainModel });
33578
- }
33579
- }
33580
- }
33581
- if (Object.keys(effectiveArrays).length > 0) {
33582
- for (const [agentName, modelArray] of Object.entries(effectiveArrays)) {
33583
- if (modelArray.length === 0)
34604
+ if (Object.keys(modelArrayMap).length > 0) {
34605
+ for (const [agentName, models] of Object.entries(modelArrayMap)) {
34606
+ if (models.length === 0)
33584
34607
  continue;
33585
- const chosen = modelArray[0];
34608
+ const chosen = models[0];
33586
34609
  const entry = configAgent[agentName];
33587
34610
  if (entry) {
33588
34611
  entry.model = chosen.id;
@@ -33718,6 +34741,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33718
34741
  }
33719
34742
  interviewManager.registerCommand(opencodeConfig);
33720
34743
  deepworkCommandHook.registerCommand(opencodeConfig);
34744
+ reflectCommandHook.registerCommand(opencodeConfig);
33721
34745
  presetManager.registerCommand(opencodeConfig);
33722
34746
  },
33723
34747
  event: async (input) => {
@@ -33784,6 +34808,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33784
34808
  await interviewManager.handleCommandExecuteBefore(input, output);
33785
34809
  await presetManager.handleCommandExecuteBefore(input, output);
33786
34810
  await deepworkCommandHook.handleCommandExecuteBefore(input, output);
34811
+ await reflectCommandHook.handleCommandExecuteBefore(input, output);
33787
34812
  },
33788
34813
  "chat.headers": chatHeadersHook["chat.headers"],
33789
34814
  "chat.message": async (input, output) => {