zooid 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  > A chat app to collaborate with AI agents, alongside the rest of your team. Open-source, self-hostable, any model, any CLI.
4
4
 
5
- Zooid is an open-source, self-hostable chat app for collaborating with AI agents alongside your team. It brings [ACP](https://agentclientprotocol.com)-compatible agents (Claude Code, [opencode](https://opencode.ai), Codex, …) into [Matrix](https://matrix.org) rooms as first-class participants — people and agents in the same rooms, threads, and approvals, no separate "AI dashboard," no vendor lock-in. Deploy with `zooid init`, run with `zooid dev`.
5
+ Zooid is an open-source, self-hostable chat app for collaborating with AI agents alongside your team. It brings [ACP](https://agentclientprotocol.com)-compatible agents (Claude Code, [opencode](https://opencode.ai), Codex, pi, …) into [Matrix](https://matrix.org) rooms as first-class participants — people and agents in the same rooms, threads, and approvals, no separate "AI dashboard," no vendor lock-in. Deploy with `zooid init`, run with `zooid dev`.
6
6
 
7
7
  **Full docs: [zooid.dev/docs](https://zooid.dev/docs)** · **Join the community server: [community.zoon.eco](https://community.zoon.eco)**
8
8
 
9
- - **Protocol-first.** Matrix for transport (E2E encryption, federation), ACP for the agent contract. Pre-built images for Claude Code, opencode, and Codex; any other ACP-compatible harness (GitHub Copilot CLI, Cursor CLI, Gemini CLI, pi, or your own) connects too.
9
+ - **Protocol-first.** Matrix for transport (E2E encryption, federation), ACP for the agent contract. Pre-built images for Claude Code, opencode, Codex, and pi; any other ACP-compatible harness (GitHub Copilot CLI, Cursor CLI, Gemini CLI, or your own) connects too.
10
10
  - **Run it anywhere.** Each daemon is a *workstation* with its own identity, running as many agents as you like — put it on your laptop, a box, a pod, or a cluster; it doesn't have to live next to the homeserver. Several workstations can share one homeserver.
11
11
  - **Containerized runtime.** Docker or Podman. Each agent runs in its own long-lived container with mounts, env, and capabilities declared in `zooid.yaml`.
12
12
  - **Multi-agent collaboration.** Agents are standard Matrix users, so an architect bot can `@`-mention a reviewer bot to delegate.
@@ -28,7 +28,9 @@ mkdir my-workforce && cd my-workforce
28
28
  zooid init
29
29
  ```
30
30
 
31
- `zooid init` asks which harness (Claude Code, opencode, Codex, …), which model provider, and how to authenticate. It writes a clean `zooid.yaml` and any `.env` it needs.
31
+ `zooid init` asks which harness (`claude`, `codex`, `opencode`, `pi`), which model provider, and how to authenticate. It writes a clean `zooid.yaml` and any `.env` it needs.
32
+
33
+ Every preset can run off a subscription you already have instead of a fresh API key. `pi` goes a step further: it only offers that choice once it has actually found a `pi` login, inherits the `defaultProvider` / `defaultModel` you already run, and gives the agent its own relocated config dir (`.pi-agent/`) so it never writes into your `~/.pi`.
32
34
 
33
35
  Boot the local stack:
34
36
 
@@ -85,6 +87,7 @@ Zooid publishes a small set of base images on GHCR. Drop one into `zooid.yaml` u
85
87
  - `ghcr.io/zooid-ai/agent-claude-code` — agent-base + the Claude Code ACP shim.
86
88
  - `ghcr.io/zooid-ai/agent-codex` — agent-base + the Codex ACP shim.
87
89
  - `ghcr.io/zooid-ai/agent-opencode` — agent-base + opencode.
90
+ - `ghcr.io/zooid-ai/agent-pi` — agent-base + pi + pi-acp.
88
91
 
89
92
  The persona — `CLAUDE.md` / `AGENTS.md`, `.claude/settings.json`, skills, MCP servers — lives in the agent's `workdir` on the host. Zooid bind-mounts that directory into the container at runtime, so the shim picks it up the same way it would on your laptop. No `docker build`, no custom image, no rebuild when you tweak instructions.
90
93
 
package/dist/bin.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  renderRegistration,
17
17
  startDaemonSocketServer,
18
18
  startWorkforcePublisher
19
- } from "./chunk-KGYQ5YNP.js";
19
+ } from "./chunk-3Q4BPAZD.js";
20
20
 
21
21
  // src/bin.ts
22
22
  import { resolve as resolve6 } from "path";
@@ -1494,12 +1494,13 @@ function loadEnvFiles(cwd) {
1494
1494
  }
1495
1495
 
1496
1496
  // src/commands/init.ts
1497
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync2, writeFileSync as writeFileSync5 } from "fs";
1497
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync2, symlinkSync, writeFileSync as writeFileSync5 } from "fs";
1498
+ import { homedir as homedir2 } from "os";
1498
1499
  import { dirname as dirname7, join as join14, resolve as resolve4 } from "path";
1499
1500
 
1500
1501
  // src/commands/init/generators.ts
1501
1502
  function generateZooidYaml(opts) {
1502
- const acpBlock = opts.preset === "opencode" || !opts.model ? ` acp: { preset: ${opts.preset} }` : ` acp:
1503
+ const acpBlock = opts.preset === "opencode" || opts.preset === "pi" || !opts.model ? ` acp: { preset: ${opts.preset} }` : ` acp:
1503
1504
  preset: ${opts.preset}
1504
1505
  model: ${opts.model}`;
1505
1506
  return `runtime: local
@@ -1586,7 +1587,10 @@ function generateEnv(opts) {
1586
1587
  `;
1587
1588
  }
1588
1589
  function generateGitignore() {
1589
- return [".env", "node_modules/", "data/", ""].join("\n");
1590
+ return [".env", ".pi-agent/", "node_modules/", "data/", ""].join("\n");
1591
+ }
1592
+ function generatePiSettings(opts) {
1593
+ return JSON.stringify({ defaultProvider: opts.provider, defaultModel: opts.model }, null, 2) + "\n";
1590
1594
  }
1591
1595
  function generateOpencodeReadme() {
1592
1596
  return `See https://opencode.ai/config for the full opencode.json schema, including
@@ -1643,18 +1647,59 @@ function findSimplePreset(name) {
1643
1647
  function findOpencodeProvider(id) {
1644
1648
  return OPENCODE_PROVIDERS.find((p) => p.id === id);
1645
1649
  }
1650
+ var PI_PROVIDERS = [
1651
+ {
1652
+ id: "openrouter",
1653
+ label: "OpenRouter",
1654
+ description: "many providers via OpenRouter credits",
1655
+ apiKeyEnvVar: "OPENROUTER_API_KEY"
1656
+ },
1657
+ {
1658
+ id: "anthropic",
1659
+ label: "Anthropic",
1660
+ description: "Claude via direct Anthropic API key",
1661
+ apiKeyEnvVar: "ANTHROPIC_API_KEY"
1662
+ },
1663
+ {
1664
+ id: "openai",
1665
+ label: "OpenAI",
1666
+ description: "GPT via direct OpenAI API key",
1667
+ apiKeyEnvVar: "OPENAI_API_KEY"
1668
+ }
1669
+ ];
1670
+ var PI_DEFAULT_PROVIDER = "openrouter";
1671
+ var PI_DEFAULT_MODEL = "deepseek/deepseek-v4-pro";
1672
+ var PI_AGENT_DIR = ".pi-agent";
1673
+ var PI_AUTH_FILE = ".pi/agent/auth.json";
1674
+ var PI_SETTINGS_FILE = ".pi/agent/settings.json";
1675
+ function findPiProvider(id) {
1676
+ return PI_PROVIDERS.find((p) => p.id === id);
1677
+ }
1646
1678
 
1647
1679
  // src/commands/init/sniff.ts
1648
- import { existsSync as existsSync6 } from "fs";
1680
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
1649
1681
  import { homedir } from "os";
1650
1682
  import { join as join13 } from "path";
1651
1683
  function sniffCredentials(preset, home = homedir()) {
1652
- const meta = findSimplePreset(preset);
1653
- if (!meta) return { found: false };
1654
- const full = join13(home, meta.credentialDir);
1684
+ const rel = preset === "pi" ? PI_AUTH_FILE : findSimplePreset(preset)?.credentialDir;
1685
+ if (!rel) return { found: false };
1686
+ const full = join13(home, rel);
1655
1687
  if (existsSync6(full)) return { found: true, path: full };
1656
1688
  return { found: false };
1657
1689
  }
1690
+ function sniffPiDefaults(home = homedir()) {
1691
+ const source = join13(home, PI_SETTINGS_FILE);
1692
+ try {
1693
+ const raw = JSON.parse(readFileSync7(source, "utf8"));
1694
+ const provider = raw.defaultProvider;
1695
+ const model = raw.defaultModel;
1696
+ if (typeof provider !== "string" || !provider) return void 0;
1697
+ if (typeof model !== "string" || !model) return void 0;
1698
+ return { provider, model, source };
1699
+ } catch {
1700
+ return void 0;
1701
+ }
1702
+ }
1658
1703
 
1659
1704
  // src/commands/init.ts
1660
1705
  var IGNORED_PREEXISTING = /* @__PURE__ */ new Set([
@@ -1680,6 +1725,7 @@ async function runInit(opts) {
1680
1725
  }
1681
1726
  }
1682
1727
  const writes = [];
1728
+ let piInherited;
1683
1729
  if (opts.preset === "claude" || opts.preset === "codex") {
1684
1730
  if (!opts.auth) throw new Error("--auth (subscription|api-key) is required for claude/codex");
1685
1731
  const meta = findSimplePreset(opts.preset);
@@ -1732,6 +1778,34 @@ async function runInit(opts) {
1732
1778
  content: generateEnv({ envVar: providerMeta.apiKeyEnvVar, value: opts.apiKey })
1733
1779
  });
1734
1780
  }
1781
+ } else if (opts.preset === "pi") {
1782
+ if (!opts.auth) throw new Error("--auth (subscription|api-key) is required for pi");
1783
+ if (opts.auth === "subscription" && !sniffCredentials("pi", opts.home).found) {
1784
+ throw new Error(
1785
+ "no pi login found \u2014 run `pi` and /login first, or use --auth api-key"
1786
+ );
1787
+ }
1788
+ if (opts.auth === "api-key" && !opts.provider) {
1789
+ throw new Error("--provider is required for pi --auth api-key");
1790
+ }
1791
+ const providerMeta = opts.provider ? findPiProvider(opts.provider) : void 0;
1792
+ if (opts.provider && !providerMeta) throw new Error(`unknown pi provider: ${opts.provider}`);
1793
+ const inherited = sniffPiDefaults(opts.home);
1794
+ const provider = opts.provider ?? inherited?.provider ?? PI_DEFAULT_PROVIDER;
1795
+ const model = opts.model ?? inherited?.model ?? PI_DEFAULT_MODEL;
1796
+ const settingsProvider = opts.model ? provider : inherited?.provider ?? provider;
1797
+ writes.push({ path: "zooid.yaml", content: generateZooidYaml({ preset: "pi" }) });
1798
+ writes.push({ path: "agents/zooid-assistant/AGENTS.md", content: generateAgentsMd() });
1799
+ writes.push({
1800
+ path: `agents/zooid-assistant/${PI_AGENT_DIR}/settings.json`,
1801
+ content: generatePiSettings({ provider: settingsProvider, model })
1802
+ });
1803
+ const envLines = [`PI_CODING_AGENT_DIR=${PI_AGENT_DIR}`];
1804
+ if (opts.auth === "api-key" && opts.apiKey) {
1805
+ envLines.push(`${providerMeta.apiKeyEnvVar}=${opts.apiKey}`);
1806
+ }
1807
+ writes.push({ path: ".env", content: envLines.join("\n") + "\n" });
1808
+ piInherited = inherited;
1735
1809
  } else {
1736
1810
  throw new Error(`unknown preset: ${String(opts.preset)}`);
1737
1811
  }
@@ -1757,6 +1831,35 @@ async function runInit(opts) {
1757
1831
  );
1758
1832
  }
1759
1833
  }
1834
+ if (opts.preset === "pi") {
1835
+ if (piInherited) {
1836
+ console.log(
1837
+ `\u2713 Inherited defaultProvider=${piInherited.provider} defaultModel=${piInherited.model} from ${piInherited.source}`
1838
+ );
1839
+ } else {
1840
+ console.log(
1841
+ `\u2713 Pinned defaultProvider=${PI_DEFAULT_PROVIDER} defaultModel=${PI_DEFAULT_MODEL} (pi's own default returns an empty turn)`
1842
+ );
1843
+ }
1844
+ const s = sniffCredentials("pi", opts.home);
1845
+ if (opts.auth === "subscription" && s.found) {
1846
+ const authSource = join14(opts.home ?? homedir2(), PI_AUTH_FILE);
1847
+ const linkPath = join14(dir, `agents/zooid-assistant/${PI_AGENT_DIR}/auth.json`);
1848
+ if (!existsSync7(linkPath)) {
1849
+ mkdirSync5(dirname7(linkPath), { recursive: true });
1850
+ symlinkSync(authSource, linkPath);
1851
+ }
1852
+ console.log(
1853
+ `\u2713 Shared pi login from ${s.path} \u2014 the agent uses your subscription via a symlink at ${PI_AGENT_DIR}/auth.json`
1854
+ );
1855
+ } else if (s.found) {
1856
+ console.log(
1857
+ `\u2713 Found pi login at ${s.path} \u2014 the agent gets its own config at ${PI_AGENT_DIR}/ and will not touch it`
1858
+ );
1859
+ } else {
1860
+ console.warn(`\u26A0 No pi login detected \u2014 the agent uses its own API key from .env`);
1861
+ }
1862
+ }
1760
1863
  console.log("\nNext: zooid dev");
1761
1864
  }
