@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/README.md +25 -97
- package/dist/api.d.ts +18 -0
- package/dist/api.js +10 -0
- package/dist/api.js.map +1 -1
- package/dist/auth.js +5 -2
- package/dist/auth.js.map +1 -1
- package/dist/binding.d.ts +16 -0
- package/dist/binding.js +125 -0
- package/dist/binding.js.map +1 -0
- package/dist/binding.test.d.ts +1 -0
- package/dist/binding.test.js +68 -0
- package/dist/binding.test.js.map +1 -0
- package/dist/commands.js +30 -32
- package/dist/commands.js.map +1 -1
- package/dist/config.js +10 -5
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +19 -1
- package/dist/config.test.js.map +1 -1
- package/dist/dev.d.ts +8 -0
- package/dist/dev.js +112 -0
- package/dist/dev.js.map +1 -0
- package/dist/dev.test.d.ts +1 -0
- package/dist/dev.test.js +38 -0
- package/dist/dev.test.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/local.js +26 -47
- package/dist/local.js.map +1 -1
- package/dist/local.test.js +29 -0
- package/dist/local.test.js.map +1 -1
- package/dist/project.d.ts +12 -1
- package/dist/project.js +574 -17
- package/dist/project.js.map +1 -1
- package/dist/project.test.js +44 -8
- package/dist/project.test.js.map +1 -1
- package/dist/slack.js +2 -6
- package/dist/slack.js.map +1 -1
- package/dist/slack.test.js +4 -2
- package/dist/slack.test.js.map +1 -1
- package/package.json +3 -3
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
|
-
"
|
|
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 = [
|
|
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, "
|
|
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
|
|
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"),
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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"))) ||
|
|
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
|
}
|