@opencomputer/cli 0.4.1 → 0.4.3

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
@@ -321,7 +321,7 @@ ${template.suggestedPrompts.map((prompt) => `- ${prompt}`).join("\n")}
321
321
  `;
322
322
  }
323
323
  function gmailToolSource() {
324
- return `import { tool } from "@opencode-ai/plugin";
324
+ return `import { tool } from "@opencomputer/agent";
325
325
 
326
326
  async function gmail(input: {
327
327
  path: string;
@@ -365,28 +365,40 @@ async function gmail(input: {
365
365
  }
366
366
 
367
367
  export const search = tool({
368
+ id: "gmail_search",
368
369
  description:
369
370
  "Read-only: search Gmail messages using a Gmail search query. Use this before reading individual messages.",
370
- args: {
371
- query: tool.schema.string(),
372
- maxResults: tool.schema.number().min(1).max(25).default(10),
373
- connection: tool.schema.string().optional(),
371
+ input: {
372
+ type: "object",
373
+ properties: {
374
+ query: { type: "string" },
375
+ maxResults: { type: "integer", minimum: 1, maximum: 25, default: 10 },
376
+ connection: { type: "string" },
377
+ },
378
+ required: ["query"],
379
+ additionalProperties: false,
374
380
  },
375
381
  async execute(args) {
376
382
  const query = encodeURIComponent(args.query);
377
383
  return JSON.stringify(await gmail({
378
- path: \`/gmail/v1/users/me/messages?q=\${query}&maxResults=\${args.maxResults}\`,
384
+ path: \`/gmail/v1/users/me/messages?q=\${query}&maxResults=\${args.maxResults ?? 10}\`,
379
385
  connection: args.connection,
380
386
  }));
381
387
  },
382
388
  });
383
389
 
384
390
  export const read = tool({
391
+ id: "gmail_read",
385
392
  description:
386
393
  "Read-only: get a Gmail message's sender, recipients, subject, date, labels, and snippet. Use this for inbox triage after gmail_search.",
387
- args: {
388
- messageId: tool.schema.string(),
389
- connection: tool.schema.string().optional(),
394
+ input: {
395
+ type: "object",
396
+ properties: {
397
+ messageId: { type: "string" },
398
+ connection: { type: "string" },
399
+ },
400
+ required: ["messageId"],
401
+ additionalProperties: false,
390
402
  },
391
403
  async execute(args) {
392
404
  return JSON.stringify(await gmail({
@@ -404,11 +416,17 @@ export const read = tool({
404
416
  });
405
417
 
406
418
  export const read_full = tool({
419
+ id: "gmail_read_full",
407
420
  description:
408
421
  "Read-only: get the complete Gmail message body. Use only when gmail_read metadata and snippet are insufficient.",
409
- args: {
410
- messageId: tool.schema.string(),
411
- connection: tool.schema.string().optional(),
422
+ input: {
423
+ type: "object",
424
+ properties: {
425
+ messageId: { type: "string" },
426
+ connection: { type: "string" },
427
+ },
428
+ required: ["messageId"],
429
+ additionalProperties: false,
412
430
  },
413
431
  async execute(args) {
414
432
  return JSON.stringify(await gmail({
@@ -419,21 +437,27 @@ export const read_full = tool({
419
437
  });
420
438
 
421
439
  export const modify = tool({
440
+ id: "gmail_modify",
422
441
  description:
423
442
  "Consequential: add or remove Gmail labels only after the user explicitly confirms the exact change.",
424
- args: {
425
- messageId: tool.schema.string(),
426
- addLabelIds: tool.schema.array(tool.schema.string()).default([]),
427
- removeLabelIds: tool.schema.array(tool.schema.string()).default([]),
428
- connection: tool.schema.string().optional(),
443
+ input: {
444
+ type: "object",
445
+ properties: {
446
+ messageId: { type: "string" },
447
+ addLabelIds: { type: "array", items: { type: "string" }, default: [] },
448
+ removeLabelIds: { type: "array", items: { type: "string" }, default: [] },
449
+ connection: { type: "string" },
450
+ },
451
+ required: ["messageId"],
452
+ additionalProperties: false,
429
453
  },
430
454
  async execute(args) {
431
455
  return JSON.stringify(await gmail({
432
456
  method: "POST",
433
457
  path: \`/gmail/v1/users/me/messages/\${encodeURIComponent(args.messageId)}/modify\`,
434
458
  body: {
435
- addLabelIds: args.addLabelIds,
436
- removeLabelIds: args.removeLabelIds,
459
+ addLabelIds: args.addLabelIds ?? [],
460
+ removeLabelIds: args.removeLabelIds ?? [],
437
461
  },
438
462
  connection: args.connection,
439
463
  }));
@@ -441,13 +465,19 @@ export const modify = tool({
441
465
  });
442
466
 
443
467
  export const send = tool({
468
+ id: "gmail_send",
444
469
  description:
445
470
  "Consequential: send an email only after the user reviews the full draft and explicitly confirms this exact send.",
446
- args: {
447
- to: tool.schema.string(),
448
- subject: tool.schema.string(),
449
- body: tool.schema.string(),
450
- connection: tool.schema.string().optional(),
471
+ input: {
472
+ type: "object",
473
+ properties: {
474
+ to: { type: "string" },
475
+ subject: { type: "string" },
476
+ body: { type: "string" },
477
+ connection: { type: "string" },
478
+ },
479
+ required: ["to", "subject", "body"],
480
+ additionalProperties: false,
451
481
  },
452
482
  async execute(args) {
453
483
  if (/[\\r\\n]/.test(args.to) || /[\\r\\n]/.test(args.subject)) {
@@ -471,7 +501,7 @@ export const send = tool({
471
501
  `;
