oh-my-opencode-slim 2.0.2 → 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.
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-61-orange.svg?style=flat-square)](#contributors-)
660
661
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
661
662
  </div>
662
663
 
@@ -747,6 +748,9 @@ 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
+ </tr>
750
754
  </tbody>
751
755
  </table>
752
756
 
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,12 +749,7 @@ 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
  }
@@ -1267,6 +1277,7 @@ function mergePluginConfigs(base, override) {
1267
1277
  backgroundJobs: deepMerge(base.backgroundJobs, override.backgroundJobs),
1268
1278
  fallback: deepMerge(base.fallback, override.fallback),
1269
1279
  council: deepMerge(base.council, override.council),
1280
+ acpAgents: deepMerge(base.acpAgents, override.acpAgents),
1270
1281
  companion: deepMerge(base.companion, override.companion)
1271
1282
  };
1272
1283
  }
@@ -1582,28 +1593,75 @@ function writeBackgroundSubagentsBlock(targetPath) {
1582
1593
  writeFileSync2(targetPath, nextContent);
1583
1594
  }
1584
1595
 
1585
- // src/cli/companion.ts
1596
+ // src/companion/updater.ts
1597
+ init_compat();
1598
+ import { createHash } from "node:crypto";
1586
1599
  import {
1587
1600
  chmodSync,
1588
1601
  copyFileSync as copyFileSync3,
1589
1602
  existsSync as existsSync6,
1590
1603
  mkdirSync as mkdirSync5,
1591
1604
  mkdtempSync,
1605
+ readFileSync as readFileSync5,
1592
1606
  renameSync as renameSync2,
1593
1607
  rmSync as rmSync2,
1608
+ statSync as statSync4,
1594
1609
  writeFileSync as writeFileSync3
1595
1610
  } from "node:fs";
1596
- import { homedir as homedir4, tmpdir } from "node:os";
1611
+ import { homedir as homedir4, platform, tmpdir } from "node:os";
1597
1612
  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";
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
+ };
1601
1657
  function getCompanionTarget() {
1602
1658
  const p = process.platform;
1603
1659
  const a = process.arch;
1604
1660
  if (p === "darwin") {
1605
1661
  if (a === "arm64")
1606
1662
  return "aarch64-apple-darwin";
1663
+ if (a === "x64")
1664
+ return "x86_64-apple-darwin";
1607
1665
  } else if (p === "linux") {
1608
1666
  if (a === "x64")
1609
1667
  return "x86_64-unknown-linux-gnu";
@@ -1618,47 +1676,109 @@ function getCompanionTarget() {
1618
1676
  function getCompanionBinaryPath() {
1619
1677
  const xdg = process.env.XDG_DATA_HOME?.trim();
1620
1678
  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");
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");
1622
1680
  }
1623
- 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
+ }
1624
1691
  const target = getCompanionTarget();
1625
- const finalBinaryPath = getCompanionBinaryPath();
1626
1692
  if (!target) {
1627
1693
  return {
1628
- success: false,
1629
- configPath: finalBinaryPath,
1694
+ status: "failed",
1695
+ binaryPath,
1630
1696
  error: `Unsupported platform/architecture: ${process.platform} ${process.arch}`
1631
1697
  };
1632
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) {
1633
1743
  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}`);
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) {
1641
1748
  return {
1642
- success: true,
1643
- configPath: finalBinaryPath
1749
+ status: "failed",
1750
+ binaryPath: finalBinaryPath,
1751
+ error: `Missing SHA256 checksum for companion archive: ${archiveName}`
1644
1752
  };
1645
1753
  }
1646
1754
  let buffer;
1755
+ const controller = new AbortController;
1756
+ const timeout = setTimeout(() => controller.abort(), downloadTimeoutMs);
1647
1757
  try {
1648
- const res = await fetch(downloadUrl);
1758
+ const res = await fetch(downloadUrl, { signal: controller.signal });
1649
1759
  if (!res.ok) {
1650
1760
  return {
1651
- success: false,
1652
- configPath: finalBinaryPath,
1761
+ status: "failed",
1762
+ binaryPath: finalBinaryPath,
1653
1763
  error: `Failed to download companion binary (HTTP ${res.status}): ${res.statusText}`
1654
1764
  };
1655
1765
  }
1656
1766
  buffer = await res.arrayBuffer();
1657
1767
  } catch (err) {
1658
1768
  return {
1659
- success: false,
1660
- configPath: finalBinaryPath,
1661
- 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"
1662
1782
  };
1663
1783
  }
1664
1784
  let tempDir = "";
@@ -1672,14 +1792,13 @@ async function installCompanion(config) {
1672
1792
  const { extractZip: extractZip2 } = await Promise.resolve().then(() => (init_zip_extractor(), exports_zip_extractor));
1673
1793
  await extractZip2(archivePath, extractedDir);
1674
1794
  } else {
1675
- const { crossSpawn: crossSpawn2 } = await Promise.resolve().then(() => (init_compat(), exports_compat));
1676
- const proc = crossSpawn2(["tar", "-xzf", archivePath, "-C", extractedDir]);
1795
+ const proc = crossSpawn(["tar", "-xzf", archivePath, "-C", extractedDir]);
1677
1796
  const exitCode = await proc.exited;
1678
1797
  if (exitCode !== 0) {
1679
1798
  const stderr = await proc.stderr();
1680
1799
  return {
1681
- success: false,
1682
- configPath: finalBinaryPath,
1800
+ status: "failed",
1801
+ binaryPath: finalBinaryPath,
1683
1802
  error: `Archive extraction failed (tar exited with ${exitCode}): ${stderr}`
1684
1803
  };
1685
1804
  }
@@ -1688,8 +1807,8 @@ async function installCompanion(config) {
1688
1807
  const extractedBinaryPath = path2.join(extractedDir, binaryName);
1689
1808
  if (!existsSync6(extractedBinaryPath)) {
1690
1809
  return {
1691
- success: false,
1692
- configPath: finalBinaryPath,
1810
+ status: "failed",
1811
+ binaryPath: finalBinaryPath,
1693
1812
  error: `Binary ${binaryName} not found in extracted archive`
1694
1813
  };
1695
1814
  }
@@ -1701,15 +1820,24 @@ async function installCompanion(config) {
1701
1820
  chmodSync(tmpFinalPath, 493);
1702
1821
  }
1703
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
+ });
1704
1831
  return {
1705
- success: true,
1706
- configPath: finalBinaryPath
1832
+ status: "installed",
1833
+ binaryPath: finalBinaryPath,
1834
+ version: manifest.version
1707
1835
  };
1708
1836
  } catch (err) {
1709
1837
  return {
1710
- success: false,
1711
- configPath: finalBinaryPath,
1712
- 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)}`
1713
1841
  };
1714
1842
  } finally {
1715
1843
  if (tempDir) {
@@ -1719,10 +1847,131 @@ async function installCompanion(config) {
1719
1847
  }
1720
1848
  }
1721
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
+ }
1722
1971
  // src/cli/system.ts
