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/cli/index.js CHANGED
@@ -19,14 +19,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  // src/utils/compat.ts
22
- var exports_compat = {};
23
- __export(exports_compat, {
24
- isBun: () => isBun,
25
- crossWrite: () => crossWrite,
26
- crossSpawn: () => crossSpawn
27
- });
28
22
  import { spawn as nodeSpawn } from "node:child_process";
29
- import { writeFile as fsWriteFile } from "node:fs/promises";
30
23
  function collectStream(stream) {
31
24
  if (!stream)
32
25
  return () => Promise.resolve("");
@@ -69,13 +62,7 @@ function crossSpawn(command, options) {
69
62
  }
70
63
  };
71
64
  }
72
- async function crossWrite(path, data) {
73
- await fsWriteFile(path, Buffer.from(data));
74
- }
75
- var isBun;
76
- var init_compat = __esm(() => {
77
- isBun = typeof globalThis.Bun !== "undefined";
78
- });
65
+ var init_compat = () => {};
79
66
 
80
67
  // src/utils/zip-extractor.ts
81
68
  var exports_zip_extractor = {};
@@ -306,11 +293,11 @@ var SUBAGENT_NAMES = [
306
293
  "council",
307
294
  "councillor"
308
295
  ];
309
- var ORCHESTRATOR_NAME = "orchestrator";
310
- var ALL_AGENT_NAMES = [ORCHESTRATOR_NAME, ...SUBAGENT_NAMES];
296
+ var ALL_AGENT_NAMES = ["orchestrator", ...SUBAGENT_NAMES];
311
297
  var PROTECTED_AGENTS = new Set(["orchestrator", "councillor"]);
312
- var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
313
298
  var MAX_POLL_TIME_MS = 5 * 60 * 1000;