1762
1865
 
@@ -1774,10 +1877,11 @@ async function resolveOptions(flags) {
1774
1877
  choices: [
1775
1878
  { name: "claude (Claude Code)", value: "claude" },
1776
1879
  { name: "codex (OpenAI Codex)", value: "codex" },
1777
- { name: "opencode", value: "opencode" }
1880
+ { name: "opencode", value: "opencode" },
1881
+ { name: "pi", value: "pi" }
1778
1882
  ]
1779
1883
  }),
1780
- () => "--preset is required (claude | codex | opencode)"
1884
+ () => "--preset is required (claude | codex | opencode | pi)"
1781
1885
  );
1782
1886
  if (preset === "opencode") {
1783
1887
  const provider = flags.provider ?? "opencode-go";
@@ -1806,6 +1910,45 @@ async function resolveOptions(flags) {
1806
1910
  overwrite: flags.overwrite
1807
1911
  };
1808
1912
  }
1913
+ if (preset === "pi") {
1914
+ const loginFound = sniffCredentials("pi").found;
1915
+ const auth2 = flags.auth ?? (loginFound ? await ask(
1916
+ () => select({
1917
+ message: "A pi login was found. How should the agent authenticate?",
1918
+ choices: [
1919
+ { name: "Share my pi login (agent uses my existing subscription)", value: "subscription" },
1920
+ { name: "Give the agent its own key (billed and revoked separately)", value: "api-key" }
1921
+ ]
1922
+ }),
1923
+ () => "--auth is required (subscription | api-key)"
1924
+ ) : "api-key");
1925
+ if (auth2 === "subscription") {
1926
+ return {
1927
+ dir: flags.dir,
1928
+ preset,
1929
+ auth: auth2,
1930
+ force: flags.force,
1931
+ overwrite: flags.overwrite
1932
+ };
1933
+ }
1934
+ const provider = flags.provider ?? PI_DEFAULT_PROVIDER;
1935
+ const providerMeta = findPiProvider(provider);
1936
+ if (!providerMeta) throw new Error(`unknown pi provider: ${provider}`);
1937
+ const apiKey2 = flags.apiKey ?? await ask(
1938
+ () => password({ message: `${providerMeta.label} API key:` }),
1939
+ () => "--api-key is required"
1940
+ );
1941
+ return {
1942
+ dir: flags.dir,
1943
+ preset,
1944
+ auth: auth2,
1945
+ provider,
1946
+ model: flags.model,
1947
+ apiKey: apiKey2,
1948
+ force: flags.force,
1949
+ overwrite: flags.overwrite
1950
+ };
1951
+ }
1809
1952
  const meta = findSimplePreset(preset);
