@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/README.md +12 -5
- package/dist/auth.js +5 -2
- package/dist/auth.js.map +1 -1
- package/dist/binding.d.ts +16 -0
- package/dist/binding.js +115 -0
- package/dist/binding.js.map +1 -0
- package/dist/binding.test.d.ts +1 -0
- package/dist/binding.test.js +70 -0
- package/dist/binding.test.js.map +1 -0
- package/dist/commands.js +12 -19
- 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 +1 -1
- package/dist/local.js +9 -0
- 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 +1 -1
- package/dist/project.js +281 -67
- package/dist/project.js.map +1 -1
- package/dist/project.test.js +107 -16
- package/dist/project.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",
|
|
@@ -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, "
|
|
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, "
|
|
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, "
|
|
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"),
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
"
|
|
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/
|
|
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
|
-
|
|
1564
|
+
await initializeTemplateAgentProject(HELLO_WORLD_TEMPLATE, agentRoot);
|
|
1512
1565
|
const manifest = {
|
|
1513
|
-
|
|
1514
|
-
|
|
1566
|
+
schema: 1,
|
|
1567
|
+
id: project?.agentId ?? "hello-world",
|
|
1515
1568
|
name: "Hello World",
|
|
1569
|
+
template: "hello-world",
|
|
1516
1570
|
};
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1664
|
-
|
|
1665
|
-
: "/
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|