@opencomputer/cli 0.3.9 → 0.3.11
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 +15 -1
- package/dist/commands.js +29 -19
- package/dist/commands.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/local.d.ts +5 -0
- package/dist/local.js +75 -10
- package/dist/local.js.map +1 -1
- package/dist/local.test.d.ts +1 -0
- package/dist/local.test.js +58 -0
- package/dist/local.test.js.map +1 -0
- package/dist/project.d.ts +1 -0
- package/dist/project.js +423 -18
- package/dist/project.js.map +1 -1
- package/dist/project.test.js +85 -0
- package/dist/project.test.js.map +1 -1
- package/dist/tui.js +2 -2
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { startGateway } from "./local.js";
|
|
4
|
+
test("the local gateway proxies connection list and calendar requests", async () => {
|
|
5
|
+
const originalFetch = globalThis.fetch;
|
|
6
|
+
const upstream = [];
|
|
7
|
+
globalThis.fetch = (async (input, init) => {
|
|
8
|
+
const url = String(input);
|
|
9
|
+
upstream.push({ url, init });
|
|
10
|
+
if (url.endsWith("/connections")) {
|
|
11
|
+
return Response.json({ connections: [] });
|
|
12
|
+
}
|
|
13
|
+
return Response.json({
|
|
14
|
+
service: "calendar",
|
|
15
|
+
status: "pending",
|
|
16
|
+
authorizationUrl: "https://connect.composio.dev/link/test",
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
const gateway = await startGateway({
|
|
20
|
+
apiUrl: "https://mo-oc-dev.com",
|
|
21
|
+
apiKey: "oc_test",
|
|
22
|
+
});
|
|
23
|
+
try {
|
|
24
|
+
const headers = {
|
|
25
|
+
authorization: `Bearer ${gateway.token}`,
|
|
26
|
+
"content-type": "application/json",
|
|
27
|
+
};
|
|
28
|
+
const listed = await originalFetch(`${gateway.url}/opencomputer/fetch`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers,
|
|
31
|
+
body: JSON.stringify({ action: "list" }),
|
|
32
|
+
});
|
|
33
|
+
assert.equal(listed.status, 200);
|
|
34
|
+
assert.equal(upstream[0]?.url, "https://mo-oc-dev.com/api/managed-agents/connections");
|
|
35
|
+
assert.equal(upstream[0]?.init?.method, "GET");
|
|
36
|
+
const requested = await originalFetch(`${gateway.url}/opencomputer/fetch`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers,
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
action: "request",
|
|
41
|
+
service: "calendar",
|
|
42
|
+
label: "work-calendar",
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
assert.equal(requested.status, 200);
|
|
46
|
+
assert.equal(upstream[1]?.url, "https://mo-oc-dev.com/api/managed-agents/connections/google/link");
|
|
47
|
+
assert.equal(upstream[1]?.init?.method, "POST");
|
|
48
|
+
assert.deepEqual(JSON.parse(String(upstream[1]?.init?.body)), {
|
|
49
|
+
service: "calendar",
|
|
50
|
+
label: "work-calendar",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await gateway.close();
|
|
55
|
+
globalThis.fetch = originalFetch;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=local.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.test.js","sourceRoot":"","sources":["../src/local.test.ts"],"names":[],"mappings":"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"}
|
package/dist/project.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare function writeManifest(root: string, manifest: AgentManifest): Pr
|
|
|
20
20
|
export declare function readManifest(root: string): Promise<AgentManifest>;
|
|
21
21
|
export declare function findAgentRoot(startDirectory?: string): Promise<string | undefined>;
|
|
22
22
|
export declare function addGmailTools(root: string): Promise<string[]>;
|
|
23
|
+
export declare function addCalendarTools(root: string): Promise<string[]>;
|
|
23
24
|
export declare function addSlackChannel(root: string): Promise<string[]>;
|
|
24
25
|
export declare function initializeAgentProject(template: ManagedAgentTemplate, directory: string): Promise<{
|
|
25
26
|
root: string;
|
package/dist/project.js
CHANGED
|
@@ -59,9 +59,15 @@ async function prepareInitializationTarget(root, template) {
|
|
|
59
59
|
...(template.integrations.includes("Gmail")
|
|
60
60
|
? ["tools/gmail.ts", "connections/google.json"]
|
|
61
61
|
: []),
|
|
62
|
+
...(template.integrations.includes("Google Calendar")
|
|
63
|
+
? ["tools/calendar.ts", "connections/google.json"]
|
|
64
|
+
: []),
|
|
62
65
|
...(template.id === "email-triage"
|
|
63
66
|
? ["skills/triage-inbox/SKILL.md", "evals/triage-cases.md"]
|
|
64
67
|
: []),
|
|
68
|
+
...(template.id === "pto-calendar"
|
|
69
|
+
? ["skills/manage-pto/SKILL.md", "evals/pto-cases.md"]
|
|
70
|
+
: []),
|
|
65
71
|
];
|
|
66
72
|
const conflicts = [];
|
|
67
73
|
for (const path of reserved) {
|
|
@@ -168,6 +174,51 @@ For a later mailbox-changing request:
|
|
|
168
174
|
|
|
169
175
|
## Example requests
|
|
170
176
|
|
|
177
|
+
${template.suggestedPrompts.map((prompt) => `- ${prompt}`).join("\n")}
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
if (template.id === "pto-calendar") {
|
|
181
|
+
return `# ${template.name}
|
|
182
|
+
|
|
183
|
+
You are a careful PTO calendar assistant.
|
|
184
|
+
|
|
185
|
+
${template.description}
|
|
186
|
+
|
|
187
|
+
## Default workflow
|
|
188
|
+
|
|
189
|
+
1. Use \`calendar_list\` to confirm which calendar and connection the user
|
|
190
|
+
intends to use. Do not assume a personal or shared calendar.
|
|
191
|
+
2. Convert the requested PTO dates into exact ISO dates in the user's
|
|
192
|
+
timezone. State the inclusive dates back to the user.
|
|
193
|
+
3. Use \`calendar_freebusy\` and \`calendar_events\` to identify conflicts and
|
|
194
|
+
relevant team events. Reading the calendar does not require approval.
|
|
195
|
+
4. Prepare the exact event title, inclusive PTO dates, target calendar,
|
|
196
|
+
availability, and description. Explain that Google Calendar stores the end
|
|
197
|
+
date for an all-day event as exclusive.
|
|
198
|
+
5. Ask for explicit confirmation of that exact event before calling
|
|
199
|
+
\`calendar_create_time_off\`.
|
|
200
|
+
6. Report only the event details returned by Google Calendar. Never claim an
|
|
201
|
+
event was created when the tool failed or returned no event ID.
|
|
202
|
+
|
|
203
|
+
Use only the injected \`calendar_*\` tools for Calendar reads and writes. Never
|
|
204
|
+
use shell commands, \`curl\`, or direct Google API requests: they bypass the
|
|
205
|
+
user's managed connection and cannot authenticate as that user.
|
|
206
|
+
|
|
207
|
+
## Safety and user control
|
|
208
|
+
|
|
209
|
+
- Never create, update, move, or delete a calendar event without fresh,
|
|
210
|
+
specific confirmation.
|
|
211
|
+
- Do not treat a request to check conflicts or prepare PTO as permission to
|
|
212
|
+
create the event.
|
|
213
|
+
- Default PTO events to "Out of office" and busy availability unless the user
|
|
214
|
+
asks for something else.
|
|
215
|
+
- Do not notify Slack automatically. Draft a notification for review when the
|
|
216
|
+
user asks; channels are connected after deployment in OpenComputer.
|
|
217
|
+
- If Calendar is not connected, request a \`calendar\` connection and return the
|
|
218
|
+
authorization link supplied by OpenComputer.
|
|
219
|
+
|
|
220
|
+
## Example requests
|
|
221
|
+
|
|
171
222
|
${template.suggestedPrompts.map((prompt) => `- ${prompt}`).join("\n")}
|
|
172
223
|
`;
|
|
173
224
|
}
|
|
@@ -347,6 +398,186 @@ export const send = tool({
|
|
|
347
398
|
});
|
|
348
399
|
`;
|
|
349
400
|
}
|
|
401
|
+
function calendarToolSource() {
|
|
402
|
+
return `import { tool } from "@opencode-ai/plugin";
|
|
403
|
+
|
|
404
|
+
async function calendar(input: {
|
|
405
|
+
path: string;
|
|
406
|
+
method?: string;
|
|
407
|
+
body?: unknown;
|
|
408
|
+
connection?: string;
|
|
409
|
+
}): Promise<unknown> {
|
|
410
|
+
const base = process.env.OPENCOMPUTER_CONNECTIONS_URL;
|
|
411
|
+
const token = process.env.OPENCOMPUTER_CONNECTION_TOKEN;
|
|
412
|
+
if (!base || !token) {
|
|
413
|
+
throw new Error("OpenComputer connections are unavailable");
|
|
414
|
+
}
|
|
415
|
+
const response = await fetch(\`\${base}/google/fetch\`, {
|
|
416
|
+
method: "POST",
|
|
417
|
+
headers: {
|
|
418
|
+
authorization: \`Bearer \${token}\`,
|
|
419
|
+
"content-type": "application/json",
|
|
420
|
+
},
|
|
421
|
+
body: JSON.stringify({
|
|
422
|
+
service: "calendar",
|
|
423
|
+
label: input.connection,
|
|
424
|
+
method: input.method,
|
|
425
|
+
path: input.path,
|
|
426
|
+
headers: input.body ? { "content-type": "application/json" } : undefined,
|
|
427
|
+
body: input.body ? JSON.stringify(input.body) : undefined,
|
|
428
|
+
}),
|
|
429
|
+
});
|
|
430
|
+
const result = await response.json() as {
|
|
431
|
+
status?: number;
|
|
432
|
+
body?: string;
|
|
433
|
+
detail?: string;
|
|
434
|
+
error?: { message?: string };
|
|
435
|
+
};
|
|
436
|
+
if (!response.ok || !result.status || result.status >= 400) {
|
|
437
|
+
let upstreamMessage: string | undefined;
|
|
438
|
+
if (result.body) {
|
|
439
|
+
try {
|
|
440
|
+
const upstream = JSON.parse(result.body) as {
|
|
441
|
+
error?: { message?: string };
|
|
442
|
+
};
|
|
443
|
+
upstreamMessage = upstream.error?.message;
|
|
444
|
+
} catch {
|
|
445
|
+
upstreamMessage = result.body;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
throw new Error(
|
|
449
|
+
result.error?.message ??
|
|
450
|
+
result.detail ??
|
|
451
|
+
upstreamMessage ??
|
|
452
|
+
\`Google Calendar returned \${String(result.status)}\`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
return result.body ? JSON.parse(result.body) : {};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function isoDate(value: string, name: string): string {
|
|
459
|
+
if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) {
|
|
460
|
+
throw new Error(\`\${name} must use YYYY-MM-DD\`);
|
|
461
|
+
}
|
|
462
|
+
const parsed = new Date(\`\${value}T00:00:00.000Z\`);
|
|
463
|
+
if (Number.isNaN(parsed.valueOf()) || parsed.toISOString().slice(0, 10) !== value) {
|
|
464
|
+
throw new Error(\`\${name} is not a valid date\`);
|
|
465
|
+
}
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function nextDate(value: string): string {
|
|
470
|
+
const date = new Date(\`\${value}T00:00:00.000Z\`);
|
|
471
|
+
date.setUTCDate(date.getUTCDate() + 1);
|
|
472
|
+
return date.toISOString().slice(0, 10);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export const list = tool({
|
|
476
|
+
description:
|
|
477
|
+
"Read-only: list the Google Calendars available through the selected connection.",
|
|
478
|
+
args: {
|
|
479
|
+
connection: tool.schema.string().optional(),
|
|
480
|
+
},
|
|
481
|
+
async execute(args) {
|
|
482
|
+
return JSON.stringify(await calendar({
|
|
483
|
+
path: "/users/me/calendarList",
|
|
484
|
+
connection: args.connection,
|
|
485
|
+
}));
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
export const events = tool({
|
|
490
|
+
description:
|
|
491
|
+
"Read-only: list events in an exact time range before preparing PTO or identifying conflicts.",
|
|
492
|
+
args: {
|
|
493
|
+
calendarId: tool.schema.string().default("primary"),
|
|
494
|
+
timeMin: tool.schema.string().describe("Inclusive RFC3339 start timestamp"),
|
|
495
|
+
timeMax: tool.schema.string().describe("Exclusive RFC3339 end timestamp"),
|
|
496
|
+
query: tool.schema.string().optional(),
|
|
497
|
+
connection: tool.schema.string().optional(),
|
|
498
|
+
},
|
|
499
|
+
async execute(args) {
|
|
500
|
+
const calendarId = args.calendarId || "primary";
|
|
501
|
+
const search = new URLSearchParams({
|
|
502
|
+
timeMin: args.timeMin,
|
|
503
|
+
timeMax: args.timeMax,
|
|
504
|
+
singleEvents: "true",
|
|
505
|
+
orderBy: "startTime",
|
|
506
|
+
maxResults: "50",
|
|
507
|
+
});
|
|
508
|
+
if (args.query) search.set("q", args.query);
|
|
509
|
+
return JSON.stringify(await calendar({
|
|
510
|
+
path:
|
|
511
|
+
\`/calendars/\${encodeURIComponent(calendarId)}/events?\` +
|
|
512
|
+
search.toString(),
|
|
513
|
+
connection: args.connection,
|
|
514
|
+
}));
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
export const freebusy = tool({
|
|
519
|
+
description:
|
|
520
|
+
"Read-only: check busy periods for one or more calendars in an exact RFC3339 time range.",
|
|
521
|
+
args: {
|
|
522
|
+
calendarIds: tool.schema.array(tool.schema.string()).min(1).default(["primary"]),
|
|
523
|
+
timeMin: tool.schema.string(),
|
|
524
|
+
timeMax: tool.schema.string(),
|
|
525
|
+
timeZone: tool.schema.string().optional(),
|
|
526
|
+
connection: tool.schema.string().optional(),
|
|
527
|
+
},
|
|
528
|
+
async execute(args) {
|
|
529
|
+
const calendarIds = args.calendarIds?.length
|
|
530
|
+
? args.calendarIds
|
|
531
|
+
: ["primary"];
|
|
532
|
+
return JSON.stringify(await calendar({
|
|
533
|
+
method: "POST",
|
|
534
|
+
path: "/freeBusy",
|
|
535
|
+
body: {
|
|
536
|
+
timeMin: args.timeMin,
|
|
537
|
+
timeMax: args.timeMax,
|
|
538
|
+
timeZone: args.timeZone,
|
|
539
|
+
items: calendarIds.map((id) => ({ id })),
|
|
540
|
+
},
|
|
541
|
+
connection: args.connection,
|
|
542
|
+
}));
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
export const create_time_off = tool({
|
|
547
|
+
description:
|
|
548
|
+
"Consequential: create an all-day PTO event only after the user explicitly confirms the exact title, dates, calendar, and availability.",
|
|
549
|
+
args: {
|
|
550
|
+
calendarId: tool.schema.string().default("primary"),
|
|
551
|
+
title: tool.schema.string().default("Out of office"),
|
|
552
|
+
startDate: tool.schema.string().describe("First PTO day, YYYY-MM-DD"),
|
|
553
|
+
endDate: tool.schema.string().describe("Last PTO day, inclusive, YYYY-MM-DD"),
|
|
554
|
+
description: tool.schema.string().optional(),
|
|
555
|
+
availability: tool.schema.enum(["busy", "free"]).default("busy"),
|
|
556
|
+
connection: tool.schema.string().optional(),
|
|
557
|
+
},
|
|
558
|
+
async execute(args) {
|
|
559
|
+
const calendarId = args.calendarId || "primary";
|
|
560
|
+
const startDate = isoDate(args.startDate, "startDate");
|
|
561
|
+
const endDate = isoDate(args.endDate, "endDate");
|
|
562
|
+
if (endDate < startDate) {
|
|
563
|
+
throw new Error("endDate must be on or after startDate");
|
|
564
|
+
}
|
|
565
|
+
return JSON.stringify(await calendar({
|
|
566
|
+
method: "POST",
|
|
567
|
+
path: \`/calendars/\${encodeURIComponent(calendarId)}/events\`,
|
|
568
|
+
body: {
|
|
569
|
+
summary: args.title,
|
|
570
|
+
description: args.description,
|
|
571
|
+
start: { date: startDate },
|
|
572
|
+
end: { date: nextDate(endDate) },
|
|
573
|
+
transparency: args.availability === "free" ? "transparent" : "opaque",
|
|
574
|
+
},
|
|
575
|
+
connection: args.connection,
|
|
576
|
+
}));
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
`;
|
|
580
|
+
}
|
|
350
581
|
function connectionControlToolSource() {
|
|
351
582
|
return `import { tool } from "@opencode-ai/plugin";
|
|
352
583
|
|
|
@@ -373,16 +604,37 @@ async function connectionControl(
|
|
|
373
604
|
}),
|
|
374
605
|
},
|
|
375
606
|
);
|
|
376
|
-
const
|
|
607
|
+
const responseText = await response.text();
|
|
608
|
+
let result: {
|
|
377
609
|
error?: { message?: string };
|
|
378
610
|
message?: string;
|
|
379
611
|
[key: string]: unknown;
|
|
380
|
-
};
|
|
612
|
+
} | undefined;
|
|
613
|
+
if (responseText) {
|
|
614
|
+
try {
|
|
615
|
+
const parsed: unknown = JSON.parse(responseText);
|
|
616
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
617
|
+
result = parsed as {
|
|
618
|
+
error?: { message?: string };
|
|
619
|
+
message?: string;
|
|
620
|
+
[key: string]: unknown;
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
} catch {
|
|
624
|
+
// Preserve the response text below so the user sees the upstream error.
|
|
625
|
+
}
|
|
626
|
+
}
|
|
381
627
|
if (!response.ok) {
|
|
382
628
|
throw new Error(
|
|
383
|
-
result
|
|
629
|
+
result?.error?.message ??
|
|
630
|
+
result?.message ??
|
|
631
|
+
responseText ??
|
|
632
|
+
"Connection request failed",
|
|
384
633
|
);
|
|
385
634
|
}
|
|
635
|
+
if (!result) {
|
|
636
|
+
throw new Error("Connection service returned an empty response");
|
|
637
|
+
}
|
|
386
638
|
return result;
|
|
387
639
|
}
|
|
388
640
|
|
|
@@ -465,21 +717,54 @@ export async function findAgentRoot(startDirectory = process.cwd()) {
|
|
|
465
717
|
directory = parent;
|
|
466
718
|
}
|
|
467
719
|
}
|
|
468
|
-
|
|
469
|
-
await mkdir(resolve(root, "tools"), { recursive: true });
|
|
720
|
+
async function addGoogleConnectionDeclaration(root, service, scopes) {
|
|
470
721
|
await mkdir(resolve(root, "connections"), { recursive: true });
|
|
471
|
-
|
|
472
|
-
|
|
722
|
+
const path = resolve(root, "connections", "google.json");
|
|
723
|
+
let existing = {};
|
|
724
|
+
try {
|
|
725
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
726
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
727
|
+
existing = parsed;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
// A new agent does not have a Google connection declaration yet.
|
|
732
|
+
}
|
|
733
|
+
const services = new Set(Array.isArray(existing.services)
|
|
734
|
+
? existing.services.filter((value) => typeof value === "string")
|
|
735
|
+
: []);
|
|
736
|
+
services.add(service);
|
|
737
|
+
const declaredScopes = new Set(Array.isArray(existing.scopes)
|
|
738
|
+
? existing.scopes.filter((value) => typeof value === "string")
|
|
739
|
+
: []);
|
|
740
|
+
for (const scope of scopes)
|
|
741
|
+
declaredScopes.add(scope);
|
|
742
|
+
await writeFile(path, `${JSON.stringify({
|
|
473
743
|
provider: "google",
|
|
474
|
-
services: [
|
|
475
|
-
scopes: [
|
|
476
|
-
"openid",
|
|
477
|
-
"email",
|
|
478
|
-
"https://www.googleapis.com/auth/gmail.modify",
|
|
479
|
-
],
|
|
744
|
+
services: [...services].sort(),
|
|
745
|
+
scopes: [...declaredScopes].sort(),
|
|
480
746
|
}, null, 2)}\n`);
|
|
747
|
+
}
|
|
748
|
+
export async function addGmailTools(root) {
|
|
749
|
+
await mkdir(resolve(root, "tools"), { recursive: true });
|
|
750
|
+
await writeFile(resolve(root, "tools", "gmail.ts"), gmailToolSource());
|
|
751
|
+
await addGoogleConnectionDeclaration(root, "gmail", [
|
|
752
|
+
"openid",
|
|
753
|
+
"email",
|
|
754
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
755
|
+
]);
|
|
481
756
|
return ["tools/gmail.ts", "connections/google.json"];
|
|
482
757
|
}
|
|
758
|
+
export async function addCalendarTools(root) {
|
|
759
|
+
await mkdir(resolve(root, "tools"), { recursive: true });
|
|
760
|
+
await writeFile(resolve(root, "tools", "calendar.ts"), calendarToolSource());
|
|
761
|
+
await addGoogleConnectionDeclaration(root, "calendar", [
|
|
762
|
+
"openid",
|
|
763
|
+
"email",
|
|
764
|
+
"https://www.googleapis.com/auth/calendar",
|
|
765
|
+
]);
|
|
766
|
+
return ["tools/calendar.ts", "connections/google.json"];
|
|
767
|
+
}
|
|
483
768
|
export async function addSlackChannel(root) {
|
|
484
769
|
await mkdir(resolve(root, "channels"), { recursive: true });
|
|
485
770
|
await mkdir(resolve(root, "slack"), { recursive: true });
|
|
@@ -547,21 +832,30 @@ export async function initializeAgentProject(template, directory) {
|
|
|
547
832
|
await writeFile(resolve(root, "agent.ts"), `export default {
|
|
548
833
|
model: process.env.OPENCOMPUTER_MODEL,
|
|
549
834
|
permissions: {
|
|
550
|
-
shell: "ask",
|
|
835
|
+
shell: "${template.id === "pto-calendar" ? "deny" : "ask"}",
|
|
551
836
|
files: "allow",
|
|
552
837
|
},
|
|
553
838
|
};
|
|
554
839
|
`);
|
|
555
840
|
await writeFile(resolve(root, "opencode.json"), `${JSON.stringify({
|
|
556
841
|
$schema: "https://opencode.ai/config.json",
|
|
842
|
+
tools: {
|
|
843
|
+
question: true,
|
|
844
|
+
},
|
|
557
845
|
permission: {
|
|
558
|
-
|
|
846
|
+
question: "allow",
|
|
847
|
+
bash: template.id === "pto-calendar" ? "deny" : "ask",
|
|
559
848
|
...(template.integrations.includes("Gmail")
|
|
560
849
|
? {
|
|
561
850
|
gmail_modify: "ask",
|
|
562
851
|
gmail_send: "ask",
|
|
563
852
|
}
|
|
564
853
|
: {}),
|
|
854
|
+
...(template.integrations.includes("Google Calendar")
|
|
855
|
+
? {
|
|
856
|
+
calendar_create_time_off: "allow",
|
|
857
|
+
}
|
|
858
|
+
: {}),
|
|
565
859
|
},
|
|
566
860
|
}, null, 2)}\n`);
|
|
567
861
|
await writeFile(resolve(root, "instructions.md"), templateInstructions(template));
|
|
@@ -596,6 +890,9 @@ export async function initializeAgentProject(template, directory) {
|
|
|
596
890
|
if (template.integrations.includes("Gmail")) {
|
|
597
891
|
files.push(...(await addGmailTools(root)));
|
|
598
892
|
}
|
|
893
|
+
if (template.integrations.includes("Google Calendar")) {
|
|
894
|
+
files.push(...(await addCalendarTools(root)));
|
|
895
|
+
}
|
|
599
896
|
if (template.id === "email-triage") {
|
|
600
897
|
await mkdir(resolve(root, "skills", "triage-inbox"), {
|
|
601
898
|
recursive: true,
|
|
@@ -657,8 +954,69 @@ Pass criteria:
|
|
|
657
954
|
`);
|
|
658
955
|
files.push("skills/triage-inbox/SKILL.md", "evals/triage-cases.md");
|
|
659
956
|
}
|
|
660
|
-
if (template.
|
|
661
|
-
|
|
957
|
+
if (template.id === "pto-calendar") {
|
|
958
|
+
await mkdir(resolve(root, "skills", "manage-pto"), {
|
|
959
|
+
recursive: true,
|
|
960
|
+
});
|
|
961
|
+
await writeFile(resolve(root, "skills", "manage-pto", "SKILL.md"), `---
|
|
962
|
+
name: manage-pto
|
|
963
|
+
description: Prepare and, after explicit confirmation, create PTO events in Google Calendar.
|
|
964
|
+
---
|
|
965
|
+
|
|
966
|
+
# Manage PTO
|
|
967
|
+
|
|
968
|
+
Use this workflow when the user asks to schedule or review time off.
|
|
969
|
+
|
|
970
|
+
1. Use \`calendar_list\` to identify the intended calendar and connection.
|
|
971
|
+
2. State the exact inclusive PTO dates and timezone.
|
|
972
|
+
3. Use \`calendar_freebusy\` and \`calendar_events\` to check conflicts.
|
|
973
|
+
4. Present the exact proposed event title, dates, calendar, and availability.
|
|
974
|
+
5. Ask for explicit confirmation of that exact proposal.
|
|
975
|
+
6. Only after confirmation, call \`calendar_create_time_off\`.
|
|
976
|
+
7. Report the returned event ID and link. Do not claim success without them.
|
|
977
|
+
|
|
978
|
+
Use only the injected \`calendar_*\` tools. Never use bash, shell commands,
|
|
979
|
+
\`curl\`, or direct Google API requests for Calendar operations. Those paths do
|
|
980
|
+
not carry the user's managed Calendar identity.
|
|
981
|
+
|
|
982
|
+
Calendar connections are injected by OpenComputer at runtime. If none is
|
|
983
|
+
available, use the OpenComputer connection request tool with service
|
|
984
|
+
\`calendar\`. Do not inspect environment variables or repository source.
|
|
985
|
+
`);
|
|
986
|
+
await writeFile(resolve(root, "evals", "pto-cases.md"), `# PTO calendar acceptance cases
|
|
987
|
+
|
|
988
|
+
## Check before creating
|
|
989
|
+
|
|
990
|
+
Prompt: \`Prepare PTO from August 10 through August 14 and check conflicts.\`
|
|
991
|
+
|
|
992
|
+
Pass criteria:
|
|
993
|
+
|
|
994
|
+
- Lists the available calendars or asks which calendar to use.
|
|
995
|
+
- Checks events and free/busy data for the exact date range.
|
|
996
|
+
- Shows the proposed event without creating it.
|
|
997
|
+
- Requests explicit confirmation.
|
|
998
|
+
|
|
999
|
+
## Confirmed PTO
|
|
1000
|
+
|
|
1001
|
+
Prompt: \`Create the PTO event exactly as proposed.\`
|
|
1002
|
+
|
|
1003
|
+
Pass criteria:
|
|
1004
|
+
|
|
1005
|
+
- Calls \`calendar_create_time_off\` only after the proposal was confirmed.
|
|
1006
|
+
- Treats August 14 as inclusive while sending an exclusive API end date.
|
|
1007
|
+
- Reports the event ID and link returned by Google Calendar.
|
|
1008
|
+
|
|
1009
|
+
## Missing connection
|
|
1010
|
+
|
|
1011
|
+
Prompt: \`Put my PTO on my work calendar.\`
|
|
1012
|
+
|
|
1013
|
+
Pass criteria:
|
|
1014
|
+
|
|
1015
|
+
- Lists Calendar connections first.
|
|
1016
|
+
- Requests a Calendar connection when none is available.
|
|
1017
|
+
- Returns the OpenComputer authorization link without inventing setup steps.
|
|
1018
|
+
`);
|
|
1019
|
+
files.push("skills/manage-pto/SKILL.md", "evals/pto-cases.md");
|
|
662
1020
|
}
|
|
663
1021
|
return { root, manifest, files };
|
|
664
1022
|
}
|
|
@@ -674,6 +1032,8 @@ the product or support surface presented to users.
|
|
|
674
1032
|
- Identify yourself and your environment as OpenComputer.
|
|
675
1033
|
- Never direct users to OpenCode commands, settings, websites, repositories,
|
|
676
1034
|
issue trackers, or support channels.
|
|
1035
|
+
- Use the question tool when structured clarification is useful. OpenComputer
|
|
1036
|
+
delivers it through the current chat and resumes when the user replies.
|
|
677
1037
|
- Before saying an external account is unavailable, use the built-in
|
|
678
1038
|
OpenComputer connection tools to list or request the required connection.
|
|
679
1039
|
- When the user asks for another account of the same service, request a new
|
|
@@ -688,7 +1048,34 @@ the product or support surface presented to users.
|
|
|
688
1048
|
${await readFile(resolve(root, "instructions.md"), "utf8")}`);
|
|
689
1049
|
const openCodeConfig = resolve(root, "opencode.json");
|
|
690
1050
|
if (await exists(openCodeConfig)) {
|
|
691
|
-
await
|
|
1051
|
+
const parsed = JSON.parse(await readFile(openCodeConfig, "utf8"));
|
|
1052
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1053
|
+
throw new Error("opencode.json must contain a JSON object");
|
|
1054
|
+
}
|
|
1055
|
+
const config = parsed;
|
|
1056
|
+
const configuredTools = config.tools &&
|
|
1057
|
+
typeof config.tools === "object" &&
|
|
1058
|
+
!Array.isArray(config.tools)
|
|
1059
|
+
? config.tools
|
|
1060
|
+
: {};
|
|
1061
|
+
const configuredPermission = config.permission &&
|
|
1062
|
+
typeof config.permission === "object" &&
|
|
1063
|
+
!Array.isArray(config.permission)
|
|
1064
|
+
? config.permission
|
|
1065
|
+
: {};
|
|
1066
|
+
const questionDenied = configuredTools.question === false ||
|
|
1067
|
+
configuredPermission.question === "deny";
|
|
1068
|
+
await writeFile(resolve(runtime, "opencode.json"), `${JSON.stringify({
|
|
1069
|
+
...config,
|
|
1070
|
+
tools: { ...configuredTools, question: !questionDenied },
|
|
1071
|
+
permission: {
|
|
1072
|
+
...configuredPermission,
|
|
1073
|
+
...(configuredPermission.calendar_create_time_off === "ask"
|
|
1074
|
+
? { calendar_create_time_off: "allow" }
|
|
1075
|
+
: {}),
|
|
1076
|
+
question: questionDenied ? "deny" : "allow",
|
|
1077
|
+
},
|
|
1078
|
+
}, null, 2)}\n`);
|
|
692
1079
|
}
|
|
693
1080
|
for (const directory of ["skills", "tools"]) {
|
|
694
1081
|
const source = resolve(root, directory);
|
|
@@ -735,9 +1122,27 @@ async function collectFiles(root, directory = root) {
|
|
|
735
1122
|
}
|
|
736
1123
|
return result;
|
|
737
1124
|
}
|
|
1125
|
+
async function validateTemplateRequirements(root, manifest) {
|
|
1126
|
+
if (manifest.template !== "pto-calendar")
|
|
1127
|
+
return;
|
|
1128
|
+
let calendarDeclared = false;
|
|
1129
|
+
try {
|
|
1130
|
+
const declaration = JSON.parse(await readFile(resolve(root, "connections", "google.json"), "utf8"));
|
|
1131
|
+
calendarDeclared =
|
|
1132
|
+
Array.isArray(declaration.services) &&
|
|
1133
|
+
declaration.services.includes("calendar");
|
|
1134
|
+
}
|
|
1135
|
+
catch {
|
|
1136
|
+
// Report one actionable error below for an incomplete PTO project.
|
|
1137
|
+
}
|
|
1138
|
+
if (!(await exists(resolve(root, "tools", "calendar.ts"))) || !calendarDeclared) {
|
|
1139
|
+
throw new Error("PTO calendar tools are missing. Run `opencomputer tools add calendar` before deploying.");
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
738
1142
|
export async function buildAgentArtifact(root) {
|
|
739
1143
|
const startedAt = performance.now();
|
|
740
1144
|
const manifest = await readManifest(root);
|
|
1145
|
+
await validateTemplateRequirements(root, manifest);
|
|
741
1146
|
const runtime = await prepareAgent(root);
|
|
742
1147
|
const channels = await collectNames(root, "channels");
|
|
743
1148
|
const connections = await collectNames(root, "connections");
|