@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/binding.js +3 -13
- package/dist/binding.js.map +1 -1
- package/dist/binding.test.js +4 -2
- package/dist/binding.test.js.map +1 -1
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +4 -4
- package/dist/dev.js.map +1 -1
- package/dist/dev.test.js +1 -1
- package/dist/dev.test.js.map +1 -1
- package/dist/project.d.ts +1 -1
- package/dist/project.js +340 -140
- package/dist/project.js.map +1 -1
- package/dist/project.test.js +138 -9
- package/dist/project.test.js.map +1 -1
- package/package.json +1 -1
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 "@
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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 "@
|
|
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
|
-
|
|
551
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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 "@
|
|
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
|
-
|
|
750
|
-
|
|
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
|
-
|
|
819
|
-
|
|
879
|
+
input: {
|
|
880
|
+
type: "object",
|
|
881
|
+
properties: { pullRequestUrl: { type: "string", format: "uri" } },
|
|
882
|
+
required: ["pullRequestUrl"],
|
|
883
|
+
additionalProperties: false,
|
|
820
884
|
},
|
|
821
|
-
async execute(args
|
|
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(
|
|
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 "@
|
|
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
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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/
|
|
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
|
-
|
|
1642
|
+
await initializeTemplateAgentProject(HELLO_WORLD_TEMPLATE, agentRoot);
|
|
1555
1643
|
const manifest = {
|
|
1556
|
-
|
|
1557
|
-
|
|
1644
|
+
schema: 1,
|
|
1645
|
+
id: project?.agentId ?? "hello-world",
|
|
1558
1646
|
name: "Hello World",
|
|
1647
|
+
template: "hello-world",
|
|
1559
1648
|
};
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
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
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
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
|
-
|
|
2045
|
-
const
|
|
2046
|
-
await writeFile(resolve(runtime, "
|
|
2047
|
-
|
|
2048
|
-
|
|
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,
|