oh-my-opencode-slim 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja-JP.md CHANGED
@@ -595,6 +595,7 @@ Worktrees は、Git worktree を `.slim/worktrees/<slug>/` 配下の安全で隔
595
595
  |-----|----------------|
596
596
  | **[Council](docs/council.md)** | 複数のモデルを並列実行し、`@council` で 1 つの回答に統合します |
597
597
  | **[Custom Agents](docs/configuration.md#custom-agents)** | カスタムプロンプト、モデル、MCP アクセス、Orchestrator の委譲ルールを備えた独自の専門エージェントを定義します |
598
+ | **[ACP Agents](docs/acp-agents.md)** | Claude Code ACP や Gemini ACP などの外部 ACP 互換エージェントを委譲可能なサブエージェントとして接続します |
598
599
  | **[Multiplexer Integration](docs/multiplexer-integration.md)** | エージェントの動作を Tmux や Zellij のペインでライブ表示します |
599
600
  | **[Codemap](docs/codemap.md)** | 階層的なコードマップを生成し、大規模コードベースを迅速に理解します |
600
601
  | **[Clonedeps](docs/clonedeps.md)** | 選択した依存関係のソースを ignore 済みのローカルワークスペースにクローンし、調査できるようにします |
package/README.ko-KR.md CHANGED
@@ -593,6 +593,7 @@ Worktrees는 Git worktree를 `.slim/worktrees/<slug>/` 아래의 안전하고
593
593
  |-----|------|
594
594
  | **[Council](docs/council.md)** | `@council`로 여러 모델을 병렬 실행하고 하나의 답변으로 종합 |
595
595
  | **[Custom Agents](docs/configuration.md#custom-agents)** | 커스텀 프롬프트, 모델, MCP 접근, Orchestrator 위임 규칙으로 커스텀 전문 에이전트 정의 |
596
+ | **[ACP Agents](docs/acp-agents.md)** | Claude Code ACP 또는 Gemini ACP 같은 외부 ACP 호환 에이전트를 위임 가능한 서브에이전트로 연결 |
596
597
  | **[Multiplexer Integration](docs/multiplexer-integration.md)** | Tmux 또는 Zellij 페인에서 에이전트 작업을 실시간으로 확인 |
597
598
  | **[Codemap](docs/codemap.md)** | 계층형 코드맵을 생성하여 대규모 코드베이스를 빠르게 파악 |
598
599
  | **[Clonedeps](docs/clonedeps.md)** | 선택한 의존성 소스를 무시된 로컬 워크스페이스에 복제하여 검사 |
package/README.md CHANGED
@@ -619,6 +619,7 @@ Use this section as a map: start with installation, then jump to features, confi
619
619
  |-----|----------------|
620
620
  | **[Council](docs/council.md)** | Run multiple models in parallel and synthesize a single answer with `@council` |
621
621
  | **[Custom Agents](docs/configuration.md#custom-agents)** | Define your own specialists with custom prompts, models, MCP access, and Orchestrator delegation rules |
622
+ | **[ACP Agents](docs/acp-agents.md)** | Connect external ACP-compatible agents such as Claude Code ACP or Gemini ACP as delegatable subagents |
622
623
  | **[Multiplexer Integration](docs/multiplexer-integration.md)** | Watch agents work live in Tmux or Zellij panes |
623
624
  | **[Codemap](docs/codemap.md)** | Generate hierarchical codemaps to understand large codebases faster |
624
625
  | **[Clonedeps](docs/clonedeps.md)** | Clone selected dependency source into an ignored local workspace for inspection |
@@ -656,7 +657,7 @@ Use this section as a map: start with installation, then jump to features, confi
656
657
  <p><sub>Every merged contribution leaves a mark on the realm.</sub></p>
657
658
 
658
659
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
659
- [![All Contributors](https://img.shields.io/badge/all_contributors-60-orange.svg?style=flat-square)](#contributors-)
660
+ [![All Contributors](https://img.shields.io/badge/all_contributors-63-orange.svg?style=flat-square)](#contributors-)
660
661
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
661
662
  </div>
662
663
 
@@ -747,6 +748,11 @@ Use this section as a map: start with installation, then jump to features, confi
747
748
  <td align="center" valign="top" width="16.66%"><a href="https://github.com/smatheusblu"><img src="https://avatars.githubusercontent.com/u/5666794?v=4?s=100" width="100px;" alt="Matheus Nogueira Silveira"/><br /><sub><b>Matheus Nogueira Silveira</b></sub></a><br /><a href="https://github.com/alvinunreal/oh-my-opencode-slim/commits?author=smatheusblu" title="Code">💻</a></td>
748
749
  <td align="center" valign="top" width="16.66%"><a href="https://github.com/sktr"><img src="https://avatars.githubusercontent.com/u/44969514?v=4?s=100" width="100px;" alt="sktr"/><br /><sub><b>sktr</b></sub></a><br /><a href="https://github.com/alvinunreal/oh-my-opencode-slim/commits?author=sktr" title="Code">💻</a></td>
749
750
  </tr>
751
+ <tr>
752
+ <td align="center" valign="top" width="16.66%"><a href="https://github.com/bobbyunknown"><img src="https://avatars.githubusercontent.com/u/62272380?v=4?s=100" width="100px;" alt="Insomnia"/><br /><sub><b>Insomnia</b></sub></a><br /><a href="https://github.com/alvinunreal/oh-my-opencode-slim/commits?author=bobbyunknown" title="Code">💻</a></td>
753
+ <td align="center" valign="top" width="16.66%"><a href="https://github.com/andrescastane"><img src="https://avatars.githubusercontent.com/u/13487870?v=4?s=100" width="100px;" alt="Andres Castañeda"/><br /><sub><b>Andres Castañeda</b></sub></a><br /><a href="https://github.com/alvinunreal/oh-my-opencode-slim/commits?author=andrescastane" title="Code">💻</a></td>
754
+ <td align="center" valign="top" width="16.66%"><a href="https://zaradacht.com/"><img src="https://avatars.githubusercontent.com/u/24251016?v=4?s=100" width="100px;" alt="Zaradacht Taifour (Zack)"/><br /><sub><b>Zaradacht Taifour (Zack)</b></sub></a><br /><a href="https://github.com/alvinunreal/oh-my-opencode-slim/commits?author=Zaradacht" title="Code">💻</a></td>
755
+ </tr>
750
756
  </tbody>
751
757
  </table>
752
758
 
package/README.zh-CN.md CHANGED
@@ -590,6 +590,7 @@ Worktrees 将 Git worktree 作为安全、隔离的编码通道管理,默认
590
590
  |-----|----------------|
591
591
  | **[Council](docs/council.md)** | 使用 `@council` 并行运行多个模型并合成单一答案 |
592
592
  | **[自定义智能体](docs/configuration.md#custom-agents)** | 使用自定义提示词、模型、MCP 访问和 Orchestrator 委派规则定义自己的专家 |
593
+ | **[ACP Agents](docs/acp-agents.md)** | 将 Claude Code ACP 或 Gemini ACP 等外部 ACP 兼容智能体连接为可委派子智能体 |
593
594
  | **[多路复用器集成](docs/multiplexer-integration.md)** | 在 Tmux 或 Zellij 窗格中实时观看智能体工作 |
594
595
  | **[Codemap](docs/codemap.md)** | 生成层级代码地图,更快理解大型代码库 |
595
596
  | **[Clonedeps](docs/clonedeps.md)** | 将选定的依赖源码克隆到被忽略的本地工作区中以供检查 |
@@ -1,4 +1,4 @@
1
+ import { getCompanionBinaryPath, getCompanionTarget } from '../companion/updater';
1
2
  import type { ConfigMergeResult, InstallConfig } from './types';
2
- export declare function getCompanionTarget(): string | null;
3
- export declare function getCompanionBinaryPath(): string;
3
+ export { getCompanionBinaryPath, getCompanionTarget };
4
4
  export declare function installCompanion(config: InstallConfig): Promise<ConfigMergeResult>;
package/dist/cli/index.js CHANGED
@@ -19,13 +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
- crossWrite: () => crossWrite,
25
- crossSpawn: () => crossSpawn
26
- });
27
22
  import { spawn as nodeSpawn } from "node:child_process";
28
- import { writeFile as fsWriteFile } from "node:fs/promises";
29
23
  function collectStream(stream) {
30
24
  if (!stream)
31
25
  return () => Promise.resolve("");
@@ -68,9 +62,6 @@ function crossSpawn(command, options) {
68
62
  }
69
63
  };
70
64
  }
71
- async function crossWrite(path, data) {
72
- await fsWriteFile(path, Buffer.from(data));
73
- }
74
65
  var init_compat = () => {};
75
66
 
76
67
  // src/utils/zip-extractor.ts
@@ -465,9 +456,28 @@ var FailoverConfigSchema = z2.object({
465
456
  }).strict();
466
457
  var CompanionConfigSchema = z2.object({
467
458
  enabled: z2.boolean().optional(),
459
+ binaryPath: z2.string().min(1).optional().describe("Path to a custom companion binary to launch."),
468
460
  position: z2.enum(["bottom-right", "bottom-left", "top-right", "top-left"]).optional(),
469
- 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.")
470
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);
471
481
  function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
472
482
  for (const [name, override] of Object.entries(overrides)) {
473
483
  const isBuiltInOrAlias = ALL_AGENT_NAMES.includes(name) || AGENT_ALIASES[name] !== undefined;
@@ -505,7 +515,8 @@ var PluginConfigSchema = z2.object({
505
515
  backgroundJobs: BackgroundJobsConfigSchema.optional(),
506
516
  fallback: FailoverConfigSchema.optional(),
507
517
  council: CouncilConfigSchema.optional(),
508
- companion: CompanionConfigSchema.optional()
518
+ companion: CompanionConfigSchema.optional(),
519
+ acpAgents: AcpAgentsConfigSchema.optional()
509
520
  }).superRefine((value, ctx) => {
510
521
  if (value.agents) {
511
522
  validateCustomOnlyPromptFields(value.agents, ctx, ["agents"]);
@@ -726,7 +737,11 @@ function generateLiteConfig(installConfig) {
726
737
  config.companion = {
727
738
  enabled: true,
728
739
  position: "bottom-right",
729
- size: "medium"
740
+ size: "medium",
741
+ gifPack: "default",
742
+ loopStyle: "classic",
743
+ speed: 1,
744
+ debug: false
730
745
  };
731
746
  }
732
747
  return config;
@@ -734,15 +749,24 @@ function generateLiteConfig(installConfig) {
734
749
 
735
750
  // src/cli/config-io.ts
736
751
  var PACKAGE_NAME = "oh-my-opencode-slim";
737
- var DEFAULT_OPENCODE_AGENTS_TO_DISABLE = [
738
- "build",
739
- "explore",
740
- "general",
741
- "plan"
742
- ];
752
+ var DEFAULT_OPENCODE_AGENTS_TO_DISABLE = ["explore", "general"];
743
753
  function isString(value) {
744
754
  return typeof value === "string";
745
755
  }
756
+ function getModelIds(model) {
757
+ if (isString(model))
758
+ return [model];
759
+ if (!Array.isArray(model))
760
+ return [];
761
+ return model.flatMap((entry) => {
762
+ if (isString(entry))
763
+ return [entry];
764
+ if (entry && typeof entry === "object" && isString(entry.id)) {
765
+ return [entry.id];
766
+ }
767
+ return [];
768
+ });
769
+ }
746
770
  function getPlugins(config) {
747
771
  return Array.isArray(config.plugin) ? config.plugin : [];
748
772
  }
@@ -1207,17 +1231,17 @@ function detectCurrentConfig() {
1207
1231
  const presetName = configObj.preset;
1208
1232
  const presets = configObj.presets;
1209
1233
  const agents = presets?.[presetName];
1210
- if (agents) {
1211
- const models = Object.values(agents).map((a) => a?.model).filter(Boolean);
1212
- result.hasOpenAI = models.some((m) => m?.startsWith("openai/"));
1213
- result.hasAnthropic = models.some((m) => m?.startsWith("anthropic/"));
1214
- result.hasCopilot = models.some((m) => m?.startsWith("github-copilot/"));
1215
- result.hasZaiPlan = models.some((m) => m?.startsWith("zai-coding-plan/"));
1216
- result.hasOpencodeZen = models.some((m) => m?.startsWith("opencode/"));
1217
- if (models.some((m) => m?.startsWith("google/"))) {
1234
+ if (agents && typeof agents === "object") {
1235
+ const models = Object.values(agents).filter((a) => a && typeof a === "object").flatMap((a) => getModelIds(a.model));
1236
+ result.hasOpenAI ||= models.some((m) => m.startsWith("openai/"));
1237
+ result.hasAnthropic ||= models.some((m) => m.startsWith("anthropic/"));
1238
+ result.hasCopilot ||= models.some((m) => m.startsWith("github-copilot/"));
1239
+ result.hasZaiPlan ||= models.some((m) => m.startsWith("zai-coding-plan/"));
1240
+ result.hasOpencodeZen ||= models.some((m) => m.startsWith("opencode/"));
1241
+ if (models.some((m) => m.startsWith("google/"))) {
1218
1242
  result.hasAntigravity = true;
1219
1243
  }
1220
- if (models.some((m) => m?.startsWith("chutes/"))) {
1244
+ if (models.some((m) => m.startsWith("chutes/"))) {
1221
1245
  result.hasChutes = true;
1222
1246
  }
1223
1247
  }
@@ -1267,6 +1291,7 @@ function mergePluginConfigs(base, override) {
1267
1291
  backgroundJobs: deepMerge(base.backgroundJobs, override.backgroundJobs),
1268
1292
  fallback: deepMerge(base.fallback, override.fallback),
1269
1293
  council: deepMerge(base.council, override.council),
1294
+ acpAgents: deepMerge(base.acpAgents, override.acpAgents),
1270
1295
  companion: deepMerge(base.companion, override.companion)
1271
1296
  };
1272
1297
  }
@@ -1582,28 +1607,75 @@ function writeBackgroundSubagentsBlock(targetPath) {
1582
1607
  writeFileSync2(targetPath, nextContent);
1583
1608
  }
1584
1609
 
1585
- // src/cli/companion.ts
1610
+ // src/companion/updater.ts
1611
+ init_compat();
1612
+ import { createHash } from "node:crypto";
1586
1613
  import {
1587
1614
  chmodSync,
1588
1615
  copyFileSync as copyFileSync3,
1589
1616
  existsSync as existsSync6,
1590
1617
  mkdirSync as mkdirSync5,
1591
1618
  mkdtempSync,
1619
+ readFileSync as readFileSync5,
1592
1620
  renameSync as renameSync2,
1593
1621
  rmSync as rmSync2,
1622
+ statSync as statSync4,
1594
1623
  writeFileSync as writeFileSync3
1595
1624
  } from "node:fs";
1596
- import { homedir as homedir4, tmpdir } from "node:os";
1625
+ import { homedir as homedir4, platform, tmpdir } from "node:os";
1597
1626
  import * as path2 from "node:path";
1598
- var COMPANION_VERSION = "0.1.2";
1599
- var COMPANION_TAG = "companion-v0.1.2";
1600
- var GITHUB_REPO = "alvinunreal/oh-my-opencode-slim";
1627
+ import { setTimeout as delay } from "node:timers/promises";
1628
+
1629
+ // src/utils/logger.ts
1630
+ import { appendFile } from "node:fs/promises";
1631
+ var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
1632
+ var logFile = null;
1633
+ var writeChain = Promise.resolve();
1634
+ function log(message, data) {
1635
+ const target = logFile;
1636
+ if (!target)
1637
+ return;
1638
+ try {
1639
+ const timestamp = new Date().toISOString();
1640
+ let dataStr = "";
1641
+ if (data !== undefined) {
1642
+ try {
1643
+ dataStr = JSON.stringify(data);
1644
+ } catch {
1645
+ dataStr = "[unserializable]";
1646
+ }
1647
+ }
1648
+ const logEntry = `[${timestamp}] ${message} ${dataStr}
1649
+ `;
1650
+ writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
1651
+ } catch {}
1652
+ }
1653
+
1654
+ // src/companion/updater.ts
1655
+ var DOWNLOAD_TIMEOUT_MS = 30000;
1656
+ var LOCK_TIMEOUT_MS = 2000;
1657
+ var STALE_LOCK_MS = 5 * 60000;
1658
+ var FIRST_METADATA_VERSION = "0.1.2";
1659
+ var COMPANION_MANIFEST = {
1660
+ version: "0.1.3",
1661
+ tag: "companion-v0.1.3",
1662
+ repo: "alvinunreal/oh-my-opencode-slim",
1663
+ checksums: {
1664
+ "oh-my-opencode-slim-companion-v0.1.3-aarch64-apple-darwin.tar.gz": "b4885f9b1900c02376e5f8f5ae6f3b8a89d26f7514b03f836d7e3d618164a0ed",
1665
+ "oh-my-opencode-slim-companion-v0.1.3-aarch64-unknown-linux-gnu.tar.gz": "ed7cffc583e1eaa78c9bea702e6b6aa3bbc5bb4d881713fb2050237ba6b7aca5",
1666
+ "oh-my-opencode-slim-companion-v0.1.3-x86_64-apple-darwin.tar.gz": "98d8ea7c7bc4415b18e0d4c524adb4eb9a84c872919840fdc021f0f50c61f808",
1667
+ "oh-my-opencode-slim-companion-v0.1.3-x86_64-pc-windows-msvc.zip": "9316a49bf01f3b4fb1ce2d62edfc46094e73bb153d6ce023fb7df085afcf77bd",
1668
+ "oh-my-opencode-slim-companion-v0.1.3-x86_64-unknown-linux-gnu.tar.gz": "33f5fd4b6c80155a019391e5efb13904ca9531ba8dd8c6cba30a161f1b07b764"
1669
+ }
1670
+ };
1601
1671
  function getCompanionTarget() {
1602
1672
  const p = process.platform;
1603
1673
  const a = process.arch;
1604
1674
  if (p === "darwin") {
1605
1675
  if (a === "arm64")
1606
1676
  return "aarch64-apple-darwin";
1677
+ if (a === "x64")
1678
+ return "x86_64-apple-darwin";
1607
1679
  } else if (p === "linux") {
1608
1680
  if (a === "x64")
1609
1681
  return "x86_64-unknown-linux-gnu";
@@ -1618,47 +1690,109 @@ function getCompanionTarget() {
1618
1690
  function getCompanionBinaryPath() {
1619
1691
  const xdg = process.env.XDG_DATA_HOME?.trim();
1620
1692
  const base = xdg && path2.isAbsolute(xdg) ? xdg : path2.join(homedir4(), ".local", "share");
1621
- 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");
1693
+ return path2.join(base, "opencode", "storage", "oh-my-opencode-slim", "bin", platform() === "win32" ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion");
1622
1694
  }
1623
- async function installCompanion(config) {
1695
+ async function ensureCompanionVersion(options) {
1696
+ const { config, dryRun = false } = options;
1697
+ const manifest = options.manifest ?? COMPANION_MANIFEST;
1698
+ const binaryPath = getCompanionBinaryPath();
1699
+ if (config?.enabled !== true) {
1700
+ return { status: "skipped", reason: "disabled", binaryPath };
1701
+ }
1702
+ if (config.binaryPath?.trim()) {
1703
+ return { status: "skipped", reason: "custom-binary", binaryPath };
1704
+ }
1624
1705
  const target = getCompanionTarget();
1625
- const finalBinaryPath = getCompanionBinaryPath();
1626
1706
  if (!target) {
1627
1707
  return {
1628
- success: false,
1629
- configPath: finalBinaryPath,
1708
+ status: "failed",
1709
+ binaryPath,
1630
1710
  error: `Unsupported platform/architecture: ${process.platform} ${process.arch}`
1631
1711
  };
1632
1712
  }
1713
+ const current = readInstallMetadata(binaryPath);
1714
+ if (existsSync6(binaryPath) && !current && manifest.version === FIRST_METADATA_VERSION) {
1715
+ const archiveName = companionArchiveName(manifest.version, target);
1716
+ writeInstallMetadata(binaryPath, {
1717
+ version: manifest.version,
1718
+ tag: manifest.tag,
1719
+ target,
1720
+ installedAt: new Date().toISOString(),
1721
+ archiveName,
1722
+ checksum: manifest.checksums?.[archiveName]
1723
+ });
1724
+ return { status: "current", binaryPath, version: manifest.version };
1725
+ }
1726
+ if (existsSync6(binaryPath) && current?.target === target && compareSemver(current.version, manifest.version) >= 0) {
1727
+ return { status: "current", binaryPath, version: current.version };
1728
+ }
1729
+ if (dryRun) {
1730
+ return { status: "installed", binaryPath, version: manifest.version };
1731
+ }
1732
+ return withCompanionInstallLock(binaryPath, options.lockTimeoutMs, options.lockStaleMs, async () => {
1733
+ const lockedCurrent = readInstallMetadata(binaryPath);
1734
+ if (existsSync6(binaryPath) && !lockedCurrent && manifest.version === FIRST_METADATA_VERSION) {
1735
+ const archiveName = companionArchiveName(manifest.version, target);
1736
+ writeInstallMetadata(binaryPath, {
1737
+ version: manifest.version,
1738
+ tag: manifest.tag,
1739
+ target,
1740
+ installedAt: new Date().toISOString(),
1741
+ archiveName,
1742
+ checksum: manifest.checksums?.[archiveName]
1743
+ });
1744
+ return { status: "current", binaryPath, version: manifest.version };
1745
+ }
1746
+ if (existsSync6(binaryPath) && lockedCurrent?.target === target && compareSemver(lockedCurrent.version, manifest.version) >= 0) {
1747
+ return {
1748
+ status: "current",
1749
+ binaryPath,
1750
+ version: lockedCurrent.version
1751
+ };
1752
+ }
1753
+ return installCompanionArchive(binaryPath, target, manifest, options.downloadTimeoutMs ?? DOWNLOAD_TIMEOUT_MS);
1754
+ });
1755
+ }
1756
+ async function installCompanionArchive(finalBinaryPath, target, manifest, downloadTimeoutMs) {
1633
1757
  const isWindows = process.platform === "win32";
1634
- const ext = isWindows ? "zip" : "tar.gz";
1635
- const archiveName = `oh-my-opencode-slim-companion-v${COMPANION_VERSION}-${target}.${ext}`;
1636
- const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/${COMPANION_TAG}/${archiveName}`;
1637
- if (config.dryRun) {
1638
- console.log(` [dry-run] Detected companion target: ${target}`);
1639
- console.log(` [dry-run] Would download archive: ${downloadUrl}`);
1640
- console.log(` [dry-run] Would extract and install to: ${finalBinaryPath}`);
1758
+ const archiveName = companionArchiveName(manifest.version, target, isWindows);
1759
+ const downloadUrl = `https://github.com/${manifest.repo}/releases/download/${manifest.tag}/${archiveName}`;
1760
+ const expectedChecksum = manifest.checksums?.[archiveName];
1761
+ if (!expectedChecksum) {
1641
1762
  return {
1642
- success: true,
1643
- configPath: finalBinaryPath
1763
+ status: "failed",
1764
+ binaryPath: finalBinaryPath,
1765
+ error: `Missing SHA256 checksum for companion archive: ${archiveName}`
1644
1766
  };
1645
1767
  }
1646
1768
  let buffer;
1769
+ const controller = new AbortController;
1770
+ const timeout = setTimeout(() => controller.abort(), downloadTimeoutMs);
1647
1771
  try {
1648
- const res = await fetch(downloadUrl);
1772
+ const res = await fetch(downloadUrl, { signal: controller.signal });
1649
1773
  if (!res.ok) {
1650
1774
  return {
1651
- success: false,
1652
- configPath: finalBinaryPath,
1775
+ status: "failed",
1776
+ binaryPath: finalBinaryPath,
1653
1777
  error: `Failed to download companion binary (HTTP ${res.status}): ${res.statusText}`
1654
1778
  };
1655
1779
  }
1656
1780
  buffer = await res.arrayBuffer();
1657
1781
  } catch (err) {
1658
1782
  return {
1659
- success: false,
1660
- configPath: finalBinaryPath,
1661
- error: `Failed to fetch companion archive: ${err instanceof Error ? err.message : String(err)}`
1783
+ status: "failed",
1784
+ binaryPath: finalBinaryPath,
1785
+ error: `Failed to fetch companion archive: ${formatError(err)}`
1786
+ };
1787
+ } finally {
1788
+ clearTimeout(timeout);
1789
+ }
1790
+ const checksum = createHash("sha256").update(Buffer.from(buffer)).digest("hex");
1791
+ if (checksum !== expectedChecksum) {
1792
+ return {
1793
+ status: "failed",
1794
+ binaryPath: finalBinaryPath,
1795
+ error: "Companion archive checksum mismatch"
1662
1796
  };
1663
1797
  }
1664
1798
  let tempDir = "";
@@ -1672,14 +1806,13 @@ async function installCompanion(config) {
1672
1806
  const { extractZip: extractZip2 } = await Promise.resolve().then(() => (init_zip_extractor(), exports_zip_extractor));
1673
1807
  await extractZip2(archivePath, extractedDir);
1674
1808
  } else {
1675
- const { crossSpawn: crossSpawn2 } = await Promise.resolve().then(() => (init_compat(), exports_compat));
1676
- const proc = crossSpawn2(["tar", "-xzf", archivePath, "-C", extractedDir]);
1809
+ const proc = crossSpawn(["tar", "-xzf", archivePath, "-C", extractedDir]);
1677
1810
  const exitCode = await proc.exited;
1678
1811
  if (exitCode !== 0) {
1679
1812
  const stderr = await proc.stderr();
1680
1813
  return {
1681
- success: false,
1682
- configPath: finalBinaryPath,
1814
+ status: "failed",
1815
+ binaryPath: finalBinaryPath,
1683
1816
  error: `Archive extraction failed (tar exited with ${exitCode}): ${stderr}`
1684
1817
  };
1685
1818
  }
@@ -1688,8 +1821,8 @@ async function installCompanion(config) {
1688
1821
  const extractedBinaryPath = path2.join(extractedDir, binaryName);
1689
1822
  if (!existsSync6(extractedBinaryPath)) {
1690
1823
  return {
1691
- success: false,
1692
- configPath: finalBinaryPath,
1824
+ status: "failed",
1825
+ binaryPath: finalBinaryPath,
1693
1826
  error: `Binary ${binaryName} not found in extracted archive`
1694
1827
  };
1695
1828
  }
@@ -1701,15 +1834,24 @@ async function installCompanion(config) {
1701
1834
  chmodSync(tmpFinalPath, 493);
1702
1835
  }
1703
1836
  renameSync2(tmpFinalPath, finalBinaryPath);
1837
+ writeInstallMetadata(finalBinaryPath, {
1838
+ version: manifest.version,
1839
+ tag: manifest.tag,
1840
+ target,
1841
+ installedAt: new Date().toISOString(),
1842
+ archiveName,
1843
+ checksum
1844
+ });
1704
1845
  return {
1705
- success: true,
1706
- configPath: finalBinaryPath
1846
+ status: "installed",
1847
+ binaryPath: finalBinaryPath,
1848
+ version: manifest.version
1707
1849
  };
1708
1850
  } catch (err) {
1709
1851
  return {
1710
- success: false,
1711
- configPath: finalBinaryPath,
1712
- error: `Failed to install companion: ${err instanceof Error ? err.message : String(err)}`
1852
+ status: "failed",
1853
+ binaryPath: finalBinaryPath,
1854
+ error: `Failed to install companion: ${formatError(err)}`
1713
1855
  };
1714
1856
  } finally {
1715
1857
  if (tempDir) {
@@ -1719,10 +1861,131 @@ async function installCompanion(config) {
1719
1861
  }
1720
1862
  }
1721
1863
  }
1864
+ function readInstallMetadata(binaryPath) {
1865
+ try {
1866
+ const parsed = JSON.parse(readFileSync5(metadataPath(binaryPath), "utf8"));
1867
+ if (parsed?.version && parsed.tag && parsed.target) {
1868
+ return parsed;
1869
+ }
1870
+ } catch {}
1871
+ return null;
1872
+ }
1873
+ function writeInstallMetadata(binaryPath, metadata) {
1874
+ writeFileSync3(metadataPath(binaryPath), JSON.stringify(metadata, null, 2));
1875
+ }
1876
+ function metadataPath(binaryPath) {
1877
+ return `${binaryPath}.json`;
1878
+ }
1879
+ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
1880
+ const lock = `${binaryPath}.lock`;
1881
+ const deadline = Date.now() + (timeoutMs ?? LOCK_TIMEOUT_MS);
1882
+ const staleAfterMs = staleMs ?? STALE_LOCK_MS;
1883
+ mkdirSync5(path2.dirname(binaryPath), { recursive: true });
1884
+ while (Date.now() <= deadline) {
1885
+ try {
1886
+ mkdirSync5(lock);
1887
+ try {
1888
+ return await run();
1889
+ } finally {
1890
+ try {
1891
+ rmSync2(lock, { recursive: true, force: true });
1892
+ } catch {}
1893
+ }
1894
+ } catch (err) {
1895
+ const code = err.code;
1896
+ if (code !== "EEXIST")
1897
+ throw err;
1898
+ try {
1899
+ const ageMs = Date.now() - statSync4(lock).mtimeMs;
1900
+ if (ageMs > staleAfterMs) {
1901
+ rmSync2(lock, { recursive: true, force: true });
1902
+ log("[companion] removed stale install lock", lock);
1903
+ continue;
1904
+ }
1905
+ } catch (statErr) {
1906
+ const statCode = statErr.code;
1907
+ if (statCode !== "ENOENT")
1908
+ throw statErr;
1909
+ }
1910
+ await delay(25);
1911
+ }
1912
+ }
1913
+ log("[companion] install lock timed out", lock);
1914
+ return {
1915
+ status: "failed",
1916
+ binaryPath,
1917
+ error: "Timed out waiting for companion install lock"
1918
+ };
1919
+ }
1920
+ function companionArchiveName(version, target, isWindows = process.platform === "win32") {
1921
+ const ext = isWindows ? "zip" : "tar.gz";
1922
+ return `oh-my-opencode-slim-companion-v${version}-${target}.${ext}`;
1923
+ }
1924
+ function compareSemver(a, b) {
1925
+ const left = parseSemver(a);
1926
+ const right = parseSemver(b);
1927
+ if (!left || !right)
1928
+ return a.localeCompare(b);
1929
+ for (let i = 0;i < 3; i++) {
1930
+ const diff = left[i] - right[i];
1931
+ if (diff !== 0)
1932
+ return diff;
1933
+ }
1934
+ return 0;
1935
+ }
1936
+ function parseSemver(version) {
1937
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
1938
+ if (!match)
1939
+ return null;
1940
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
1941
+ }
1942
+ function formatError(err) {
1943
+ return err instanceof Error ? err.message : String(err);
1944
+ }
1945
+
1946
+ // src/cli/companion.ts
1947
+ async function installCompanion(config) {
1948
+ const target = getCompanionTarget();
1949
+ const finalBinaryPath = getCompanionBinaryPath();
1950
+ if (!target) {
1951
+ return {
1952
+ success: false,
1953
+ configPath: finalBinaryPath,
1954
+ error: `Unsupported platform/architecture: ${process.platform} ${process.arch}`
1955
+ };
1956
+ }
1957
+ const ext = process.platform === "win32" ? "zip" : "tar.gz";
1958
+ const archiveName = `oh-my-opencode-slim-companion-v${COMPANION_MANIFEST.version}-${target}.${ext}`;
1959
+ const downloadUrl = `https://github.com/${COMPANION_MANIFEST.repo}/releases/download/${COMPANION_MANIFEST.tag}/${archiveName}`;
1960
+ if (config.dryRun) {
1961
+ console.log(` [dry-run] Detected companion target: ${target}`);
1962
+ console.log(` [dry-run] Would download archive: ${downloadUrl}`);
1963
+ console.log(` [dry-run] Would extract and install to: ${finalBinaryPath}`);
1964
+ return {
1965
+ success: true,
1966
+ configPath: finalBinaryPath
1967
+ };
1968
+ }
1969
+ const result = await ensureCompanionVersion({
1970
+ config: { enabled: true },
1971
+ manifest: COMPANION_MANIFEST
1972
+ });
1973
+ if (result.status === "installed" || result.status === "current") {
1974
+ return {
1975
+ success: true,
1976
+ configPath: finalBinaryPath
1977
+ };
1978
+ }
1979
+ return {
1980
+ success: false,
1981
+ configPath: finalBinaryPath,
1982
+ error: result.status === "failed" ? result.error : `Companion install skipped: ${result.reason}`
1983
+ };
1984
+ }
1722
1985
  // src/cli/system.ts
1723
1986
  init_compat();
1724
1987
  import { spawnSync as spawnSync2 } from "node:child_process";
1725
- import { statSync as statSync4 } from "node:fs";
1988
+ import { statSync as statSync5 } from "node:fs";
1726
1989
  var cachedOpenCodePath = null;
1727
1990
  function resolvePathCommand(command) {
1728
1991
  try {
@@ -1795,7 +2058,7 @@ function resolveOpenCodePath() {
1795
2058
  if (opencodePath === "opencode")
1796
2059
  continue;
1797
2060
  try {
1798
- const stat = statSync4(opencodePath);
2061
+ const stat = statSync5(opencodePath);
1799
2062
  if (stat.isFile()) {
1800
2063
  cachedOpenCodePath = opencodePath;
1801
2064
  return opencodePath;
@@ -1864,8 +2127,8 @@ var SYMBOLS = {
1864
2127
  warn: `${YELLOW}[!]${RESET}`,
1865
2128
  star: `${YELLOW}★${RESET}`
1866
2129
  };
1867
- var GITHUB_REPO2 = "alvinunreal/oh-my-opencode-slim";
1868
- var GITHUB_URL = `https://github.com/${GITHUB_REPO2}`;
2130
+ var GITHUB_REPO = "alvinunreal/oh-my-opencode-slim";
2131
+ var GITHUB_URL = `https://github.com/${GITHUB_REPO}`;
1869
2132
  function printHeader(isUpdate) {
1870
2133
  console.log();
1871
2134
  console.log(`${BOLD}oh-my-opencode-slim ${isUpdate ? "Update" : "Install"}${RESET}`);
@@ -1908,7 +2171,7 @@ async function askToStarRepo(config) {
1908
2171
  return;
1909
2172
  try {
1910
2173
  const { execFileSync } = await import("node:child_process");
1911
- execFileSync("gh", ["api", "--silent", "--method", "PUT", `/user/starred/${GITHUB_REPO2}`], { stdio: "ignore", timeout: 1e4 });
2174
+ execFileSync("gh", ["api", "--silent", "--method", "PUT", `/user/starred/${GITHUB_REPO}`], { stdio: "ignore", timeout: 1e4 });
1912
2175
  printSuccess("Thanks for starring! ★");
1913
2176
  } catch {
1914
2177
  printInfo(`Couldn't star automatically. You can star manually:
@@ -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
@@ -15,6 +16,7 @@ export declare class CompanionManager {
15
16
  /** sessionId → agent name, for sessions currently busy. */
16
17
  private readonly busyAgentSessions;
17
18
  private readonly config?;
19
+ private companionProcess;
18
20
  constructor(sessionId: string, cwd: string, config?: CompanionConfig);
19
21
  onLoad(): void;
20
22
  /**