1810
1953
  if (!meta) throw new Error(`unknown preset: ${preset}`);
1811
1954
  const auth = flags.auth ?? await ask(
@@ -1938,7 +2081,7 @@ async function runStart(flags) {
1938
2081
  }
1939
2082
 
1940
2083
  // src/commands/status.ts
1941
- import { readFileSync as readFileSync7 } from "fs";
2084
+ import { readFileSync as readFileSync8 } from "fs";
1942
2085
  import { dirname as dirname8 } from "path";
1943
2086
  import chalk2 from "chalk";
1944
2087
  async function probe(url, timeoutMs = 2e3) {
@@ -1963,7 +2106,7 @@ async function collectStatus(opts) {
1963
2106
  agents: []
1964
2107
  };
1965
2108
  }
1966
- const cfg = loadZooidConfig(readFileSync7(found.path, "utf8"), {
2109
+ const cfg = loadZooidConfig(readFileSync8(found.path, "utf8"), {
1967
2110
  configDir: dirname8(found.path)
1968
2111
  });
1969
2112
  const matrixEntry = Object.entries(cfg.transports).find(
@@ -1994,7 +2137,7 @@ async function runStatus(flags) {
1994
2137
  if (port === void 0) {
1995
2138
  const found = findConfigFile(cwd);
1996
2139
  if (found) {
1997
- const cfg = loadZooidConfig(readFileSync7(found.path, "utf8"), {
2140
+ const cfg = loadZooidConfig(readFileSync8(found.path, "utf8"), {
1998
2141
  configDir: dirname8(found.path)
1999
2142
  });
2000
2143
  const matrix = findMatrixTransport(cfg);
@@ -2068,7 +2211,7 @@ cli.command("status", "Print Tuwunel + daemon health").option("--data <dir>", "P
2068
2211
  port: flags.port !== void 0 ? Number(flags.port) : void 0
2069
2212
  });
2070
2213
  });
2071
- cli.command("init [dir]", "Scaffold a new zooid workforce in the current (or named) directory").option("--preset <name>", "claude | codex | opencode").option("--auth <mode>", "subscription | api-key (claude/codex only)").option("--model <id>", "Model identifier").option("--provider <id>", "opencode provider: opencode-go | opencode | anthropic | openrouter | custom").option("--api-key <value>", "API key (api-key path; opencode always)").option("--force", "Allow scaffolding into a non-empty directory").option("--overwrite", "With --force, overwrite existing files").option("--no-interactive", "Disable prompts; require all flags up front").action(async (dir, flags) => {
2214
+ cli.command("init [dir]", "Scaffold a new zooid workforce in the current (or named) directory").option("--preset <name>", "claude | codex | opencode | pi").option("--auth <mode>", "subscription | api-key (claude/codex/pi only)").option("--model <id>", "Model identifier").option("--provider <id>", "opencode provider: opencode-go | opencode | anthropic | openrouter | custom; pi provider: openrouter | anthropic | openai").option("--api-key <value>", "API key (api-key path; opencode always)").option("--force", "Allow scaffolding into a non-empty directory").option("--overwrite", "With --force, overwrite existing files").option("--no-interactive", "Disable prompts; require all flags up front").action(async (dir, flags) => {
2072
2215
  const resolved = await resolveOptions({
2073
2216
  dir: dir ?? process.cwd(),
2074
2217
  preset: flags.preset,