@opencomputer/cli 0.4.0 → 0.4.2

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/dist/project.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomInt, randomUUID } from "node:crypto";
2
2
  import { access, cp, mkdir, readFile, readdir, rm, writeFile, } from "node:fs/promises";
3
- import { dirname, relative, resolve } from "node:path";
3
+ import { basename, dirname, relative, resolve } from "node:path";
4
+ import ts from "typescript";
4
5
  const AGENT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5
6
  const AGENT_NAME_ADJECTIVES = [
6
7
  "Amber",
@@ -55,7 +56,7 @@ async function prepareInitializationTarget(root, template) {
55
56
  "opencode.json",
56
57
  "package.json",
57
58
  "agent.ts",
58
- "instructions.md",
59
+ "opencomputer.ts",
59
60
  ...(template.id === "pr-review-readiness"
60
61
  ? [
61
62
  "tools/github.ts",
@@ -1045,6 +1046,18 @@ function tomlString(source, key) {
1045
1046
  }
1046
1047
  }
1047
1048
  export async function readManifest(root) {
1049
+ if (!(await exists(resolve(root, "opencomputer.toml")))) {
1050
+ const id = agentIdFromName(basename(root));
1051
+ return {
1052
+ schema: 1,
1053
+ id,
1054
+ name: id
1055
+ .split("-")
1056
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
1057
+ .join(" "),
1058
+ template: "hello-world",
1059
+ };
1060
+ }
1048
1061
  const source = await readFile(resolve(root, "opencomputer.toml"), "utf8");
1049
1062
  const schema = Number(source.match(/^\s*schema\s*=\s*(\d+)\s*$/m)?.[1]);
1050
1063
  const id = tomlString(source, "id");
@@ -1064,12 +1077,10 @@ export async function findAgentRoot(startDirectory = process.cwd()) {
1064
1077
  let directory = resolve(startDirectory);
1065
1078
  for (;;) {
1066
1079
  const nested = resolve(directory, "opencomputer");
1067
- if ((await exists(resolve(directory, "opencomputer.toml"))) &&
1068
- (await exists(resolve(directory, "instructions.md")))) {
1080
+ if ((await exists(resolve(directory, "agent.ts")))) {
1069
1081
  return directory;
1070
1082
  }
1071
- if ((await exists(resolve(nested, "opencomputer.toml"))) &&
1072
- (await exists(resolve(nested, "instructions.md")))) {
1083
+ if ((await exists(resolve(nested, "agent.ts")))) {
1073
1084
  return nested;
1074
1085
  }
1075
1086
  for (const agentsDirectory of [
@@ -1085,8 +1096,7 @@ export async function findAgentRoot(startDirectory = process.cwd()) {
1085
1096
  if (!entry.isDirectory())
1086
1097
  continue;
1087
1098
  const agent = resolve(agentsDirectory, entry.name);
1088
- if ((await exists(resolve(agent, "opencomputer.toml"))) &&
1089
- (await exists(resolve(agent, "instructions.md")))) {
1099
+ if ((await exists(resolve(agent, "agent.ts")))) {
1090
1100
  detected.push(agent);
1091
1101
  }
1092
1102
  }
@@ -1215,19 +1225,46 @@ export async function initializeTemplateAgentProject(template, directory) {
1215
1225
  name: generateAgentName(),
1216
1226
  template: template.id,
1217
1227
  };
1228
+ const reactiveTools = templateReactiveTools(template);
1218
1229
  await writeManifest(root, manifest);
1219
1230
  await writeFile(resolve(root, "opencomputer.config.ts"), `export default {
1220
1231
  runtime: "opencode",
1221
1232
  region: "auto",
1222
1233
  };
1223
1234
  `);
1224
- await writeFile(resolve(root, "agent.ts"), `export default {
1225
- model: process.env.OPENCOMPUTER_MODEL,
1226
- permissions: {
1227
- shell: "${template.id === "pto-calendar" ? "deny" : "ask"}",
1228
- files: "allow",
1229
- },
1235
+ await writeFile(resolve(root, "agent.ts"), `${reactiveTools.length ? 'import { useTool } from "./opencomputer.js";\n\n' : ""}export default function Agent() {
1236
+ ${reactiveTools
1237
+ .map((tool) => ` useTool(${JSON.stringify(tool)});`)
1238
+ .join("\n")}${reactiveTools.length ? "\n" : ""}
1239
+ return ${JSON.stringify(templateInstructions(template))};
1240
+ }
1241
+ `);
1242
+ await writeFile(resolve(root, "opencomputer.ts"), `export type SessionDataValue =
1243
+ | null | boolean | number | string
1244
+ | readonly SessionDataValue[]
1245
+ | { readonly [key: string]: SessionDataValue };
1246
+
1247
+ type Hooks = {
1248
+ useModel(model: string | { provider: string; model: string }): void;
1249
+ useTool(tool: string | { id: string }): void;
1250
+ useSubagent(agent: string | { id: string }): void;
1251
+ useSessionData<T extends SessionDataValue>(key: string): T | undefined;
1230
1252
  };
1253
+
1254
+ function hooks(): Hooks {
1255
+ const value = (globalThis as Record<PropertyKey, unknown>)[
1256
+ Symbol.for("opencomputer.agent-hooks")
1257
+ ];
1258
+ if (!value) throw new Error("OpenComputer hooks can only run while rendering an agent");
1259
+ return value as Hooks;
1260
+ }
1261
+
1262
+ export const useModel: Hooks["useModel"] = (model) => hooks().useModel(model);
1263
+ export const useTool: Hooks["useTool"] = (tool) => hooks().useTool(tool);
1264
+ export const useSubagent: Hooks["useSubagent"] = (agent) => hooks().useSubagent(agent);
1265
+ export function useSessionData<T extends SessionDataValue>(key: string): T | undefined {
1266
+ return hooks().useSessionData<T>(key);
1267
+ }
1231
1268
  `);
1232
1269
  await writeFile(resolve(root, "opencode.json"), `${JSON.stringify({
1233
1270
  $schema: "https://opencode.ai/config.json",
@@ -1250,7 +1287,6 @@ export async function initializeTemplateAgentProject(template, directory) {
1250
1287
  : {}),
1251
1288
  },
1252
1289
  }, null, 2)}\n`);
1253
- await writeFile(resolve(root, "instructions.md"), templateInstructions(template));
1254
1290
  await writeFile(resolve(root, "workspace", "README.md"), "# Agent workspace\n");
1255
1291
  await updateGitignore(root);
1256
1292
  await writeFile(resolve(root, "package.json"), `${JSON.stringify({
@@ -1275,8 +1311,9 @@ export async function initializeTemplateAgentProject(template, directory) {
1275
1311
  "opencode.json",
1276
1312
  "package.json",
1277
1313
  ".gitignore",
1314
+ "README.md",
1278
1315
  "agent.ts",
1279
- "instructions.md",
1316
+ "opencomputer.ts",
1280
1317
  "workspace/README.md",
1281
1318
  ];
1282
1319
  if (template.integrations.includes("Gmail")) {
@@ -1480,6 +1517,22 @@ const HELLO_WORLD_TEMPLATE = {
1480
1517
  integrations: [],
1481
1518
  suggestedPrompts: ["Say hello and tell me what you can do."],
1482
1519
  };
1520
+ function templateReactiveTools(template) {
1521
+ const tools = [];
1522
+ if (template.integrations.includes("Gmail")) {
1523
+ tools.push("gmail_search", "gmail_read", "gmail_read_full", "gmail_modify", "gmail_send");
1524
+ }
1525
+ if (template.integrations.includes("Google Calendar")) {
1526
+ tools.push("calendar_list", "calendar_events", "calendar_freebusy", "calendar_create_time_off");
1527
+ }
1528
+ if (template.integrations.includes("GitHub")) {
1529
+ tools.push("github_pr_context", "github_checkout");
1530
+ }
1531
+ if (tools.length) {
1532
+ tools.push("opencomputer_connections_list", "opencomputer_connections_request");
1533
+ }
1534
+ return tools;
1535
+ }
1483
1536
  export async function assertStarterTarget(directory) {
1484
1537
  const root = resolve(directory);
1485
1538
  if (!(await exists(root))) {
@@ -1488,7 +1541,7 @@ export async function assertStarterTarget(directory) {
1488
1541
  }
1489
1542
  const reserved = [
1490
1543
  "opencomputer/project.ts",
1491
- "opencomputer/agents/hello-world/opencomputer.toml",
1544
+ "opencomputer/agents/hello-world/agent.ts",
1492
1545
  "package.json",
1493
1546
  "vite.config.ts",
1494
1547
  "index.html",
@@ -1508,20 +1561,45 @@ export async function initializeAgentProject(directory, project) {
1508
1561
  const root = resolve(directory);
1509
1562
  const agentRoot = resolve(root, "opencomputer", "agents", "hello-world");
1510
1563
  await assertStarterTarget(root);
1511
- const initialized = await initializeTemplateAgentProject(HELLO_WORLD_TEMPLATE, agentRoot);
1564
+ await initializeTemplateAgentProject(HELLO_WORLD_TEMPLATE, agentRoot);
1512
1565
  const manifest = {
1513
- ...initialized.manifest,
1514
- ...(project ? { id: project.agentId } : {}),
1566
+ schema: 1,
1567
+ id: project?.agentId ?? "hello-world",
1515
1568
  name: "Hello World",
1569
+ template: "hello-world",
1516
1570
  };
1517
- await writeManifest(agentRoot, manifest);
1518
- await rm(resolve(agentRoot, "package.json"), { force: true });
1519
- await rm(resolve(agentRoot, ".gitignore"), { force: true });
1571
+ for (const path of [
1572
+ "opencomputer.toml",
1573
+ "opencomputer.config.ts",
1574
+ "opencomputer.ts",
1575
+ "opencode.json",
1576
+ "package.json",
1577
+ ".gitignore",
1578
+ "README.md",
1579
+ "tools",
1580
+ "connections",
1581
+ "skills",
1582
+ "channels",
1583
+ "workspace",
1584
+ "evals",
1585
+ ]) {
1586
+ await rm(resolve(agentRoot, path), { recursive: true, force: true });
1587
+ }
1588
+ await writeFile(resolve(agentRoot, "agent.ts"), `import { useInput, useModel } from "@opencomputer/agent";
1589
+
1590
+ export default function Agent() {
1591
+ const input = useInput();
1592
+ useModel("anthropic/claude-sonnet-4.6");
1593
+
1594
+ return input.text
1595
+ ? "You are a helpful OpenComputer agent. Respond directly to: " + input.text
1596
+ : "You are a helpful OpenComputer agent.";
1597
+ }
1598
+ `);
1520
1599
  await updateGitignore(root);
1521
1600
  await mkdir(resolve(root, "src"), { recursive: true });
1522
1601
  await writeFile(resolve(root, "opencomputer", "project.ts"), `export default {
1523
- id: ${JSON.stringify(project?.id ?? manifest.id)},
1524
- name: ${JSON.stringify(project?.name ?? "Hello World")},
1602
+ name: ${JSON.stringify(project?.name ?? basename(root))},
1525
1603
  agents: ["hello-world"],
1526
1604
  };
1527
1605
  `);
@@ -1538,17 +1616,16 @@ export async function initializeAgentProject(directory, project) {
1538
1616
  deploy: "opencomputer deploy",
1539
1617
  },
1540
1618
  dependencies: {
1619
+ "@opencomputer/agent": "^0.1.0",
1541
1620
  react: "^19.2.0",
1542
1621
  "react-dom": "^19.2.0",
1543
1622
  },
1544
1623
  devDependencies: {
1545
- "@opencomputer/cli": "^0.4.0",
1546
- "@opencode-ai/plugin": "^1.18.4",
1624
+ "@opencomputer/cli": "^0.4.2",
1547
1625
  "@types/node": "^24.0.0",
1548
1626
  "@types/react": "^19.2.0",
1549
1627
  "@types/react-dom": "^19.2.0",
1550
1628
  "@vitejs/plugin-react": "^6.0.0",
1551
- "opencode-ai": "1.18.4",
1552
1629
  typescript: "^5.9.0",
1553
1630
  vite: "^8.0.0",
1554
1631
  },
@@ -1565,7 +1642,7 @@ function openComputerDev() {
1565
1642
  resolve("opencomputer/agents/hello-world/.opencomputer/dev.json"),
1566
1643
  "utf8",
1567
1644
  ),
1568
- ) as { url: string; token: string };
1645
+ ) as { url: string; token: string; agent: string };
1569
1646
  } catch {
1570
1647
  throw new Error(
1571
1648
  "OpenComputer is not running. Start npm run dev in another terminal first.",
@@ -1573,10 +1650,29 @@ function openComputerDev() {
1573
1650
  }
1574
1651
  }
1575
1652
 
1653
+ function openComputerAgent() {
1654
+ try {
1655
+ const binding = JSON.parse(
1656
+ readFileSync(resolve(".opencomputer/project.json"), "utf8"),
1657
+ ) as { agentId?: string };
1658
+ if (binding.agentId) return binding.agentId;
1659
+ } catch {
1660
+ // The production build below reports the actionable binding error.
1661
+ }
1662
+ throw new Error(
1663
+ "This app is not connected to an OpenComputer project. Run npm run dev first.",
1664
+ );
1665
+ }
1666
+
1576
1667
  export default defineConfig(({ command }) => {
1577
1668
  const dev = command === "serve" ? openComputerDev() : undefined;
1578
1669
  return {
1579
1670
  plugins: [react()],
1671
+ define: {
1672
+ __OPENCOMPUTER_AGENT__: JSON.stringify(
1673
+ dev?.agent ?? \`\${openComputerAgent()}@production\`,
1674
+ ),
1675
+ },
1580
1676
  ...(dev ? { server: {
1581
1677
  proxy: {
1582
1678
  "/api/opencomputer": {
@@ -1624,7 +1720,7 @@ export default defineConfig(({ command }) => {
1624
1720
  </body>
1625
1721
  </html>
1626
1722
  `);
1627
- await writeFile(resolve(root, "src", "use-agent.ts"), `import { useCallback, useState } from "react";
1723
+ await writeFile(resolve(root, "src", "use-agent.ts"), `import { useCallback, useRef, useState } from "react";
1628
1724
 
1629
1725
  export interface AgentMessage {
1630
1726
  id: string;
@@ -1633,13 +1729,31 @@ export interface AgentMessage {
1633
1729
  }
1634
1730
 
1635
1731
  interface AgentEvent {
1732
+ seq: number;
1636
1733
  type: string;
1637
1734
  data: Record<string, unknown>;
1638
1735
  }
1639
1736
 
1737
+ declare const __OPENCOMPUTER_AGENT__: string;
1738
+ const AGENT = __OPENCOMPUTER_AGENT__;
1739
+
1740
+ async function request<T>(path: string, init?: RequestInit): Promise<T> {
1741
+ const response = await fetch(\`/api/opencomputer/managed-agents\${path}\`, {
1742
+ ...init,
1743
+ headers: { "content-type": "application/json", ...init?.headers },
1744
+ });
1745
+ const body = await response.json();
1746
+ if (!response.ok) {
1747
+ throw new Error(body?.error?.message ?? \`Agent request failed (\${response.status})\`);
1748
+ }
1749
+ return body as T;
1750
+ }
1751
+
1640
1752
  export function useAgent() {
1641
1753
  const [messages, setMessages] = useState<AgentMessage[]>([]);
1642
1754
  const [sessionId, setSessionId] = useState<string>();
1755
+ const sessionRef = useRef<string | undefined>(undefined);
1756
+ const cursorRef = useRef(0);
1643
1757
  const [isRunning, setIsRunning] = useState(false);
1644
1758
  const [error, setError] = useState<string>();
1645
1759
 
@@ -1660,32 +1774,28 @@ export function useAgent() {
1660
1774
  setIsRunning(true);
1661
1775
  setError(undefined);
1662
1776
  try {
1663
- const endpoint = sessionId
1664
- ? \`/api/opencomputer/sessions/\${encodeURIComponent(sessionId)}\`
1665
- : "/api/opencomputer/sessions";
1666
- const response = await fetch(endpoint, {
1667
- method: "POST",
1668
- headers: { "content-type": "application/json" },
1669
- body: JSON.stringify({ prompt }),
1670
- });
1671
- if (!response.ok || !response.body) {
1672
- throw new Error(\`Agent request failed (\${response.status})\`);
1777
+ let activeSession = sessionRef.current;
1778
+ if (!activeSession) {
1779
+ const created = await request<{ session: { id: string } }>("/sessions", {
1780
+ method: "POST",
1781
+ body: JSON.stringify({ agentId: AGENT, source: "local-react" }),
1782
+ });
1783
+ activeSession = created.session.id;
1784
+ sessionRef.current = activeSession;
1785
+ setSessionId(activeSession);
1786
+ } else {
1787
+ await request(\`/sessions/\${encodeURIComponent(activeSession)}/resume\`, {
1788
+ method: "POST",
1789
+ });
1673
1790
  }
1674
- const createdSession = response.headers.get("x-opencomputer-session-id");
1675
- if (createdSession) setSessionId(createdSession);
1676
- const reader = response.body.getReader();
1677
- const decoder = new TextDecoder();
1678
- let buffered = "";
1679
1791
  let streamed = "";
1680
- for (;;) {
1681
- const { done, value } = await reader.read();
1682
- if (done) break;
1683
- buffered += decoder.decode(value, { stream: true });
1684
- const lines = buffered.split("\\n");
1685
- buffered = lines.pop() ?? "";
1686
- for (const line of lines) {
1687
- if (!line.trim()) continue;
1688
- const event = JSON.parse(line) as AgentEvent;
1792
+ const waitFor = async (terminal: (event: AgentEvent) => boolean) => {
1793
+ for (;;) {
1794
+ const result = await request<{ events: AgentEvent[] }>(
1795
+ \`/sessions/\${encodeURIComponent(activeSession!)}/events?after=\${cursorRef.current}\`,
1796
+ );
1797
+ for (const event of result.events) {
1798
+ cursorRef.current = Math.max(cursorRef.current, event.seq);
1689
1799
  if (event.type === "message.delta") {
1690
1800
  streamed += String(event.data.text ?? "");
1691
1801
  setMessages((current) =>
@@ -1702,11 +1812,26 @@ export function useAgent() {
1702
1812
  message.id === assistantId ? { ...message, text } : message,
1703
1813
  ),
1704
1814
  );
1705
- } else if (event.type === "session.failed") {
1706
- throw new Error(String(event.data.message ?? "Agent failed"));
1707
1815
  }
1816
+ if (terminal(event)) return event;
1817
+ }
1818
+ await new Promise((done) => setTimeout(done, 500));
1708
1819
  }
1820
+ };
1821
+ await waitFor((event) => event.type === "runtime.connected");
1822
+ await request(\`/sessions/\${encodeURIComponent(activeSession)}/turns\`, {
1823
+ method: "POST",
1824
+ body: JSON.stringify({ input: prompt, idempotencyKey: crypto.randomUUID() }),
1825
+ });
1826
+ const completed = await waitFor((event) =>
1827
+ event.type === "turn.completed" || event.type === "turn.failed",
1828
+ );
1829
+ if (completed.type === "turn.failed") {
1830
+ throw new Error(String(completed.data.message ?? "Agent failed"));
1709
1831
  }
1832
+ await request(\`/sessions/\${encodeURIComponent(activeSession)}/suspend\`, {
1833
+ method: "POST",
1834
+ });
1710
1835
  } catch (cause) {
1711
1836
  const message = cause instanceof Error ? cause.message : String(cause);
1712
1837
  setError(message);
@@ -1720,7 +1845,7 @@ export function useAgent() {
1720
1845
  } finally {
1721
1846
  setIsRunning(false);
1722
1847
  }
1723
- }, [isRunning, sessionId]);
1848
+ }, [isRunning]);
1724
1849
 
1725
1850
  return { messages, send, isRunning, error };
1726
1851
  }
@@ -1744,7 +1869,7 @@ export default function App() {
1744
1869
  <section className="hero">
1745
1870
  <span className="eyebrow">OpenComputer</span>
1746
1871
  <h1>Hello, world.</h1>
1747
- <p>Your first agent is live. Ask it anything to see the local backend and React app working together.</p>
1872
+ <p>Your first agent is live. The React app stays local while agent code syncs to Development (Cloud).</p>
1748
1873
  </section>
1749
1874
 
1750
1875
  <section className="chat" aria-label="Agent conversation">
@@ -1822,12 +1947,15 @@ form button:disabled { cursor: default; opacity: .45; }
1822
1947
  This project keeps agent definitions in \`opencomputer/\` and the React app in
1823
1948
  \`src/\`.
1824
1949
 
1825
- Start the agent server in one terminal:
1950
+ Sync agent code to Development (Cloud) in one terminal:
1826
1951
 
1827
1952
  \`\`\`bash
1828
1953
  npm run dev
1829
1954
  \`\`\`
1830
1955
 
1956
+ The first run asks you to create a cloud project or select an existing one.
1957
+ That choice is saved for later development runs.
1958
+
1831
1959
  Then start the React app in another:
1832
1960
 
1833
1961
  \`\`\`bash
@@ -1852,17 +1980,54 @@ npm run dev:web
1852
1980
  manifest,
1853
1981
  files: [
1854
1982
  "opencomputer/project.ts",
1855
- ...initialized.files
1856
- .filter((path) => path !== "package.json" && path !== ".gitignore")
1857
- .map((path) => `opencomputer/agents/hello-world/${path}`),
1983
+ "opencomputer/agents/hello-world/agent.ts",
1858
1984
  ...appFiles,
1859
1985
  ],
1860
1986
  };
1861
1987
  }
1988
+ function literalHookIds(source, hook) {
1989
+ const pattern = new RegExp(`\\b${hook}\\(\\s*["']([^"']+)["']`, "g");
1990
+ return [...source.matchAll(pattern)].map((match) => match[1]).sort();
1991
+ }
1992
+ function definedMcpServerIds(source) {
1993
+ return [...source.matchAll(/\bdefineMcpServer\s*\(\s*\{[\s\S]*?\bid\s*:\s*["']([^"']+)["'][\s\S]*?\}\s*\)/g)].map((match) => match[1]).sort();
1994
+ }
1995
+ function agentApiRuntimeSource() {
1996
+ return `function hooks() {
1997
+ const value = globalThis[Symbol.for("opencomputer.agent-hooks")];
1998
+ if (!value) throw new Error("OpenComputer hooks can only run while rendering an agent");
1999
+ return value;
2000
+ }
2001
+ function id(value, kind) {
2002
+ const normalized = String(value).trim();
2003
+ if (!normalized) throw new Error(kind + " requires a non-empty id");
2004
+ return normalized;
2005
+ }
2006
+ export const connection = (value) => Object.freeze({ kind: "connection", id: id(value, "connection") });
2007
+ export const defineMcpServer = (input) => {
2008
+ const url = new URL(input.url);
2009
+ if (url.protocol !== "https:") throw new Error("MCP server URLs must use HTTPS");
2010
+ return Object.freeze({ kind: "mcp", ...input, id: id(input.id, "defineMcpServer"), url: url.toString() });
2011
+ };
2012
+ export const useInput = () => hooks().useInput();
2013
+ export const useCurrentInput = useInput;
2014
+ export const useModel = (model) => hooks().useModel(model);
2015
+ export const useTool = (tool) => hooks().useTool(tool);
2016
+ export const useSubagent = (agent) => hooks().useSubagent(agent);
2017
+ export const useConnection = (value) => hooks().useConnection(value);
2018
+ export const useMcpServer = (server) => hooks().useMcpServer(server);
2019
+ export const useSessionData = (key) => hooks().useSessionData(key);
2020
+ `;
2021
+ }
1862
2022
  export async function prepareAgent(root) {
1863
2023
  const runtime = resolve(root, ".opencomputer", "runtime");
1864
2024
  await rm(runtime, { recursive: true, force: true });
1865
2025
  await mkdir(runtime, { recursive: true });
2026
+ const agentSource = await readFile(resolve(root, "agent.ts"), "utf8");
2027
+ const reactive = /export\s+default\s+(?:async\s+)?function\b/.test(agentSource);
2028
+ if (!reactive) {
2029
+ throw new Error("agent.ts must default-export a synchronous agent function");
2030
+ }
1866
2031
  await writeFile(resolve(runtime, "AGENTS.md"), `# OpenComputer runtime identity
1867
2032
 
1868
2033
  You are an OpenComputer agent. OpenCode is an internal execution detail, not
@@ -1882,9 +2047,7 @@ the product or support surface presented to users.
1882
2047
  - If a connection tool fails, report its exact error. Do not invent alternate
1883
2048
  controls or third-party support instructions.
1884
2049
 
1885
- # Agent instructions
1886
-
1887
- ${await readFile(resolve(root, "instructions.md"), "utf8")}`);
2050
+ `);
1888
2051
  const openCodeConfig = resolve(root, "opencode.json");
1889
2052
  if (await exists(openCodeConfig)) {
1890
2053
  const parsed = JSON.parse(await readFile(openCodeConfig, "utf8"));
@@ -1931,6 +2094,57 @@ ${await readFile(resolve(root, "instructions.md"), "utf8")}`);
1931
2094
  if (await exists(workspace)) {
1932
2095
  await cp(workspace, runtime, { recursive: true });
1933
2096
  }
2097
+ await writeFile(resolve(runtime, "package.json"), `${JSON.stringify({ private: true, type: "module" }, null, 2)}\n`);
2098
+ const transpile = (source, filename) => ts.transpileModule(source, {
2099
+ fileName: filename,
2100
+ compilerOptions: {
2101
+ target: ts.ScriptTarget.ES2022,
2102
+ module: ts.ModuleKind.ESNext,
2103
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
2104
+ },
2105
+ reportDiagnostics: true,
2106
+ });
2107
+ const compiledAgent = transpile(agentSource, "agent.ts");
2108
+ const diagnostics = compiledAgent.diagnostics ?? [];
2109
+ if (diagnostics.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) {
2110
+ throw new Error(`agent.ts could not be compiled: ${diagnostics
2111
+ .map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " "))
2112
+ .join("; ")}`);
2113
+ }
2114
+ const compiledSource = compiledAgent.outputText.replace(/(["'])@opencomputer\/agent\1/g, '"./opencomputer-agent.js"');
2115
+ await writeFile(resolve(runtime, "agent.js"), compiledSource);
2116
+ await writeFile(resolve(runtime, "opencomputer-agent.js"), agentApiRuntimeSource());
2117
+ const toolEntries = await readdir(resolve(runtime, ".opencode", "tools"), {
2118
+ withFileTypes: true,
2119
+ });
2120
+ const reactiveTools = [];
2121
+ for (const entry of toolEntries.filter((candidate) => candidate.isFile())) {
2122
+ const stem = entry.name
2123
+ .replace(/\.[^.]+$/, "")
2124
+ .replace(/[^a-zA-Z0-9_]+/g, "_");
2125
+ const source = await readFile(resolve(runtime, ".opencode", "tools", entry.name), "utf8");
2126
+ for (const match of source.matchAll(/export\s+const\s+([a-zA-Z0-9_]+)\s*=/g)) {
2127
+ reactiveTools.push(`${stem}_${match[1]}`);
2128
+ }
2129
+ }
2130
+ await mkdir(resolve(runtime, ".opencomputer"), { recursive: true });
2131
+ await writeFile(resolve(runtime, ".opencomputer", "reactive.json"), `${JSON.stringify({
2132
+ version: 2,
2133
+ entry: "../agent.js",
2134
+ tools: [...new Set([
2135
+ ...reactiveTools,
2136
+ ...literalHookIds(agentSource, "useTool"),
2137
+ ])].sort(),
2138
+ subagents: literalHookIds(agentSource, "useSubagent"),
2139
+ connections: [...new Set([
2140
+ ...literalHookIds(agentSource, "connection"),
2141
+ ...literalHookIds(agentSource, "useConnection"),
2142
+ ])].sort(),
2143
+ mcpServers: [...new Set([
2144
+ ...definedMcpServerIds(agentSource),
2145
+ ...literalHookIds(agentSource, "useMcpServer"),
2146
+ ])].sort(),
2147
+ }, null, 2)}\n`);
1934
2148
  return runtime;
1935
2149
  }
1936
2150
  async function collectNames(root, type) {
@@ -1992,7 +2206,7 @@ async function validateTemplateRequirements(root, manifest) {
1992
2206
  throw new Error("PTO calendar tools are missing. Run `opencomputer tools add calendar` before deploying.");
1993
2207
  }
1994
2208
  }
1995
- export async function buildAgentArtifact(root) {
2209
+ export async function buildAgentArtifact(root, agentId) {
1996
2210
  const startedAt = performance.now();
1997
2211
  const manifest = await readManifest(root);
1998
2212
  await validateTemplateRequirements(root, manifest);
@@ -2005,7 +2219,7 @@ export async function buildAgentArtifact(root) {
2005
2219
  files: await collectFiles(runtime),
2006
2220
  }));
2007
2221
  return {
2008
- agentId: manifest.id,
2222
+ agentId: agentId ?? manifest.id,
2009
2223
  name: manifest.name,
2010
2224
  channels,
2011
2225
  connections,