@opencomputer/cli 0.3.13 → 0.4.1

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",
@@ -88,7 +89,13 @@ async function prepareInitializationTarget(root, template) {
88
89
  }
89
90
  async function updateGitignore(root) {
90
91
  const path = resolve(root, ".gitignore");
91
- const required = ["node_modules/", ".opencomputer/", ".env"];
92
+ const required = [
93
+ "node_modules/",
94
+ "dist/",
95
+ ".opencomputer/",
96
+ ".env",
97
+ ".env.local",
98
+ ];
92
99
  let existing = "";
93
100
  try {
94
101
  existing = await readFile(path, "utf8");
@@ -1057,10 +1064,39 @@ export async function readManifest(root) {
1057
1064
  export async function findAgentRoot(startDirectory = process.cwd()) {
1058
1065
  let directory = resolve(startDirectory);
1059
1066
  for (;;) {
1067
+ const nested = resolve(directory, "opencomputer");
1060
1068
  if ((await exists(resolve(directory, "opencomputer.toml"))) &&
1061
- (await exists(resolve(directory, "instructions.md")))) {
1069
+ (await exists(resolve(directory, "agent.ts")))) {
1062
1070
  return directory;
1063
1071
  }
1072
+ if ((await exists(resolve(nested, "opencomputer.toml"))) &&
1073
+ (await exists(resolve(nested, "agent.ts")))) {
1074
+ return nested;
1075
+ }
1076
+ for (const agentsDirectory of [
1077
+ resolve(directory, "opencomputer", "agents"),
1078
+ resolve(directory, "agents"),
1079
+ ]) {
1080
+ if (!(await exists(agentsDirectory)))
1081
+ continue;
1082
+ const detected = [];
1083
+ for (const entry of await readdir(agentsDirectory, {
1084
+ withFileTypes: true,
1085
+ })) {
1086
+ if (!entry.isDirectory())
1087
+ continue;
1088
+ const agent = resolve(agentsDirectory, entry.name);
1089
+ if ((await exists(resolve(agent, "opencomputer.toml"))) &&
1090
+ (await exists(resolve(agent, "agent.ts")))) {
1091
+ detected.push(agent);
1092
+ }
1093
+ }
1094
+ if (detected.length === 1)
1095
+ return detected[0];
1096
+ if (detected.length > 1) {
1097
+ throw new Error("This project has multiple agents. Select an agent when starting dev mode.");
1098
+ }
1099
+ }
1064
1100
  const parent = dirname(directory);
1065
1101
  if (parent === directory)
1066
1102
  return undefined;
@@ -1161,7 +1197,7 @@ export async function addSlackChannel(root) {
1161
1197
  }, null, 2)}\n`);
1162
1198
  return ["channels/slack.ts", "slack/manifest.json"];
1163
1199
  }
1164
- export async function initializeAgentProject(template, directory) {
1200
+ export async function initializeTemplateAgentProject(template, directory) {
1165
1201
  const root = resolve(directory);
1166
1202
  await prepareInitializationTarget(root, template);
1167
1203
  for (const path of [
@@ -1180,19 +1216,46 @@ export async function initializeAgentProject(template, directory) {
1180
1216
  name: generateAgentName(),
1181
1217
  template: template.id,
1182
1218
  };
1219
+ const reactiveTools = templateReactiveTools(template);
1183
1220
  await writeManifest(root, manifest);
1184
1221
  await writeFile(resolve(root, "opencomputer.config.ts"), `export default {
1185
1222
  runtime: "opencode",
1186
1223
  region: "auto",
1187
1224
  };
1188
1225
  `);
1189
- await writeFile(resolve(root, "agent.ts"), `export default {
1190
- model: process.env.OPENCOMPUTER_MODEL,
1191
- permissions: {
1192
- shell: "${template.id === "pto-calendar" ? "deny" : "ask"}",
1193
- files: "allow",
1194
- },
1226
+ await writeFile(resolve(root, "agent.ts"), `${reactiveTools.length ? 'import { useTool } from "./opencomputer.js";\n\n' : ""}export default function Agent() {
1227
+ ${reactiveTools
1228
+ .map((tool) => ` useTool(${JSON.stringify(tool)});`)
1229
+ .join("\n")}${reactiveTools.length ? "\n" : ""}
1230
+ return ${JSON.stringify(templateInstructions(template))};
1231
+ }
1232
+ `);
1233
+ await writeFile(resolve(root, "opencomputer.ts"), `export type SessionDataValue =
1234
+ | null | boolean | number | string
1235
+ | readonly SessionDataValue[]
1236
+ | { readonly [key: string]: SessionDataValue };
1237
+
1238
+ type Hooks = {
1239
+ useModel(model: string | { provider: string; model: string }): void;
1240
+ useTool(tool: string | { id: string }): void;
1241
+ useSubagent(agent: string | { id: string }): void;
1242
+ useSessionData<T extends SessionDataValue>(key: string): T | undefined;
1195
1243
  };
1244
+
1245
+ function hooks(): Hooks {
1246
+ const value = (globalThis as Record<PropertyKey, unknown>)[
1247
+ Symbol.for("opencomputer.agent-hooks")
1248
+ ];
1249
+ if (!value) throw new Error("OpenComputer hooks can only run while rendering an agent");
1250
+ return value as Hooks;
1251
+ }
1252
+
1253
+ export const useModel: Hooks["useModel"] = (model) => hooks().useModel(model);
1254
+ export const useTool: Hooks["useTool"] = (tool) => hooks().useTool(tool);
1255
+ export const useSubagent: Hooks["useSubagent"] = (agent) => hooks().useSubagent(agent);
1256
+ export function useSessionData<T extends SessionDataValue>(key: string): T | undefined {
1257
+ return hooks().useSessionData<T>(key);
1258
+ }
1196
1259
  `);
1197
1260
  await writeFile(resolve(root, "opencode.json"), `${JSON.stringify({
1198
1261
  $schema: "https://opencode.ai/config.json",
@@ -1215,7 +1278,6 @@ export async function initializeAgentProject(template, directory) {
1215
1278
  : {}),
1216
1279
  },
1217
1280
  }, null, 2)}\n`);
1218
- await writeFile(resolve(root, "instructions.md"), templateInstructions(template));
1219
1281
  await writeFile(resolve(root, "workspace", "README.md"), "# Agent workspace\n");
1220
1282
  await updateGitignore(root);
1221
1283
  await writeFile(resolve(root, "package.json"), `${JSON.stringify({
@@ -1241,7 +1303,7 @@ export async function initializeAgentProject(template, directory) {
1241
1303
  "package.json",
1242
1304
  ".gitignore",
1243
1305
  "agent.ts",
1244
- "instructions.md",
1306
+ "opencomputer.ts",
1245
1307
  "workspace/README.md",
1246
1308
  ];
1247
1309
  if (template.integrations.includes("Gmail")) {
@@ -1437,10 +1499,464 @@ Pass criteria:
1437
1499
  }
1438
1500
  return { root, manifest, files };
1439
1501
  }
1502
+ const HELLO_WORLD_TEMPLATE = {
1503
+ id: "hello-world",
1504
+ name: "Hello World",
1505
+ description: "Greet the user, explain that this agent is running live, and answer simple questions clearly.",
1506
+ category: "Getting started",
1507
+ integrations: [],
1508
+ suggestedPrompts: ["Say hello and tell me what you can do."],
1509
+ };
1510
+ function templateReactiveTools(template) {
1511
+ const tools = [];
1512
+ if (template.integrations.includes("Gmail")) {
1513
+ tools.push("gmail_search", "gmail_read", "gmail_read_full", "gmail_modify", "gmail_send");
1514
+ }
1515
+ if (template.integrations.includes("Google Calendar")) {
1516
+ tools.push("calendar_list", "calendar_events", "calendar_freebusy", "calendar_create_time_off");
1517
+ }
1518
+ if (template.integrations.includes("GitHub")) {
1519
+ tools.push("github_pr_context", "github_checkout");
1520
+ }
1521
+ if (tools.length) {
1522
+ tools.push("opencomputer_connections_list", "opencomputer_connections_request");
1523
+ }
1524
+ return tools;
1525
+ }
1526
+ export async function assertStarterTarget(directory) {
1527
+ const root = resolve(directory);
1528
+ if (!(await exists(root))) {
1529
+ await mkdir(root, { recursive: true });
1530
+ return;
1531
+ }
1532
+ const reserved = [
1533
+ "opencomputer/project.ts",
1534
+ "opencomputer/agents/hello-world/opencomputer.toml",
1535
+ "package.json",
1536
+ "vite.config.ts",
1537
+ "index.html",
1538
+ "src/App.tsx",
1539
+ "src/main.tsx",
1540
+ ];
1541
+ const conflicts = [];
1542
+ for (const path of reserved) {
1543
+ if (await exists(resolve(root, path)))
1544
+ conflicts.push(path);
1545
+ }
1546
+ if (conflicts.length) {
1547
+ throw new Error(`Target already contains OpenComputer app files: ${conflicts.join(", ")}`);
1548
+ }
1549
+ }
1550
+ export async function initializeAgentProject(directory, project) {
1551
+ const root = resolve(directory);
1552
+ const agentRoot = resolve(root, "opencomputer", "agents", "hello-world");
1553
+ await assertStarterTarget(root);
1554
+ const initialized = await initializeTemplateAgentProject(HELLO_WORLD_TEMPLATE, agentRoot);
1555
+ const manifest = {
1556
+ ...initialized.manifest,
1557
+ ...(project ? { id: project.agentId } : {}),
1558
+ name: "Hello World",
1559
+ };
1560
+ await writeManifest(agentRoot, manifest);
1561
+ await rm(resolve(agentRoot, "package.json"), { force: true });
1562
+ await rm(resolve(agentRoot, ".gitignore"), { force: true });
1563
+ await updateGitignore(root);
1564
+ await mkdir(resolve(root, "src"), { recursive: true });
1565
+ await writeFile(resolve(root, "opencomputer", "project.ts"), `export default {
1566
+ ${project ? ` id: ${JSON.stringify(project.id)},\n` : ""} name: ${JSON.stringify(project?.name ?? basename(root))},
1567
+ agents: ["hello-world"],
1568
+ };
1569
+ `);
1570
+ await writeFile(resolve(root, "package.json"), `${JSON.stringify({
1571
+ name: `opencomputer-app-${manifest.id}`,
1572
+ version: "0.1.0",
1573
+ private: true,
1574
+ type: "module",
1575
+ scripts: {
1576
+ dev: "opencomputer dev",
1577
+ "dev:web": "vite",
1578
+ build: "tsc -b && vite build",
1579
+ session: "opencomputer session",
1580
+ deploy: "opencomputer deploy",
1581
+ },
1582
+ dependencies: {
1583
+ react: "^19.2.0",
1584
+ "react-dom": "^19.2.0",
1585
+ },
1586
+ devDependencies: {
1587
+ "@opencomputer/cli": "^0.4.1",
1588
+ "@opencode-ai/plugin": "^1.18.4",
1589
+ "@types/node": "^24.0.0",
1590
+ "@types/react": "^19.2.0",
1591
+ "@types/react-dom": "^19.2.0",
1592
+ "@vitejs/plugin-react": "^6.0.0",
1593
+ "opencode-ai": "1.18.4",
1594
+ typescript: "^5.9.0",
1595
+ vite: "^8.0.0",
1596
+ },
1597
+ }, null, 2)}\n`);
1598
+ await writeFile(resolve(root, "vite.config.ts"), `import { readFileSync } from "node:fs";
1599
+ import { resolve } from "node:path";
1600
+ import react from "@vitejs/plugin-react";
1601
+ import { defineConfig } from "vite";
1602
+
1603
+ function openComputerDev() {
1604
+ try {
1605
+ return JSON.parse(
1606
+ readFileSync(
1607
+ resolve("opencomputer/agents/hello-world/.opencomputer/dev.json"),
1608
+ "utf8",
1609
+ ),
1610
+ ) as { url: string; token: string; agent: string };
1611
+ } catch {
1612
+ throw new Error(
1613
+ "OpenComputer is not running. Start npm run dev in another terminal first.",
1614
+ );
1615
+ }
1616
+ }
1617
+
1618
+ function openComputerAgent() {
1619
+ const manifest = readFileSync(
1620
+ resolve("opencomputer/agents/hello-world/opencomputer.toml"),
1621
+ "utf8",
1622
+ );
1623
+ const id = manifest.match(/^id\\s*=\\s*"([^"]+)"/m)?.[1];
1624
+ if (!id) throw new Error("The hello-world agent manifest has no id.");
1625
+ return id;
1626
+ }
1627
+
1628
+ export default defineConfig(({ command }) => {
1629
+ const dev = command === "serve" ? openComputerDev() : undefined;
1630
+ return {
1631
+ plugins: [react()],
1632
+ define: {
1633
+ __OPENCOMPUTER_AGENT__: JSON.stringify(
1634
+ dev?.agent ?? \`\${openComputerAgent()}@production\`,
1635
+ ),
1636
+ },
1637
+ ...(dev ? { server: {
1638
+ proxy: {
1639
+ "/api/opencomputer": {
1640
+ target: dev.url,
1641
+ headers: { authorization: \`Bearer \${dev.token}\` },
1642
+ rewrite: (path) => path.replace(/^\\/api\\/opencomputer/, ""),
1643
+ },
1644
+ },
1645
+ } } : {}),
1646
+ };
1647
+ });
1648
+ `);
1649
+ await writeFile(resolve(root, "tsconfig.json"), `${JSON.stringify({
1650
+ compilerOptions: {
1651
+ target: "ES2022",
1652
+ useDefineForClassFields: true,
1653
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
1654
+ allowJs: false,
1655
+ skipLibCheck: true,
1656
+ esModuleInterop: true,
1657
+ allowSyntheticDefaultImports: true,
1658
+ strict: true,
1659
+ forceConsistentCasingInFileNames: true,
1660
+ module: "ESNext",
1661
+ moduleResolution: "Bundler",
1662
+ resolveJsonModule: true,
1663
+ isolatedModules: true,
1664
+ noEmit: true,
1665
+ jsx: "react-jsx",
1666
+ types: ["node", "vite/client"],
1667
+ },
1668
+ include: ["src", "vite.config.ts"],
1669
+ }, null, 2)}\n`);
1670
+ await writeFile(resolve(root, "index.html"), `<!doctype html>
1671
+ <html lang="en">
1672
+ <head>
1673
+ <meta charset="UTF-8" />
1674
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1675
+ <meta name="theme-color" content="#11110f" />
1676
+ <title>Hello World · OpenComputer</title>
1677
+ </head>
1678
+ <body>
1679
+ <div id="root"></div>
1680
+ <script type="module" src="/src/main.tsx"></script>
1681
+ </body>
1682
+ </html>
1683
+ `);
1684
+ await writeFile(resolve(root, "src", "use-agent.ts"), `import { useCallback, useRef, useState } from "react";
1685
+
1686
+ export interface AgentMessage {
1687
+ id: string;
1688
+ role: "user" | "assistant";
1689
+ text: string;
1690
+ }
1691
+
1692
+ interface AgentEvent {
1693
+ seq: number;
1694
+ type: string;
1695
+ data: Record<string, unknown>;
1696
+ }
1697
+
1698
+ declare const __OPENCOMPUTER_AGENT__: string;
1699
+ const AGENT = __OPENCOMPUTER_AGENT__;
1700
+
1701
+ async function request<T>(path: string, init?: RequestInit): Promise<T> {
1702
+ const response = await fetch(\`/api/opencomputer/managed-agents\${path}\`, {
1703
+ ...init,
1704
+ headers: { "content-type": "application/json", ...init?.headers },
1705
+ });
1706
+ const body = await response.json();
1707
+ if (!response.ok) {
1708
+ throw new Error(body?.error?.message ?? \`Agent request failed (\${response.status})\`);
1709
+ }
1710
+ return body as T;
1711
+ }
1712
+
1713
+ export function useAgent() {
1714
+ const [messages, setMessages] = useState<AgentMessage[]>([]);
1715
+ const [sessionId, setSessionId] = useState<string>();
1716
+ const sessionRef = useRef<string | undefined>(undefined);
1717
+ const cursorRef = useRef(0);
1718
+ const [isRunning, setIsRunning] = useState(false);
1719
+ const [error, setError] = useState<string>();
1720
+
1721
+ const send = useCallback(async (value: string) => {
1722
+ const prompt = value.trim();
1723
+ if (!prompt || isRunning) return;
1724
+ const userMessage: AgentMessage = {
1725
+ id: crypto.randomUUID(),
1726
+ role: "user",
1727
+ text: prompt,
1728
+ };
1729
+ const assistantId = crypto.randomUUID();
1730
+ setMessages((current) => [
1731
+ ...current,
1732
+ userMessage,
1733
+ { id: assistantId, role: "assistant", text: "" },
1734
+ ]);
1735
+ setIsRunning(true);
1736
+ setError(undefined);
1737
+ try {
1738
+ let activeSession = sessionRef.current;
1739
+ if (!activeSession) {
1740
+ const created = await request<{ session: { id: string } }>("/sessions", {
1741
+ method: "POST",
1742
+ body: JSON.stringify({ agentId: AGENT, source: "local-react" }),
1743
+ });
1744
+ activeSession = created.session.id;
1745
+ sessionRef.current = activeSession;
1746
+ setSessionId(activeSession);
1747
+ } else {
1748
+ await request(\`/sessions/\${encodeURIComponent(activeSession)}/resume\`, {
1749
+ method: "POST",
1750
+ });
1751
+ }
1752
+ let streamed = "";
1753
+ const waitFor = async (terminal: (event: AgentEvent) => boolean) => {
1754
+ for (;;) {
1755
+ const result = await request<{ events: AgentEvent[] }>(
1756
+ \`/sessions/\${encodeURIComponent(activeSession!)}/events?after=\${cursorRef.current}\`,
1757
+ );
1758
+ for (const event of result.events) {
1759
+ cursorRef.current = Math.max(cursorRef.current, event.seq);
1760
+ if (event.type === "message.delta") {
1761
+ streamed += String(event.data.text ?? "");
1762
+ setMessages((current) =>
1763
+ current.map((message) =>
1764
+ message.id === assistantId
1765
+ ? { ...message, text: streamed }
1766
+ : message,
1767
+ ),
1768
+ );
1769
+ } else if (event.type === "message.completed" && !streamed) {
1770
+ const text = String(event.data.text ?? "");
1771
+ setMessages((current) =>
1772
+ current.map((message) =>
1773
+ message.id === assistantId ? { ...message, text } : message,
1774
+ ),
1775
+ );
1776
+ }
1777
+ if (terminal(event)) return event;
1778
+ }
1779
+ await new Promise((done) => setTimeout(done, 500));
1780
+ }
1781
+ };
1782
+ await waitFor((event) => event.type === "runtime.connected");
1783
+ await request(\`/sessions/\${encodeURIComponent(activeSession)}/turns\`, {
1784
+ method: "POST",
1785
+ body: JSON.stringify({ input: prompt, idempotencyKey: crypto.randomUUID() }),
1786
+ });
1787
+ const completed = await waitFor((event) =>
1788
+ event.type === "turn.completed" || event.type === "turn.failed",
1789
+ );
1790
+ if (completed.type === "turn.failed") {
1791
+ throw new Error(String(completed.data.message ?? "Agent failed"));
1792
+ }
1793
+ await request(\`/sessions/\${encodeURIComponent(activeSession)}/suspend\`, {
1794
+ method: "POST",
1795
+ });
1796
+ } catch (cause) {
1797
+ const message = cause instanceof Error ? cause.message : String(cause);
1798
+ setError(message);
1799
+ setMessages((current) =>
1800
+ current.map((item) =>
1801
+ item.id === assistantId && !item.text
1802
+ ? { ...item, text: "I couldn't complete that request." }
1803
+ : item,
1804
+ ),
1805
+ );
1806
+ } finally {
1807
+ setIsRunning(false);
1808
+ }
1809
+ }, [isRunning]);
1810
+
1811
+ return { messages, send, isRunning, error };
1812
+ }
1813
+ `);
1814
+ await writeFile(resolve(root, "src", "App.tsx"), `import { FormEvent, useState } from "react";
1815
+ import { useAgent } from "./use-agent";
1816
+
1817
+ export default function App() {
1818
+ const [input, setInput] = useState("");
1819
+ const { messages, send, isRunning, error } = useAgent();
1820
+
1821
+ function submit(event: FormEvent) {
1822
+ event.preventDefault();
1823
+ const prompt = input;
1824
+ setInput("");
1825
+ void send(prompt);
1826
+ }
1827
+
1828
+ return (
1829
+ <main>
1830
+ <section className="hero">
1831
+ <span className="eyebrow">OpenComputer</span>
1832
+ <h1>Hello, world.</h1>
1833
+ <p>Your first agent is live. The React app stays local while agent code syncs to Development (Cloud).</p>
1834
+ </section>
1835
+
1836
+ <section className="chat" aria-label="Agent conversation">
1837
+ <div className="messages">
1838
+ {messages.length === 0 ? (
1839
+ <button className="suggestion" onClick={() => void send("Say hello and tell me what you can do.")}>
1840
+ Say hello and tell me what you can do →
1841
+ </button>
1842
+ ) : (
1843
+ messages.map((message) => (
1844
+ <article key={message.id} className={message.role}>
1845
+ <strong>{message.role === "user" ? "You" : "Agent"}</strong>
1846
+ <p>{message.text || "Thinking…"}</p>
1847
+ </article>
1848
+ ))
1849
+ )}
1850
+ </div>
1851
+ {error ? <p className="error">{error}</p> : null}
1852
+ <form onSubmit={submit}>
1853
+ <input
1854
+ value={input}
1855
+ onChange={(event) => setInput(event.target.value)}
1856
+ placeholder="Message the hello-world agent…"
1857
+ aria-label="Message"
1858
+ />
1859
+ <button disabled={isRunning || !input.trim()}>
1860
+ {isRunning ? "Running…" : "Send"}
1861
+ </button>
1862
+ </form>
1863
+ </section>
1864
+ </main>
1865
+ );
1866
+ }
1867
+ `);
1868
+ await writeFile(resolve(root, "src", "main.tsx"), `import { StrictMode } from "react";
1869
+ import { createRoot } from "react-dom/client";
1870
+ import App from "./App";
1871
+ import "./styles.css";
1872
+
1873
+ createRoot(document.getElementById("root")!).render(
1874
+ <StrictMode>
1875
+ <App />
1876
+ </StrictMode>,
1877
+ );
1878
+ `);
1879
+ await writeFile(resolve(root, "src", "styles.css"), `@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=DM+Serif+Display&display=swap");
1880
+
1881
+ :root { color: #1d1c19; background: #f4f1e9; font-family: "DM Sans", sans-serif; }
1882
+ * { box-sizing: border-box; }
1883
+ body { margin: 0; min-width: 320px; min-height: 100vh; }
1884
+ button, input { font: inherit; }
1885
+ main { width: min(760px, calc(100% - 32px)); margin: 0 auto; padding: 12vh 0 48px; }
1886
+ .hero { margin-bottom: 36px; }
1887
+ .eyebrow { color: #706b60; font-size: 12px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; }
1888
+ h1 { margin: 10px 0; font: 400 clamp(48px, 10vw, 84px)/.95 "DM Serif Display", serif; }
1889
+ .hero p { max-width: 580px; color: #625e55; font-size: 18px; line-height: 1.6; }
1890
+ .chat { overflow: hidden; border: 1px solid #d9d4c8; border-radius: 18px; background: rgba(255,255,255,.72); box-shadow: 0 18px 60px rgba(56,48,34,.08); }
1891
+ .messages { display: grid; gap: 14px; min-height: 260px; max-height: 52vh; overflow-y: auto; padding: 24px; }
1892
+ article { max-width: 82%; border-radius: 14px; padding: 12px 14px; }
1893
+ article strong { display: block; margin-bottom: 4px; font-size: 12px; color: #777166; }
1894
+ article p { margin: 0; line-height: 1.55; white-space: pre-wrap; }
1895
+ article.user { justify-self: end; background: #1d1c19; color: white; }
1896
+ article.user strong { color: #bdb7ab; }
1897
+ article.assistant { background: #ece8de; }
1898
+ .suggestion { align-self: center; justify-self: center; border: 1px solid #d9d4c8; border-radius: 999px; background: transparent; padding: 10px 16px; cursor: pointer; }
1899
+ .suggestion:hover { background: #ece8de; }
1900
+ .error { margin: 0 24px 12px; color: #a33a2b; font-size: 14px; }
1901
+ form { display: flex; gap: 10px; border-top: 1px solid #ddd8cc; padding: 14px; background: white; }
1902
+ input { min-width: 0; flex: 1; border: 0; outline: 0; padding: 10px; background: transparent; }
1903
+ form button { border: 0; border-radius: 10px; background: #d85b35; color: white; padding: 10px 18px; font-weight: 600; cursor: pointer; }
1904
+ form button:disabled { cursor: default; opacity: .45; }
1905
+ `);
1906
+ await writeFile(resolve(root, "README.md"), `# Hello World OpenComputer app
1907
+
1908
+ This project keeps agent definitions in \`opencomputer/\` and the React app in
1909
+ \`src/\`.
1910
+
1911
+ Sync agent code to Development (Cloud) in one terminal:
1912
+
1913
+ \`\`\`bash
1914
+ npm run dev
1915
+ \`\`\`
1916
+
1917
+ The first run asks you to create a cloud project or select an existing one.
1918
+ That choice is saved for later development runs.
1919
+
1920
+ Then start the React app in another:
1921
+
1922
+ \`\`\`bash
1923
+ npm run dev:web
1924
+ \`\`\`
1925
+ `);
1926
+ const appFiles = [
1927
+ "package.json",
1928
+ "vite.config.ts",
1929
+ "tsconfig.json",
1930
+ "index.html",
1931
+ "README.md",
1932
+ ".gitignore",
1933
+ "src/App.tsx",
1934
+ "src/main.tsx",
1935
+ "src/styles.css",
1936
+ "src/use-agent.ts",
1937
+ ];
1938
+ return {
1939
+ root,
1940
+ agentRoot,
1941
+ manifest,
1942
+ files: [
1943
+ "opencomputer/project.ts",
1944
+ ...initialized.files
1945
+ .filter((path) => path !== "package.json" && path !== ".gitignore")
1946
+ .map((path) => `opencomputer/agents/hello-world/${path}`),
1947
+ ...appFiles,
1948
+ ],
1949
+ };
1950
+ }
1440
1951
  export async function prepareAgent(root) {
1441
1952
  const runtime = resolve(root, ".opencomputer", "runtime");
1442
1953
  await rm(runtime, { recursive: true, force: true });
1443
1954
  await mkdir(runtime, { recursive: true });
1955
+ const agentSource = await readFile(resolve(root, "agent.ts"), "utf8");
1956
+ const reactive = /export\s+default\s+(?:async\s+)?function\b/.test(agentSource);
1957
+ const legacyInstructions = reactive
1958
+ ? ""
1959
+ : await readFile(resolve(root, "instructions.md"), "utf8");
1444
1960
  await writeFile(resolve(runtime, "AGENTS.md"), `# OpenComputer runtime identity
1445
1961
 
1446
1962
  You are an OpenComputer agent. OpenCode is an internal execution detail, not
@@ -1460,9 +1976,7 @@ the product or support surface presented to users.
1460
1976
  - If a connection tool fails, report its exact error. Do not invent alternate
1461
1977
  controls or third-party support instructions.
1462
1978
 
1463
- # Agent instructions
1464
-
1465
- ${await readFile(resolve(root, "instructions.md"), "utf8")}`);
1979
+ ${legacyInstructions ? `# Agent instructions\n\n${legacyInstructions}` : ""}`);
1466
1980
  const openCodeConfig = resolve(root, "opencode.json");
1467
1981
  if (await exists(openCodeConfig)) {
1468
1982
  const parsed = JSON.parse(await readFile(openCodeConfig, "utf8"));
@@ -1509,6 +2023,48 @@ ${await readFile(resolve(root, "instructions.md"), "utf8")}`);
1509
2023
  if (await exists(workspace)) {
1510
2024
  await cp(workspace, runtime, { recursive: true });
1511
2025
  }
2026
+ if (reactive) {
2027
+ await writeFile(resolve(runtime, "package.json"), `${JSON.stringify({ private: true, type: "module" }, null, 2)}\n`);
2028
+ const transpile = (source, filename) => ts.transpileModule(source, {
2029
+ fileName: filename,
2030
+ compilerOptions: {
2031
+ target: ts.ScriptTarget.ES2022,
2032
+ module: ts.ModuleKind.ESNext,
2033
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
2034
+ },
2035
+ reportDiagnostics: true,
2036
+ });
2037
+ const compiledAgent = transpile(agentSource, "agent.ts");
2038
+ const diagnostics = compiledAgent.diagnostics ?? [];
2039
+ if (diagnostics.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) {
2040
+ throw new Error(`agent.ts could not be compiled: ${diagnostics
2041
+ .map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " "))
2042
+ .join("; ")}`);
2043
+ }
2044
+ await writeFile(resolve(runtime, "agent.js"), compiledAgent.outputText);
2045
+ const hookSource = await readFile(resolve(root, "opencomputer.ts"), "utf8");
2046
+ await writeFile(resolve(runtime, "opencomputer.js"), transpile(hookSource, "opencomputer.ts").outputText);
2047
+ const toolEntries = await readdir(resolve(runtime, ".opencode", "tools"), {
2048
+ withFileTypes: true,
2049
+ });
2050
+ const reactiveTools = [];
2051
+ for (const entry of toolEntries.filter((candidate) => candidate.isFile())) {
2052
+ const stem = entry.name
2053
+ .replace(/\.[^.]+$/, "")
2054
+ .replace(/[^a-zA-Z0-9_]+/g, "_");
2055
+ const source = await readFile(resolve(runtime, ".opencode", "tools", entry.name), "utf8");
2056
+ for (const match of source.matchAll(/export\s+const\s+([a-zA-Z0-9_]+)\s*=/g)) {
2057
+ reactiveTools.push(`${stem}_${match[1]}`);
2058
+ }
2059
+ }
2060
+ await mkdir(resolve(runtime, ".opencomputer"), { recursive: true });
2061
+ await writeFile(resolve(runtime, ".opencomputer", "reactive.json"), `${JSON.stringify({
2062
+ version: 1,
2063
+ entry: "../agent.js",
2064
+ tools: [...new Set(reactiveTools)].sort(),
2065
+ subagents: [],
2066
+ }, null, 2)}\n`);
2067
+ }
1512
2068
  return runtime;
1513
2069
  }
1514
2070
  async function collectNames(root, type) {
@@ -1565,7 +2121,8 @@ async function validateTemplateRequirements(root, manifest) {
1565
2121
  catch {
1566
2122
  // Report one actionable error below for an incomplete PTO project.
1567
2123
  }
1568
- if (!(await exists(resolve(root, "tools", "calendar.ts"))) || !calendarDeclared) {
2124
+ if (!(await exists(resolve(root, "tools", "calendar.ts"))) ||
2125
+ !calendarDeclared) {
1569
2126
  throw new Error("PTO calendar tools are missing. Run `opencomputer tools add calendar` before deploying.");
1570
2127
  }
1571
2128
  }