472
502
  }
473
503
  function calendarToolSource() {
474
- return `import { tool } from "@opencode-ai/plugin";
504
+ return `import { tool } from "@opencomputer/agent";
475
505
 
476
506
  async function calendar(input: {
477
507
  path: string;
@@ -545,10 +575,13 @@ function nextDate(value: string): string {
545
575
  }
546
576
 
547
577
  export const list = tool({
578
+ id: "calendar_list",
548
579
  description:
549
580
  "Read-only: list the Google Calendars available through the selected connection.",
550
- args: {
551
- connection: tool.schema.string().optional(),
581
+ input: {
582
+ type: "object",
583
+ properties: { connection: { type: "string" } },
584
+ additionalProperties: false,
552
585
  },
553
586
  async execute(args) {
554
587
  return JSON.stringify(await calendar({
@@ -559,14 +592,20 @@ export const list = tool({
559
592
  });
560
593
 
561
594
  export const events = tool({
595
+ id: "calendar_events",
562
596
  description:
563
597
  "Read-only: list events in an exact time range before preparing PTO or identifying conflicts.",
564
- args: {
565
- calendarId: tool.schema.string().default("primary"),
566
- timeMin: tool.schema.string().describe("Inclusive RFC3339 start timestamp"),
567
- timeMax: tool.schema.string().describe("Exclusive RFC3339 end timestamp"),
568
- query: tool.schema.string().optional(),
569
- connection: tool.schema.string().optional(),
598
+ input: {
599
+ type: "object",
600
+ properties: {
601
+ calendarId: { type: "string", default: "primary" },
602
+ timeMin: { type: "string", description: "Inclusive RFC3339 start timestamp" },
603
+ timeMax: { type: "string", description: "Exclusive RFC3339 end timestamp" },
604
+ query: { type: "string" },
605
+ connection: { type: "string" },
606
+ },
607
+ required: ["timeMin", "timeMax"],
608
+ additionalProperties: false,
570
609
  },
571
610
  async execute(args) {
572
611
  const calendarId = args.calendarId || "primary";
@@ -588,14 +627,25 @@ export const events = tool({
588
627
  });
589
628
 
590
629
  export const freebusy = tool({
630
+ id: "calendar_freebusy",
591
631
  description:
592
632
  "Read-only: check busy periods for one or more calendars in an exact RFC3339 time range.",
593
- args: {
594
- calendarIds: tool.schema.array(tool.schema.string()).min(1).default(["primary"]),
595
- timeMin: tool.schema.string(),
596
- timeMax: tool.schema.string(),
597
- timeZone: tool.schema.string().optional(),
598
- connection: tool.schema.string().optional(),
633
+ input: {
634
+ type: "object",
635
+ properties: {
636
+ calendarIds: {
637
+ type: "array",
638
+ items: { type: "string" },
639
+ minItems: 1,
640
+ default: ["primary"],
641
+ },
642
+ timeMin: { type: "string" },
643
+ timeMax: { type: "string" },
644
+ timeZone: { type: "string" },
645
+ connection: { type: "string" },
646
+ },
647
+ required: ["timeMin", "timeMax"],
648
+ additionalProperties: false,
599
649
  },
600
650
  async execute(args) {
601
651
  const calendarIds = args.calendarIds?.length
@@ -616,16 +666,22 @@ export const freebusy = tool({
616
666
  });
617
667
 
618
668
  export const create_time_off = tool({
669
+ id: "calendar_create_time_off",
619
670
  description:
620
671
  "Consequential: create an all-day PTO event only after the user explicitly confirms the exact title, dates, calendar, and availability.",
621
- args: {
622
- calendarId: tool.schema.string().default("primary"),
623
- title: tool.schema.string().default("Out of office"),
624
- startDate: tool.schema.string().describe("First PTO day, YYYY-MM-DD"),
625
- endDate: tool.schema.string().describe("Last PTO day, inclusive, YYYY-MM-DD"),
626
- description: tool.schema.string().optional(),
627
- availability: tool.schema.enum(["busy", "free"]).default("busy"),
628
- connection: tool.schema.string().optional(),
672
+ input: {
673
+ type: "object",
674
+ properties: {
675
+ calendarId: { type: "string", default: "primary" },
676
+ title: { type: "string", default: "Out of office" },
677
+ startDate: { type: "string", description: "First PTO day, YYYY-MM-DD" },
678
+ endDate: { type: "string", description: "Last PTO day, inclusive, YYYY-MM-DD" },
679
+ description: { type: "string" },
680
+ availability: { type: "string", enum: ["busy", "free"], default: "busy" },
681
+ connection: { type: "string" },
682
+ },
683
+ required: ["startDate", "endDate"],
684
+ additionalProperties: false,
629
685
  },
630
686
  async execute(args) {
631
687
  const calendarId = args.calendarId || "primary";
@@ -638,7 +694,7 @@ export const create_time_off = tool({
638
694
  method: "POST",
639
695
  path: \`/calendars/\${encodeURIComponent(calendarId)}/events\`,
640
696
  body: {
641
- summary: args.title,
697
+ summary: args.title || "Out of office",
642
698
  description: args.description,
643
699
  start: { date: startDate },
644
700
  end: { date: nextDate(endDate) },
@@ -653,7 +709,7 @@ export const create_time_off = tool({
653
709
  function githubReviewToolSource() {
654
710
  return `import { mkdir, writeFile } from "node:fs/promises";
655
711
  import { resolve, sep } from "node:path";
656
- import { tool } from "@opencode-ai/plugin";
712
+ import { tool } from "@opencomputer/agent";
657
713
 
658
714
  const MAX_PAGES = 10;
659
715
  const MAX_CHECKOUT_FILES = 100;
@@ -744,10 +800,14 @@ async function githubPages(path: string): Promise<{
744
800
  }
745
801
 
746
802
  export const pr_context = tool({
803
+ id: "github_pr_context",
747
804
  description:
748
805
  "Read-only: fetch an accessible GitHub PR, all available issue comments, reviews, inline review comments, changed files, and the full available diff through the connection broker.",
749
- args: {
750
- pullRequestUrl: tool.schema.string(),
806
+ input: {
807
+ type: "object",
808
+ properties: { pullRequestUrl: { type: "string", format: "uri" } },
809
+ required: ["pullRequestUrl"],
810
+ additionalProperties: false,
751
811
  },
752
812
  async execute(args) {
753
813
  const { repository, number } = parsePullRequestUrl(args.pullRequestUrl);
@@ -813,12 +873,16 @@ export const pr_context = tool({
813
873
  });
814
874
 
815
875
  export const checkout = tool({
876
+ id: "github_checkout",
816
877
  description:
817
878
  "Read-only: materialize changed files plus relevant repository instructions and manifests from the exact PR head into a bounded session-workspace directory, without downloading the whole repository or creating a Git remote. Use the destination returned by this tool for subsequent file reads.",
818
- args: {
819
- pullRequestUrl: tool.schema.string(),
879
+ input: {
880
+ type: "object",
881
+ properties: { pullRequestUrl: { type: "string", format: "uri" } },
882
+ required: ["pullRequestUrl"],
883
+ additionalProperties: false,
820
884
  },
821
- async execute(args, context) {
885
+ async execute(args) {
822
886
  const { repository, number } = parsePullRequestUrl(args.pullRequestUrl);
823
887
  const pull = await githubJson(
824
888
  "/repos/" + repository + "/pulls/" + String(number),
@@ -830,7 +894,7 @@ export const checkout = tool({
830
894
  if (typeof head.sha !== "string" || !/^[a-f0-9]{40}$/i.test(head.sha)) {
831
895
  throw new Error("GitHub did not return a valid PR head SHA");
832
896
  }
833
- const workspace = resolve(context.directory);
897
+ const workspace = resolve(process.cwd());
834
898
  const destinationName =
835
899
  "github-pr-" + number + "-" + head.sha.slice(0, 12).toLowerCase();
836
900
  const destination = resolve(workspace, destinationName);
@@ -936,7 +1000,7 @@ export const checkout = tool({
936
1000
  `;
937
1001
  }
938
1002
  function connectionControlToolSource() {
939
- return `import { tool } from "@opencode-ai/plugin";
1003
+ return `import { tool } from "@opencomputer/agent";
940
1004
 
941
1005
  async function connectionControl(
942
1006
  method: "GET" | "POST",
@@ -996,21 +1060,35 @@ async function connectionControl(
996
1060
  }
997
1061
 
998
1062
  export const list = tool({
1063
+ id: "opencomputer_connections_list",
999
1064
  description:
1000
1065
  "List the connected accounts available to the current session identity. Use this to discover connection providers and aliases without exposing credentials.",
1001
- args: {},
1066
+ input: {
1067
+ type: "object",
1068
+ properties: {},
1069
+ additionalProperties: false,
1070
+ },
1002
1071
  async execute() {
1003
1072
  return JSON.stringify(await connectionControl("GET"));
1004
1073
  },
1005
1074
  });
1006
1075
 
1007
1076
  export const request = tool({
1077
+ id: "opencomputer_connections_request",
1008
1078
  description:
1009
1079
  "Ask the current user to connect an account. Use gmail for an email account. Set newAccount=true when the user asks for another account of the same service. In a messaging channel OpenComputer privately sends the authorization link to that user; otherwise the result includes the link.",
1010
- args: {
1011
- service: tool.schema.enum(["gmail", "calendar", "drive", "sheets", "github"]),
1012
- label: tool.schema.string().optional(),
1013
- newAccount: tool.schema.boolean().optional(),
1080
+ input: {
1081
+ type: "object",
1082
+ properties: {
1083
+ service: {
1084
+ type: "string",
1085
+ enum: ["gmail", "calendar", "drive", "sheets", "github"],
1086
+ },
1087
+ label: { type: "string" },
1088
+ newAccount: { type: "boolean" },
1089
+ },
1090
+ required: ["service"],
1091
+ additionalProperties: false,
1014
1092
  },
1015
1093
  async execute(args) {
1016
1094
  return JSON.stringify(await connectionControl("POST", args));
@@ -1046,6 +1124,18 @@ function tomlString(source, key) {
1046
1124
  }
1047
1125
  }
1048
1126
  export async function readManifest(root) {
1127
+ if (!(await exists(resolve(root, "opencomputer.toml")))) {
1128
+ const id = agentIdFromName(basename(root));
1129
+ return {
1130
+ schema: 1,
1131
+ id,
1132
+ name: id
1133
+ .split("-")
1134
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
1135
+ .join(" "),
1136
+ template: "hello-world",
1137
+ };
1138
+ }
1049
1139
  const source = await readFile(resolve(root, "opencomputer.toml"), "utf8");
1050
1140
  const schema = Number(source.match(/^\s*schema\s*=\s*(\d+)\s*$/m)?.[1]);
1051
1141
  const id = tomlString(source, "id");
@@ -1065,12 +1155,10 @@ export async function findAgentRoot(startDirectory = process.cwd()) {
1065
1155
  let directory = resolve(startDirectory);
1066
1156
  for (;;) {
1067
1157
  const nested = resolve(directory, "opencomputer");
1068
- if ((await exists(resolve(directory, "opencomputer.toml"))) &&
1069
- (await exists(resolve(directory, "agent.ts")))) {
1158
+ if ((await exists(resolve(directory, "agent.ts")))) {
1070
1159
  return directory;
1071
1160
  }
1072
- if ((await exists(resolve(nested, "opencomputer.toml"))) &&
1073
- (await exists(resolve(nested, "agent.ts")))) {
1161
+ if ((await exists(resolve(nested, "agent.ts")))) {
1074
1162
  return nested;
1075
1163
  }
1076
1164
  for (const agentsDirectory of [
@@ -1086,8 +1174,7 @@ export async function findAgentRoot(startDirectory = process.cwd()) {
1086
1174
  if (!entry.isDirectory())
1087
1175
  continue;
1088
1176
  const agent = resolve(agentsDirectory, entry.name);
1089
- if ((await exists(resolve(agent, "opencomputer.toml"))) &&
1090
- (await exists(resolve(agent, "agent.ts")))) {
1177
+ if ((await exists(resolve(agent, "agent.ts")))) {
1091
1178
  detected.push(agent);
1092
1179
  }
1093
1180
  }
@@ -1302,6 +1389,7 @@ export function useSessionData<T extends SessionDataValue>(key: string): T | und
1302
1389
  "opencode.json",
1303
1390
  "package.json",
1304
1391
  ".gitignore",
1392
+ "README.md",
1305
1393
  "agent.ts",
1306
1394
  "opencomputer.ts",
1307
1395
  "workspace/README.md",
@@ -1531,7 +1619,7 @@ export async function assertStarterTarget(directory) {
1531
1619
  }
1532
1620
  const reserved = [
1533
1621
  "opencomputer/project.ts",
1534
- "opencomputer/agents/hello-world/opencomputer.toml",
1622
+ "opencomputer/agents/hello-world/agent.ts",
1535
1623
  "package.json",
1536
1624
  "vite.config.ts",
1537
1625
  "index.html",
@@ -1551,19 +1639,45 @@ export async function initializeAgentProject(directory, project) {
1551
1639
  const root = resolve(directory);
1552
1640
  const agentRoot = resolve(root, "opencomputer", "agents", "hello-world");
1553
1641
  await assertStarterTarget(root);
1554
- const initialized = await initializeTemplateAgentProject(HELLO_WORLD_TEMPLATE, agentRoot);
1642
+ await initializeTemplateAgentProject(HELLO_WORLD_TEMPLATE, agentRoot);
1555
1643
  const manifest = {
1556
- ...initialized.manifest,
1557
- ...(project ? { id: project.agentId } : {}),
1644
+ schema: 1,
1645
+ id: project?.agentId ?? "hello-world",
1558
1646
  name: "Hello World",
1647
+ template: "hello-world",
1559
1648
  };
1560
- await writeManifest(agentRoot, manifest);
1561
- await rm(resolve(agentRoot, "package.json"), { force: true });
1562
- await rm(resolve(agentRoot, ".gitignore"), { force: true });
1649
+ for (const path of [
1650
+ "opencomputer.toml",
1651
+ "opencomputer.config.ts",
1652
+ "opencomputer.ts",
1653
+ "opencode.json",
1654
+ "package.json",
1655
+ ".gitignore",
1656
+ "README.md",
1657
+ "tools",
1658
+ "connections",
1659
+ "skills",
1660
+ "channels",
1661
+ "workspace",
1662
+ "evals",
1663
+ ]) {
1664
+ await rm(resolve(agentRoot, path), { recursive: true, force: true });
1665
+ }
1666
+ await writeFile(resolve(agentRoot, "agent.ts"), `import { useInput, useModel } from "@opencomputer/agent";
1667
+
1668
+ export default function Agent() {
1669
+ const input = useInput();
1670
+ useModel("anthropic/claude-sonnet-4.6");
1671
+
1672
+ return input.text
1673
+ ? "You are a helpful OpenComputer agent. Respond directly to: " + input.text
1674
+ : "You are a helpful OpenComputer agent.";
1675
+ }
1676
+ `);
1563
1677
  await updateGitignore(root);
1564
1678
  await mkdir(resolve(root, "src"), { recursive: true });
1565
1679
  await writeFile(resolve(root, "opencomputer", "project.ts"), `export default {
1566
- ${project ? ` id: ${JSON.stringify(project.id)},\n` : ""} name: ${JSON.stringify(project?.name ?? basename(root))},
1680
+ name: ${JSON.stringify(project?.name ?? basename(root))},
1567
1681
  agents: ["hello-world"],
1568
1682
  };
1569
1683
  `);
@@ -1580,17 +1694,16 @@ ${project ? ` id: ${JSON.stringify(project.id)},\n` : ""} name: ${JSON.stringi
1580
1694
  deploy: "opencomputer deploy",
1581
1695
  },
1582
1696
  dependencies: {
1697
+ "@opencomputer/agent": "^0.2.0",
1583
1698
  react: "^19.2.0",
1584
1699
  "react-dom": "^19.2.0",
1585
1700
  },
1586
1701
  devDependencies: {
1587
- "@opencomputer/cli": "^0.4.1",
1588
- "@opencode-ai/plugin": "^1.18.4",
1702
+ "@opencomputer/cli": "^0.4.3",
1589
1703
  "@types/node": "^24.0.0",
1590
1704
  "@types/react": "^19.2.0",
1591
1705
  "@types/react-dom": "^19.2.0",
1592
1706
  "@vitejs/plugin-react": "^6.0.0",
1593
- "opencode-ai": "1.18.4",
1594
1707
  typescript: "^5.9.0",
1595
1708
  vite: "^8.0.0",
1596
1709
  },
@@ -1616,13 +1729,17 @@ function openComputerDev() {
1616
1729
  }
1617
1730
 
1618
1731
  function openComputerAgent() {
1619
- const manifest = readFileSync(
1620
- resolve("opencomputer/agents/hello-world/opencomputer.toml"),
1621
- "utf8",
1732
+ try {
1733
+ const binding = JSON.parse(
1734
+ readFileSync(resolve(".opencomputer/project.json"), "utf8"),
1735
+ ) as { agentId?: string };
1736
+ if (binding.agentId) return binding.agentId;
1737
+ } catch {
1738
+ // The production build below reports the actionable binding error.
1739
+ }
1740
+ throw new Error(
1741
+ "This app is not connected to an OpenComputer project. Run npm run dev first.",
1622
1742
  );
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
1743
  }
1627
1744
 
1628
1745
  export default defineConfig(({ command }) => {
@@ -1941,22 +2058,64 @@ npm run dev:web
1941
2058
  manifest,
1942
2059
  files: [
1943
2060
  "opencomputer/project.ts",
1944
- ...initialized.files
1945
- .filter((path) => path !== "package.json" && path !== ".gitignore")
1946
- .map((path) => `opencomputer/agents/hello-world/${path}`),
2061
+ "opencomputer/agents/hello-world/agent.ts",
1947
2062
  ...appFiles,
1948
2063
  ],
1949
2064
  };
1950
2065
  }
2066
+ function literalHookIds(source, hook) {
2067
+ const pattern = new RegExp(`\\b${hook}\\(\\s*["']([^"']+)["']`, "g");
2068
+ return [...source.matchAll(pattern)].map((match) => match[1]).sort();
2069
+ }
2070
+ function definedMcpServerIds(source) {
2071
+ return [...source.matchAll(/\bdefineMcpServer\s*\(\s*\{[\s\S]*?\bid\s*:\s*["']([^"']+)["'][\s\S]*?\}\s*\)/g)].map((match) => match[1]).sort();
2072
+ }
2073
+ function definedToolIds(source) {
2074
+ return [...source.matchAll(/\btool(?:<[^>]+>)?\s*\(\s*\{[\s\S]*?\bid\s*:\s*["']([^"']+)["'][\s\S]*?\}\s*\)/g)].map((match) => match[1]).sort();
2075
+ }
2076
+ function agentApiRuntimeSource() {
2077
+ return `function hooks() {
2078
+ const value = globalThis[Symbol.for("opencomputer.agent-hooks")];
2079
+ if (!value) throw new Error("OpenComputer hooks can only run while rendering an agent");
2080
+ return value;
2081
+ }
2082
+ function id(value, kind) {
2083
+ const normalized = String(value).trim();
2084
+ if (!normalized) throw new Error(kind + " requires a non-empty id");
2085
+ return normalized;
2086
+ }
2087
+ export const connection = (value) => Object.freeze({ kind: "connection", id: id(value, "connection") });
2088
+ export const defineMcpServer = (input) => {
2089
+ const url = new URL(input.url);
2090
+ if (url.protocol !== "https:") throw new Error("MCP server URLs must use HTTPS");
2091
+ return Object.freeze({ kind: "mcp", ...input, id: id(input.id, "defineMcpServer"), url: url.toString() });
2092
+ };
2093
+ export const tool = (input) => {
2094
+ const toolId = id(input.id, "tool");
2095
+ if (!/^[a-zA-Z0-9_-]+$/.test(toolId)) throw new Error("Invalid tool id " + JSON.stringify(toolId));
2096
+ if (!String(input.description).trim()) throw new Error("tool requires a non-empty description");
2097
+ if (!input.input || typeof input.input !== "object") throw new Error("tool requires a JSON Schema input object");
2098
+ return Object.freeze({ kind: "tool", version: 1, ...input, id: toolId });
2099
+ };
2100
+ export const useInput = () => hooks().useInput();
2101
+ export const useCurrentInput = useInput;
2102
+ export const useModel = (model) => hooks().useModel(model);
2103
+ export const useTool = (tool) => hooks().useTool(tool);
2104
+ export const useSubagent = (agent) => hooks().useSubagent(agent);
2105
+ export const useConnection = (value) => hooks().useConnection(value);
2106
+ export const useMcpServer = (server) => hooks().useMcpServer(server);
2107
+ export const useSessionData = (key) => hooks().useSessionData(key);
2108
+ `;
2109
+ }
1951
2110
  export async function prepareAgent(root) {
1952
2111
  const runtime = resolve(root, ".opencomputer", "runtime");
1953
2112
  await rm(runtime, { recursive: true, force: true });
1954
2113
  await mkdir(runtime, { recursive: true });
1955
2114
  const agentSource = await readFile(resolve(root, "agent.ts"), "utf8");
1956
2115
  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");
2116
+ if (!reactive) {
2117
+ throw new Error("agent.ts must default-export a synchronous agent function");
2118
+ }
1960
2119
  await writeFile(resolve(runtime, "AGENTS.md"), `# OpenComputer runtime identity
1961
2120
 
1962
2121
  You are an OpenComputer agent. OpenCode is an internal execution detail, not
@@ -1976,7 +2135,7 @@ the product or support surface presented to users.
1976
2135
  - If a connection tool fails, report its exact error. Do not invent alternate
1977
2136
  controls or third-party support instructions.
1978
2137
 
1979
- ${legacyInstructions ? `# Agent instructions\n\n${legacyInstructions}` : ""}`);
2138
+ `);
1980
2139
  const openCodeConfig = resolve(root, "opencode.json");
1981
2140
  if (await exists(openCodeConfig)) {
1982
2141
  const parsed = JSON.parse(await readFile(openCodeConfig, "utf8"));
@@ -2008,63 +2167,104 @@ ${legacyInstructions ? `# Agent instructions\n\n${legacyInstructions}` : ""}`);
2008
2167
  },
2009
2168
  }, null, 2)}\n`);
2010
2169
  }
2011
- for (const directory of ["skills", "tools"]) {
2012
- const source = resolve(root, directory);
2013
- if (await exists(source)) {
2014
- await mkdir(resolve(runtime, ".opencode"), { recursive: true });
2015
- await cp(source, resolve(runtime, ".opencode", directory), {
2016
- recursive: true,
2017
- });
2018
- }
2170
+ const skills = resolve(root, "skills");
2171
+ if (await exists(skills)) {
2172
+ await mkdir(resolve(runtime, ".opencode"), { recursive: true });
2173
+ await cp(skills, resolve(runtime, ".opencode", "skills"), {
2174
+ recursive: true,
2175
+ });
2019
2176
  }
2020
- await mkdir(resolve(runtime, ".opencode", "tools"), { recursive: true });
2021
- await writeFile(resolve(runtime, ".opencode", "tools", "opencomputer-connections.ts"), connectionControlToolSource());
2022
2177
  const workspace = resolve(root, "workspace");
2023
2178
  if (await exists(workspace)) {
2024
2179
  await cp(workspace, runtime, { recursive: true });
2025
2180
  }
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
2181
+ await writeFile(resolve(runtime, "package.json"), `${JSON.stringify({ private: true, type: "module" }, null, 2)}\n`);
2182
+ const transpile = (source, filename) => ts.transpileModule(source, {
2183
+ fileName: filename,
2184
+ compilerOptions: {
2185
+ target: ts.ScriptTarget.ES2022,
2186
+ module: ts.ModuleKind.ESNext,
2187
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
2188
+ },
2189
+ reportDiagnostics: true,
2190
+ });
2191
+ const compiledAgent = transpile(agentSource, "agent.ts");
2192
+ const diagnostics = compiledAgent.diagnostics ?? [];
2193
+ if (diagnostics.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) {
2194
+ throw new Error(`agent.ts could not be compiled: ${diagnostics
2195
+ .map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " "))
2196
+ .join("; ")}`);
2197
+ }
2198
+ const compiledSource = compiledAgent.outputText.replace(/(["'])@opencomputer\/agent\1/g, '"./opencomputer-agent.js"');
2199
+ await writeFile(resolve(runtime, "agent.js"), compiledSource);
2200
+ await writeFile(resolve(runtime, "opencomputer-agent.js"), agentApiRuntimeSource());
2201
+ const toolSources = [
2202
+ {
2203
+ filename: "opencomputer-connections.ts",
2204
+ source: connectionControlToolSource(),
2205
+ },
2206
+ ];
2207
+ const sourceTools = resolve(root, "tools");
2208
+ if (await exists(sourceTools)) {
2209
+ const entries = await readdir(sourceTools, { withFileTypes: true });
2210
+ for (const entry of entries) {
2211
+ if (!entry.isFile() || !/\.[cm]?[jt]s$/.test(entry.name))
2212
+ continue;
2213
+ toolSources.push({
2214
+ filename: entry.name,
2215
+ source: await readFile(resolve(sourceTools, entry.name), "utf8"),
2216
+ });
2217
+ }
2218
+ }
2219
+ const reactiveTools = [];
2220
+ const toolModules = [];
2221
+ await mkdir(resolve(runtime, "tools"), { recursive: true });
2222
+ for (const candidate of toolSources) {
2223
+ const ids = definedToolIds(candidate.source);
2224
+ const calls = [
2225
+ ...candidate.source.matchAll(/\btool(?:<[^>]+>)?\s*\(/g),
2226
+ ].length;
2227
+ if (ids.length !== calls) {
2228
+ throw new Error(`${candidate.filename} must give every tool() a literal string id`);
2229
+ }
2230
+ const compiledTool = transpile(candidate.source, candidate.filename);
2231
+ const toolDiagnostics = compiledTool.diagnostics ?? [];
2232
+ if (toolDiagnostics.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) {
2233
+ throw new Error(`${candidate.filename} could not be compiled: ${toolDiagnostics
2041
2234
  .map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " "))
2042
2235
  .join("; ")}`);
2043
2236
  }
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
- }
2237
+ const outputName = candidate.filename.replace(/\.[^.]+$/, ".js");
2238
+ const output = compiledTool.outputText.replace(/(["'])@opencomputer\/agent\1/g, '"../opencomputer-agent.js"');
2239
+ await writeFile(resolve(runtime, "tools", outputName), output);
2240
+ if (ids.length > 0) {
2241
+ reactiveTools.push(...ids);
2242
+ toolModules.push(`../tools/${outputName}`);
2059
2243
  }
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
2244
  }
2245
+ const duplicateTool = reactiveTools.find((id, index) => reactiveTools.indexOf(id) !== index);
2246
+ if (duplicateTool) {
2247
+ throw new Error(`Tool id ${JSON.stringify(duplicateTool)} is defined more than once`);
2248
+ }
2249
+ await mkdir(resolve(runtime, ".opencomputer"), { recursive: true });
2250
+ await writeFile(resolve(runtime, ".opencomputer", "reactive.json"), `${JSON.stringify({
2251
+ version: 2,
2252
+ entry: "../agent.js",
2253
+ tools: [...new Set([
2254
+ ...reactiveTools,
2255
+ ...literalHookIds(agentSource, "useTool"),
2256
+ ])].sort(),
2257
+ toolModules: toolModules.sort(),
2258
+ subagents: literalHookIds(agentSource, "useSubagent"),
2259
+ connections: [...new Set([
2260
+ ...literalHookIds(agentSource, "connection"),
2261
+ ...literalHookIds(agentSource, "useConnection"),
2262
+ ])].sort(),
2263
+ mcpServers: [...new Set([
2264
+ ...definedMcpServerIds(agentSource),
2265
+ ...literalHookIds(agentSource, "useMcpServer"),
2266
+ ])].sort(),
2267
+ }, null, 2)}\n`);
2068
2268
  return runtime;
2069
2269
  }
2070
2270
  async function collectNames(root, type) {
@@ -2126,7 +2326,7 @@ async function validateTemplateRequirements(root, manifest) {
2126
2326
  throw new Error("PTO calendar tools are missing. Run `opencomputer tools add calendar` before deploying.");
2127
2327
  }
2128
2328
  }
2129
- export async function buildAgentArtifact(root) {
2329
+ export async function buildAgentArtifact(root, agentId) {
2130
2330
  const startedAt = performance.now();
2131
2331
  const manifest = await readManifest(root);
2132
2332
  await validateTemplateRequirements(root, manifest);
@@ -2139,7 +2339,7 @@ export async function buildAgentArtifact(root) {
2139
2339
  files: await collectFiles(runtime),
2140
2340
  }));
2141
2341
  return {
2142
- agentId: manifest.id,
2342
+ agentId: agentId ?? manifest.id,
2143
2343
  name: manifest.name,
2144
2344
  channels,
2145
2345
  connections,