1723
1972
  init_compat();
1724
1973
  import { spawnSync as spawnSync2 } from "node:child_process";
1725
- import { statSync as statSync4 } from "node:fs";
1974
+ import { statSync as statSync5 } from "node:fs";
1726
1975
  var cachedOpenCodePath = null;
1727
1976
  function resolvePathCommand(command) {
1728
1977
  try {
@@ -1795,7 +2044,7 @@ function resolveOpenCodePath() {
1795
2044
  if (opencodePath === "opencode")
1796
2045
  continue;
1797
2046
  try {
1798
- const stat = statSync4(opencodePath);
2047
+ const stat = statSync5(opencodePath);
1799
2048
  if (stat.isFile()) {
1800
2049
  cachedOpenCodePath = opencodePath;
1801
2050
  return opencodePath;
@@ -1864,8 +2113,8 @@ var SYMBOLS = {
1864
2113
  warn: `${YELLOW}[!]${RESET}`,
1865
2114
  star: `${YELLOW}★${RESET}`
1866
2115
  };
1867
- var GITHUB_REPO2 = "alvinunreal/oh-my-opencode-slim";
1868
- 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}`;
1869
2118
  function printHeader(isUpdate) {
1870
2119
  console.log();
1871
2120
  console.log(`${BOLD}oh-my-opencode-slim ${isUpdate ? "Update" : "Install"}${RESET}`);
@@ -1908,7 +2157,7 @@ async function askToStarRepo(config) {
1908
2157
  return;
1909
2158
  try {
1910
2159
  const { execFileSync } = await import("node:child_process");
1911
- 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 });
1912
2161
  printSuccess("Thanks for starring! ★");
1913
2162
  } catch {
1914
2163
  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
@@ -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>;
@@ -2,4 +2,4 @@ export * from './constants';
2
2
  export * from './council-schema';
3
3
  export { deepMerge, loadAgentPrompt, loadPluginConfig, } from './loader';
4
4
  export * from './schema';
5
- export { getAgentOverride, getCustomAgentNames } from './utils';
5
+ export { getAcpAgentNames, getAgentOverride, getCustomAgentNames, } from './utils';