299
+ 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!`;
300
+ var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
314
301
  // src/config/council-schema.ts
315
302
  import { z } from "zod";
316
303
  var ModelIdSchema = z.string().regex(/^[^/\s]+\/[^\s]+$/, 'Expected provider/model format (e.g. "openai/gpt-5.4-mini")');
@@ -357,17 +344,11 @@ var CouncilConfigSchema = z.object({
357
344
  default_preset: z.string().default("default"),
358
345
  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).'),
359
346
  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."),
360
- master: z.unknown().optional().describe("DEPRECATED — ignored. Council agent synthesizes directly."),
361
- master_timeout: z.unknown().optional().describe('DEPRECATED — ignored. Use "timeout" instead.'),
362
- master_fallback: z.unknown().optional().describe("DEPRECATED — ignored. No separate master session.")
347
+ master: z.unknown().optional().describe("DEPRECATED — ignored. Council agent synthesizes directly.")
363
348
  }).transform((data) => {
364
349
  const deprecated = [];
365
350
  if (data.master !== undefined)
366
351
  deprecated.push("master");
367
- if (data.master_timeout !== undefined)
368
- deprecated.push("master_timeout");
369
- if (data.master_fallback !== undefined)
370
- deprecated.push("master_fallback");
371
352
  const legacyMasterModel = typeof data.master === "object" && data.master !== null && "model" in data.master && typeof data.master.model === "string" ? data.master.model : undefined;
372
353
  return {
373
354
  presets: data.presets,
@@ -409,15 +390,6 @@ var ManualPlanSchema = z2.object({
409
390
  librarian: ManualAgentPlanSchema,
410
391
  fixer: ManualAgentPlanSchema
411
392
  }).strict();
412
- var AgentModelChainSchema = z2.array(z2.string()).min(1);
413
- var FallbackChainsSchema = z2.object({
414
- orchestrator: AgentModelChainSchema.optional(),
415
- oracle: AgentModelChainSchema.optional(),
416
- designer: AgentModelChainSchema.optional(),
417
- explorer: AgentModelChainSchema.optional(),
418
- librarian: AgentModelChainSchema.optional(),
419
- fixer: AgentModelChainSchema.optional()
420
- }).catchall(AgentModelChainSchema);
421
393
  var AgentOverrideConfigSchema = z2.object({
422
394
  model: z2.union([
423
395
  z2.string(),
@@ -480,14 +452,32 @@ var FailoverConfigSchema = z2.object({
480
452
  enabled: z2.boolean().default(true),
481
453
  timeoutMs: z2.number().min(0).default(15000),
482
454
  retryDelayMs: z2.number().min(0).default(500),
483
- chains: FallbackChainsSchema.default({}),
484
455
  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.")
485
- });
456
+ }).strict();
486
457
  var CompanionConfigSchema = z2.object({
487
458
  enabled: z2.boolean().optional(),
459
+ binaryPath: z2.string().min(1).optional().describe("Path to a custom companion binary to launch."),
488
460
  position: z2.enum(["bottom-right", "bottom-left", "top-right", "top-left"]).optional(),
489
- size: z2.enum(["small", "medium", "large"]).optional()
461
+ size: z2.enum(["small", "medium", "large"]).optional(),
462
+ gifPack: z2.enum(["default"]).optional().describe("Bundled companion animation pack to use."),
463
+ loopStyle: z2.enum(["classic", "smooth"]).optional().describe("Companion animation playback style: classic loops or smooth ping-pong playback."),
464
+ speed: z2.number().min(0.25).max(4).optional().describe("Companion animation playback speed multiplier. Defaults to 1."),
465
+ debug: z2.boolean().optional().describe("Enable verbose native companion debug logs.")
490
466
  });
467
+ var AcpAgentPermissionModeSchema = z2.enum(["ask", "allow", "reject"]);
468
+ var AcpAgentConfigSchema = z2.object({
469
+ command: z2.string().min(1),
470
+ args: z2.array(z2.string()).default([]),
471
+ env: z2.record(z2.string(), z2.string()).default({}),
472
+ cwd: z2.string().min(1).optional(),
473
+ description: z2.string().min(1).optional(),
474
+ prompt: z2.string().min(1).optional(),
475
+ orchestratorPrompt: z2.string().min(1).optional(),
476
+ wrapperModel: ProviderModelIdSchema.optional(),
477
+ timeoutMs: z2.number().int().min(1000).max(900000).default(300000),
478
+ permissionMode: AcpAgentPermissionModeSchema.default("ask")
479
+ }).strict();
480
+ var AcpAgentsConfigSchema = z2.record(z2.string(), AcpAgentConfigSchema);
491
481
  function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
492
482
  for (const [name, override] of Object.entries(overrides)) {
493
483
  const isBuiltInOrAlias = ALL_AGENT_NAMES.includes(name) || AGENT_ALIASES[name] !== undefined;
@@ -513,10 +503,7 @@ function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
513
503
  var PluginConfigSchema = z2.object({
514
504
  preset: z2.string().optional(),
515
505
  setDefaultAgent: z2.boolean().optional(),
516
- scoringEngineVersion: z2.enum(["v1", "v2-shadow", "v2"]).optional(),
517
- balanceProviderUsage: z2.boolean().optional(),
518
506
  autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
519
- manualPlan: ManualPlanSchema.optional(),
520
507
  presets: z2.record(z2.string(), PresetSchema).optional(),
521
508
  agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
522
509
  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."),
@@ -528,7 +515,8 @@ var PluginConfigSchema = z2.object({
528
515
  backgroundJobs: BackgroundJobsConfigSchema.optional(),
529
516
  fallback: FailoverConfigSchema.optional(),
530
517
  council: CouncilConfigSchema.optional(),
531
- companion: CompanionConfigSchema.optional()
518
+ companion: CompanionConfigSchema.optional(),
519
+ acpAgents: AcpAgentsConfigSchema.optional()
532
520
  }).superRefine((value, ctx) => {
533
521
  if (value.agents) {
534
522
  validateCustomOnlyPromptFields(value.agents, ctx, ["agents"]);
@@ -587,11 +575,23 @@ var CUSTOM_SKILLS = [
587
575
  allowedAgents: ["orchestrator"],
588
576
  sourcePath: "src/skills/deepwork"
589
577
  },
578
+ {
579
+ name: "reflect",
580
+ description: "Review repeated work and suggest reusable workflow improvements",
581
+ allowedAgents: ["orchestrator"],
582
+ sourcePath: "src/skills/reflect"
583
+ },
590
584
  {
591
585
  name: "oh-my-opencode-slim",
592
586
  description: "Configure, customize, and safely improve oh-my-opencode-slim setups",
593
587
  allowedAgents: ["orchestrator"],
594
588
  sourcePath: "src/skills/oh-my-opencode-slim"
589
+ },
590
+ {
591
+ name: "worktrees",
592
+ description: "Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work",
593
+ allowedAgents: ["orchestrator"],
594
+ sourcePath: "src/skills/worktrees"
595
595
  }
596
596
  ];
597
597
  function getCustomSkillsDir() {
@@ -737,7 +737,11 @@ function generateLiteConfig(installConfig) {
737
737
  config.companion = {
738
738
  enabled: true,
739
739
  position: "bottom-right",
740
- size: "medium"
740
+ size: "medium",
741
+ gifPack: "default",
742
+ loopStyle: "classic",
743
+ speed: 1,
744
+ debug: false
741
745
  };
742
746
  }
743
747
  return config;
@@ -745,12 +749,7 @@ function generateLiteConfig(installConfig) {
745
749
 
746
750
  // src/cli/config-io.ts
747
751
  var PACKAGE_NAME = "oh-my-opencode-slim";
748
- var DEFAULT_OPENCODE_AGENTS_TO_DISABLE = [
749
- "build",
750
- "explore",
751
- "general",
752
- "plan"
753
- ];
752
+ var DEFAULT_OPENCODE_AGENTS_TO_DISABLE = ["explore", "general"];
754
753
  function isString(value) {
755
754
  return typeof value === "string";
756
755
  }
@@ -1278,6 +1277,7 @@ function mergePluginConfigs(base, override) {
1278
1277
  backgroundJobs: deepMerge(base.backgroundJobs, override.backgroundJobs),
1279
1278
  fallback: deepMerge(base.fallback, override.fallback),
1280
1279
  council: deepMerge(base.council, override.council),
1280
+ acpAgents: deepMerge(base.acpAgents, override.acpAgents),
1281
1281
  companion: deepMerge(base.companion, override.companion)
1282
1282
  };
1283
1283
  }
@@ -1593,22 +1593,67 @@ function writeBackgroundSubagentsBlock(targetPath) {
1593
1593
  writeFileSync2(targetPath, nextContent);
1594
1594
  }
1595
1595
 
1596
- // src/cli/companion.ts
1596
+ // src/companion/updater.ts
1597
+ init_compat();
1598
+ import { createHash } from "node:crypto";
1597
1599
  import {
1598
1600
  chmodSync,
1599
1601
  copyFileSync as copyFileSync3,
1600
1602
  existsSync as existsSync6,
1601
1603
  mkdirSync as mkdirSync5,
1602
1604
  mkdtempSync,
1605
+ readFileSync as readFileSync5,
1603
1606
  renameSync as renameSync2,
1604
1607
  rmSync as rmSync2,
1608
+ statSync as statSync4,
1605
1609
  writeFileSync as writeFileSync3
1606
1610
  } from "node:fs";
1607
- import { homedir as homedir4, tmpdir } from "node:os";
1611
+ import { homedir as homedir4, platform, tmpdir } from "node:os";
1608
1612
  import * as path2 from "node:path";
1609
- var COMPANION_VERSION = "0.1.0";
1610
- var COMPANION_TAG = "companion-v0.1.0";
1611
- var GITHUB_REPO = "alvinunreal/oh-my-opencode-slim";
1613
+ import { setTimeout as delay } from "node:timers/promises";
1614
+
1615
+ // src/utils/logger.ts
1616
+ import { appendFile } from "node:fs/promises";
1617
+ var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
1618
+ var logFile = null;
1619
+ var writeChain = Promise.resolve();
1620
+ function log(message, data) {
1621
+ const target = logFile;
1622
+ if (!target)
1623
+ return;
1624
+ try {
1625
+ const timestamp = new Date().toISOString();
1626
+ let dataStr = "";
1627
+ if (data !== undefined) {
1628
+ try {
1629
+ dataStr = JSON.stringify(data);
1630
+ } catch {
1631
+ dataStr = "[unserializable]";
1632
+ }
1633
+ }
1634
+ const logEntry = `[${timestamp}] ${message} ${dataStr}
1635
+ `;
1636
+ writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
1637
+ } catch {}
1638
+ }
1639
+
1640
+ // src/companion/updater.ts
1641
+ var DOWNLOAD_TIMEOUT_MS = 30000;
1642
+ var LOCK_TIMEOUT_MS = 2000;
1643
+ var STALE_LOCK_MS = 5 * 60000;
1644
+ var FIRST_METADATA_VERSION = "0.1.2";
1645
+ var COMPANION_MANIFEST = {
1646
+ version: "0.1.3",
1647
+ tag: "companion-v0.1.3",
1648
+ repo: "alvinunreal/oh-my-opencode-slim",
1649
+ checksums: {
1650
+ "oh-my-opencode-slim-companion-v0.1.3-aarch64-apple-darwin.tar.gz": "b4885f9b1900c02376e5f8f5ae6f3b8a89d26f7514b03f836d7e3d618164a0ed",
1651
+ "oh-my-opencode-slim-companion-v0.1.3-aarch64-unknown-linux-gnu.tar.gz": "ed7cffc583e1eaa78c9bea702e6b6aa3bbc5bb4d881713fb2050237ba6b7aca5",
1652
+ "oh-my-opencode-slim-companion-v0.1.3-x86_64-apple-darwin.tar.gz": "98d8ea7c7bc4415b18e0d4c524adb4eb9a84c872919840fdc021f0f50c61f808",
1653
+ "oh-my-opencode-slim-companion-v0.1.3-x86_64-pc-windows-msvc.zip": "9316a49bf01f3b4fb1ce2d62edfc46094e73bb153d6ce023fb7df085afcf77bd",
1654
+ "oh-my-opencode-slim-companion-v0.1.3-x86_64-unknown-linux-gnu.tar.gz": "33f5fd4b6c80155a019391e5efb13904ca9531ba8dd8c6cba30a161f1b07b764"
1655
+ }
1656
+ };
1612
1657
  function getCompanionTarget() {
1613
1658
  const p = process.platform;
1614
1659
  const a = process.arch;
@@ -1631,47 +1676,109 @@ function getCompanionTarget() {
1631
1676
  function getCompanionBinaryPath() {
1632
1677
  const xdg = process.env.XDG_DATA_HOME?.trim();
1633
1678
  const base = xdg && path2.isAbsolute(xdg) ? xdg : path2.join(homedir4(), ".local", "share");
1634
- return path2.join(base, "opencode", "storage", "oh-my-opencode-slim", "bin", process.platform === "win32" ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion");
1679
+ return path2.join(base, "opencode", "storage", "oh-my-opencode-slim", "bin", platform() === "win32" ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion");
1635
1680
  }
1636
- async function installCompanion(config) {
1681
+ async function ensureCompanionVersion(options) {
1682
+ const { config, dryRun = false } = options;
1683
+ const manifest = options.manifest ?? COMPANION_MANIFEST;
1684
+ const binaryPath = getCompanionBinaryPath();
1685
+ if (config?.enabled !== true) {
1686
+ return { status: "skipped", reason: "disabled", binaryPath };
1687
+ }
1688
+ if (config.binaryPath?.trim()) {
1689
+ return { status: "skipped", reason: "custom-binary", binaryPath };
1690
+ }
1637
1691
  const target = getCompanionTarget();
1638
- const finalBinaryPath = getCompanionBinaryPath();
1639
1692
  if (!target) {
1640
1693
  return {
1641
- success: false,
1642
- configPath: finalBinaryPath,
1694
+ status: "failed",
1695
+ binaryPath,
1643
1696
  error: `Unsupported platform/architecture: ${process.platform} ${process.arch}`
1644
1697
  };
1645
1698
  }
1699
+ const current = readInstallMetadata(binaryPath);
1700
+ if (existsSync6(binaryPath) && !current && manifest.version === FIRST_METADATA_VERSION) {
1701
+ const archiveName = companionArchiveName(manifest.version, target);
1702
+ writeInstallMetadata(binaryPath, {
1703
+ version: manifest.version,
1704
+ tag: manifest.tag,
1705
+ target,
1706
+ installedAt: new Date().toISOString(),
1707
+ archiveName,
1708
+ checksum: manifest.checksums?.[archiveName]
1709
+ });
1710
+ return { status: "current", binaryPath, version: manifest.version };
1711
+ }
1712
+ if (existsSync6(binaryPath) && current?.target === target && compareSemver(current.version, manifest.version) >= 0) {
1713
+ return { status: "current", binaryPath, version: current.version };
1714
+ }
1715
+ if (dryRun) {
1716
+ return { status: "installed", binaryPath, version: manifest.version };
1717
+ }
1718
+ return withCompanionInstallLock(binaryPath, options.lockTimeoutMs, options.lockStaleMs, async () => {
1719
+ const lockedCurrent = readInstallMetadata(binaryPath);
1720
+ if (existsSync6(binaryPath) && !lockedCurrent && manifest.version === FIRST_METADATA_VERSION) {
1721
+ const archiveName = companionArchiveName(manifest.version, target);
1722
+ writeInstallMetadata(binaryPath, {
1723
+ version: manifest.version,
1724
+ tag: manifest.tag,
1725
+ target,
1726
+ installedAt: new Date().toISOString(),
1727
+ archiveName,
1728
+ checksum: manifest.checksums?.[archiveName]
1729
+ });
1730
+ return { status: "current", binaryPath, version: manifest.version };
1731
+ }
1732
+ if (existsSync6(binaryPath) && lockedCurrent?.target === target && compareSemver(lockedCurrent.version, manifest.version) >= 0) {
1733
+ return {
1734
+ status: "current",
1735
+ binaryPath,
1736
+ version: lockedCurrent.version
1737
+ };
1738
+ }
1739
+ return installCompanionArchive(binaryPath, target, manifest, options.downloadTimeoutMs ?? DOWNLOAD_TIMEOUT_MS);
1740
+ });
1741
+ }
1742
+ async function installCompanionArchive(finalBinaryPath, target, manifest, downloadTimeoutMs) {
1646
1743
  const isWindows = process.platform === "win32";
1647
- const ext = isWindows ? "zip" : "tar.gz";
1648
- const archiveName = `oh-my-opencode-slim-companion-v${COMPANION_VERSION}-${target}.${ext}`;
1649
- const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/${COMPANION_TAG}/${archiveName}`;
1650
- if (config.dryRun) {
1651
- console.log(` [dry-run] Detected companion target: ${target}`);
1652
- console.log(` [dry-run] Would download archive: ${downloadUrl}`);
1653
- console.log(` [dry-run] Would extract and install to: ${finalBinaryPath}`);
1744
+ const archiveName = companionArchiveName(manifest.version, target, isWindows);
1745
+ const downloadUrl = `https://github.com/${manifest.repo}/releases/download/${manifest.tag}/${archiveName}`;
1746
+ const expectedChecksum = manifest.checksums?.[archiveName];
1747
+ if (!expectedChecksum) {
1654
1748
  return {
1655
- success: true,
1656
- configPath: finalBinaryPath
1749
+ status: "failed",
1750
+ binaryPath: finalBinaryPath,
1751
+ error: `Missing SHA256 checksum for companion archive: ${archiveName}`
1657
1752
  };
1658
1753
  }
1659
1754
  let buffer;
1755
+ const controller = new AbortController;
1756
+ const timeout = setTimeout(() => controller.abort(), downloadTimeoutMs);
1660
1757
  try {
1661
- const res = await fetch(downloadUrl);
1758
+ const res = await fetch(downloadUrl, { signal: controller.signal });
1662
1759
  if (!res.ok) {
1663
1760
  return {
1664
- success: false,
1665
- configPath: finalBinaryPath,
1761
+ status: "failed",
1762
+ binaryPath: finalBinaryPath,
1666
1763
  error: `Failed to download companion binary (HTTP ${res.status}): ${res.statusText}`
1667
1764
  };
1668
1765
  }
1669
1766
  buffer = await res.arrayBuffer();
1670
1767
  } catch (err) {
1671
1768
  return {
1672
- success: false,
1673
- configPath: finalBinaryPath,
1674
- error: `Failed to fetch companion archive: ${err instanceof Error ? err.message : String(err)}`
1769
+ status: "failed",
1770
+ binaryPath: finalBinaryPath,
1771
+ error: `Failed to fetch companion archive: ${formatError(err)}`
1772
+ };
1773
+ } finally {
1774
+ clearTimeout(timeout);
1775
+ }
1776
+ const checksum = createHash("sha256").update(Buffer.from(buffer)).digest("hex");
1777
+ if (checksum !== expectedChecksum) {
1778
+ return {
1779
+ status: "failed",
1780
+ binaryPath: finalBinaryPath,
1781
+ error: "Companion archive checksum mismatch"
1675
1782
  };
1676
1783
  }
1677
1784
  let tempDir = "";
@@ -1685,14 +1792,13 @@ async function installCompanion(config) {
1685
1792
  const { extractZip: extractZip2 } = await Promise.resolve().then(() => (init_zip_extractor(), exports_zip_extractor));
1686
1793
  await extractZip2(archivePath, extractedDir);
1687
1794
  } else {
1688
- const { crossSpawn: crossSpawn2 } = await Promise.resolve().then(() => (init_compat(), exports_compat));
1689
- const proc = crossSpawn2(["tar", "-xzf", archivePath, "-C", extractedDir]);
1795
+ const proc = crossSpawn(["tar", "-xzf", archivePath, "-C", extractedDir]);
1690
1796
  const exitCode = await proc.exited;
1691
1797
  if (exitCode !== 0) {
1692
1798
  const stderr = await proc.stderr();
1693
1799
  return {
1694
- success: false,
1695
- configPath: finalBinaryPath,
1800
+ status: "failed",
1801
+ binaryPath: finalBinaryPath,
1696
1802
  error: `Archive extraction failed (tar exited with ${exitCode}): ${stderr}`
1697
1803
  };
1698
1804
  }
@@ -1701,8 +1807,8 @@ async function installCompanion(config) {
1701
1807
  const extractedBinaryPath = path2.join(extractedDir, binaryName);
1702
1808
  if (!existsSync6(extractedBinaryPath)) {
1703
1809
  return {
1704
- success: false,
1705
- configPath: finalBinaryPath,
1810
+ status: "failed",
1811
+ binaryPath: finalBinaryPath,
1706
1812
  error: `Binary ${binaryName} not found in extracted archive`
1707
1813
  };
1708
1814
  }
@@ -1714,15 +1820,24 @@ async function installCompanion(config) {
1714
1820
  chmodSync(tmpFinalPath, 493);
1715
1821
  }
1716
1822
  renameSync2(tmpFinalPath, finalBinaryPath);
1823
+ writeInstallMetadata(finalBinaryPath, {
1824
+ version: manifest.version,
1825
+ tag: manifest.tag,
1826
+ target,
1827
+ installedAt: new Date().toISOString(),
1828
+ archiveName,
1829
+ checksum
1830
+ });
1717
1831
  return {
1718
- success: true,
1719
- configPath: finalBinaryPath
1832
+ status: "installed",
1833
+ binaryPath: finalBinaryPath,
1834
+ version: manifest.version
1720
1835
  };
1721
1836
  } catch (err) {
1722
1837
  return {
1723
- success: false,
1724
- configPath: finalBinaryPath,
1725
- error: `Failed to install companion: ${err instanceof Error ? err.message : String(err)}`
1838
+ status: "failed",
1839
+ binaryPath: finalBinaryPath,
1840
+ error: `Failed to install companion: ${formatError(err)}`
1726
1841
  };
1727
1842
  } finally {
1728
1843
  if (tempDir) {
@@ -1732,10 +1847,131 @@ async function installCompanion(config) {
1732
1847
  }
1733
1848
  }
1734
1849
  }
1850
+ function readInstallMetadata(binaryPath) {
1851
+ try {
1852
+ const parsed = JSON.parse(readFileSync5(metadataPath(binaryPath), "utf8"));
1853
+ if (parsed?.version && parsed.tag && parsed.target) {
1854
+ return parsed;
1855
+ }
1856
+ } catch {}
1857
+ return null;
1858
+ }
1859
+ function writeInstallMetadata(binaryPath, metadata) {
1860
+ writeFileSync3(metadataPath(binaryPath), JSON.stringify(metadata, null, 2));
1861
+ }
1862
+ function metadataPath(binaryPath) {
1863
+ return `${binaryPath}.json`;
1864
+ }
1865
+ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
1866
+ const lock = `${binaryPath}.lock`;
1867
+ const deadline = Date.now() + (timeoutMs ?? LOCK_TIMEOUT_MS);
1868
+ const staleAfterMs = staleMs ?? STALE_LOCK_MS;
1869
+ mkdirSync5(path2.dirname(binaryPath), { recursive: true });
1870
+ while (Date.now() <= deadline) {
1871
+ try {
1872
+ mkdirSync5(lock);
1873
+ try {
1874
+ return await run();
1875
+ } finally {
1876
+ try {
1877
+ rmSync2(lock, { recursive: true, force: true });
1878
+ } catch {}
1879
+ }
1880
+ } catch (err) {
1881
+ const code = err.code;
1882
+ if (code !== "EEXIST")
1883
+ throw err;
1884
+ try {
1885
+ const ageMs = Date.now() - statSync4(lock).mtimeMs;
1886
+ if (ageMs > staleAfterMs) {
1887
+ rmSync2(lock, { recursive: true, force: true });
1888
+ log("[companion] removed stale install lock", lock);
1889
+ continue;
1890
+ }
1891
+ } catch (statErr) {
1892
+ const statCode = statErr.code;
1893
+ if (statCode !== "ENOENT")
1894
+ throw statErr;
1895
+ }
1896
+ await delay(25);
1897
+ }
1898
+ }
1899
+ log("[companion] install lock timed out", lock);
1900
+ return {
1901
+ status: "failed",
1902
+ binaryPath,
1903
+ error: "Timed out waiting for companion install lock"
1904
+ };
1905
+ }
1906
+ function companionArchiveName(version, target, isWindows = process.platform === "win32") {
1907
+ const ext = isWindows ? "zip" : "tar.gz";
1908
+ return `oh-my-opencode-slim-companion-v${version}-${target}.${ext}`;
1909
+ }
1910
+ function compareSemver(a, b) {
1911
+ const left = parseSemver(a);
1912
+ const right = parseSemver(b);
1913
+ if (!left || !right)
1914
+ return a.localeCompare(b);
1915
+ for (let i = 0;i < 3; i++) {
1916
+ const diff = left[i] - right[i];
1917
+ if (diff !== 0)
1918
+ return diff;
1919
+ }
1920
+ return 0;
1921
+ }
1922
+ function parseSemver(version) {
1923
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
1924
+ if (!match)
1925
+ return null;
1926
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
1927
+ }
1928
+ function formatError(err) {
1929
+ return err instanceof Error ? err.message : String(err);
1930
+ }
1931
+
1932
+ // src/cli/companion.ts
1933
+ async function installCompanion(config) {
1934
+ const target = getCompanionTarget();
1935
+ const finalBinaryPath = getCompanionBinaryPath();
1936
+ if (!target) {
1937
+ return {
1938
+ success: false,
1939
+ configPath: finalBinaryPath,
1940
+ error: `Unsupported platform/architecture: ${process.platform} ${process.arch}`
1941
+ };
1942
+ }
1943
+ const ext = process.platform === "win32" ? "zip" : "tar.gz";
1944
+ const archiveName = `oh-my-opencode-slim-companion-v${COMPANION_MANIFEST.version}-${target}.${ext}`;
1945
+ const downloadUrl = `https://github.com/${COMPANION_MANIFEST.repo}/releases/download/${COMPANION_MANIFEST.tag}/${archiveName}`;
1946
+ if (config.dryRun) {
1947
+ console.log(` [dry-run] Detected companion target: ${target}`);
1948
+ console.log(` [dry-run] Would download archive: ${downloadUrl}`);
1949
+ console.log(` [dry-run] Would extract and install to: ${finalBinaryPath}`);
1950
+ return {
1951
+ success: true,
1952
+ configPath: finalBinaryPath
1953
+ };
1954
+ }
1955
+ const result = await ensureCompanionVersion({
1956
+ config: { enabled: true },
1957
+ manifest: COMPANION_MANIFEST
1958
+ });
1959
+ if (result.status === "installed" || result.status === "current") {
1960
+ return {
1961
+ success: true,
1962
+ configPath: finalBinaryPath
1963
+ };
1964
+ }
1965
+ return {
1966
+ success: false,
1967
+ configPath: finalBinaryPath,
1968
+ error: result.status === "failed" ? result.error : `Companion install skipped: ${result.reason}`
1969
+ };
1970
+ }
1735
1971
  // src/cli/system.ts
1736
1972
  init_compat();
1737
1973
  import { spawnSync as spawnSync2 } from "node:child_process";
1738
- import { statSync as statSync4 } from "node:fs";
1974
+ import { statSync as statSync5 } from "node:fs";
1739
1975
  var cachedOpenCodePath = null;
1740
1976
  function resolvePathCommand(command) {
1741
1977
  try {
@@ -1808,7 +2044,7 @@ function resolveOpenCodePath() {
1808
2044
  if (opencodePath === "opencode")
1809
2045
  continue;
1810
2046
  try {
1811
- const stat = statSync4(opencodePath);
2047
+ const stat = statSync5(opencodePath);
1812
2048
  if (stat.isFile()) {
1813
2049
  cachedOpenCodePath = opencodePath;
1814
2050
  return opencodePath;
@@ -1877,8 +2113,8 @@ var SYMBOLS = {
1877
2113
  warn: `${YELLOW}[!]${RESET}`,
1878
2114
  star: `${YELLOW}★${RESET}`
1879
2115
  };
1880
- var GITHUB_REPO2 = "alvinunreal/oh-my-opencode-slim";
1881
- var GITHUB_URL = `https://github.com/${GITHUB_REPO2}`;
2116
+ var GITHUB_REPO = "alvinunreal/oh-my-opencode-slim";
2117
+ var GITHUB_URL = `https://github.com/${GITHUB_REPO}`;
1882
2118
  function printHeader(isUpdate) {
1883
2119
  console.log();
1884
2120
  console.log(`${BOLD}oh-my-opencode-slim ${isUpdate ? "Update" : "Install"}${RESET}`);
@@ -1921,7 +2157,7 @@ async function askToStarRepo(config) {
1921
2157
  return;
1922
2158
  try {
1923
2159
  const { execFileSync } = await import("node:child_process");
1924
- execFileSync("gh", ["api", "--silent", "--method", "PUT", `/user/starred/${GITHUB_REPO2}`], { stdio: "ignore", timeout: 1e4 });
2160
+ execFileSync("gh", ["api", "--silent", "--method", "PUT", `/user/starred/${GITHUB_REPO}`], { stdio: "ignore", timeout: 1e4 });
1925
2161
  printSuccess("Thanks for starring! ★");
1926
2162
  } catch {
1927
2163
  printInfo(`Couldn't star automatically. You can star manually:
@@ -2001,8 +2237,9 @@ ${block}
2001
2237
  return { enabledNow: false, configuredTarget: target };
2002
2238
  }
2003
2239
  async function shouldInstallCompanion(config) {
2004
- if (config.companion === "yes")
2240
+ if (config.companion === "yes") {
2005
2241
  return true;
2242
+ }
2006
2243
  if (config.companion === "no")
2007
2244
  return false;
2008
2245
  if (config.dryRun) {
@@ -1,5 +1,6 @@
1
1
  import type { CompanionConfig } from '../config/schema';
2
2
  export declare function stateFilePath(): string;
3
+ export declare function resolveCompanionBinaryPath(config?: CompanionConfig): string | null;
3
4
  /**
4
5
  * Tracks live agent activity per session and mirrors it to the companion
5
6
  * state file. Source of truth is OpenCode's session.status events: every
@@ -0,0 +1,36 @@
1
+ import type { CompanionConfig } from '../config/schema';
2
+ export interface CompanionManifest {
3
+ version: string;
4
+ tag: string;
5
+ repo: string;
6
+ checksums?: Record<string, string>;
7
+ }
8
+ export type CompanionUpdateResult = {
9
+ status: 'installed';
10
+ binaryPath: string;
11
+ version: string;
12
+ } | {
13
+ status: 'current';
14
+ binaryPath: string;
15
+ version: string;
16
+ } | {
17
+ status: 'skipped';
18
+ reason: string;
19
+ binaryPath?: string;
20
+ } | {
21
+ status: 'failed';
22
+ error: string;
23
+ binaryPath: string;
24
+ };
25
+ export declare const COMPANION_MANIFEST: CompanionManifest;
26
+ export declare function getCompanionTarget(): string | null;
27
+ export declare function getCompanionBinaryPath(): string;
28
+ export declare function loadCompanionManifestFromPackageRoot(packageRoot: string): CompanionManifest | null;
29
+ export declare function ensureCompanionVersion(options: {
30
+ config?: CompanionConfig;
31
+ manifest?: CompanionManifest;
32
+ dryRun?: boolean;
33
+ downloadTimeoutMs?: number;
34
+ lockTimeoutMs?: number;
35
+ lockStaleMs?: number;
36
+ }): Promise<CompanionUpdateResult>;
@@ -5,10 +5,6 @@ export declare const DEFAULT_AGENT_MCPS: Record<AgentName, string[]>;
5
5
  * Parse a list with wildcard and exclusion syntax.
6
6
  */
7
7
  export declare function parseList(items: string[], allAvailable: string[]): string[];
8
- /**
9
- * Get available MCP names from schema and config.
10
- */
11
- export declare function getAvailableMcpNames(config?: PluginConfig): string[];
12
8
  /**
13
9
  * Get the MCP list for an agent (from config or defaults).
14
10
  */