@digitalpresence/cliclaw 0.1.0
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/__tests__/calendar.integration.test.d.ts +2 -0
- package/dist/__tests__/calendar.integration.test.d.ts.map +1 -0
- package/dist/__tests__/calendar.integration.test.js +98 -0
- package/dist/__tests__/calendar.integration.test.js.map +1 -0
- package/dist/__tests__/forms.integration.test.d.ts +2 -0
- package/dist/__tests__/forms.integration.test.d.ts.map +1 -0
- package/dist/__tests__/forms.integration.test.js +243 -0
- package/dist/__tests__/forms.integration.test.js.map +1 -0
- package/dist/__tests__/gdrive.integration.test.d.ts +2 -0
- package/dist/__tests__/gdrive.integration.test.d.ts.map +1 -0
- package/dist/__tests__/gdrive.integration.test.js +186 -0
- package/dist/__tests__/gdrive.integration.test.js.map +1 -0
- package/dist/__tests__/gmail.integration.test.d.ts +2 -0
- package/dist/__tests__/gmail.integration.test.d.ts.map +1 -0
- package/dist/__tests__/gmail.integration.test.js +197 -0
- package/dist/__tests__/gmail.integration.test.js.map +1 -0
- package/dist/__tests__/gslides.integration.test.d.ts +2 -0
- package/dist/__tests__/gslides.integration.test.d.ts.map +1 -0
- package/dist/__tests__/gslides.integration.test.js +124 -0
- package/dist/__tests__/gslides.integration.test.js.map +1 -0
- package/dist/__tests__/sheets.integration.test.d.ts +2 -0
- package/dist/__tests__/sheets.integration.test.d.ts.map +1 -0
- package/dist/__tests__/sheets.integration.test.js +150 -0
- package/dist/__tests__/sheets.integration.test.js.map +1 -0
- package/dist/agent/crud.d.ts +6 -0
- package/dist/agent/crud.d.ts.map +1 -0
- package/dist/agent/crud.js +41 -0
- package/dist/agent/crud.js.map +1 -0
- package/dist/agent/memory.d.ts +14 -0
- package/dist/agent/memory.d.ts.map +1 -0
- package/dist/agent/memory.js +66 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/permissions.d.ts +4 -0
- package/dist/agent/permissions.d.ts.map +1 -0
- package/dist/agent/permissions.js +32 -0
- package/dist/agent/permissions.js.map +1 -0
- package/dist/calendar/accounts.d.ts +3 -0
- package/dist/calendar/accounts.d.ts.map +1 -0
- package/dist/calendar/accounts.js +21 -0
- package/dist/calendar/accounts.js.map +1 -0
- package/dist/calendar/auth.d.ts +3 -0
- package/dist/calendar/auth.d.ts.map +1 -0
- package/dist/calendar/auth.js +41 -0
- package/dist/calendar/auth.js.map +1 -0
- package/dist/calendar/calendars.d.ts +3 -0
- package/dist/calendar/calendars.d.ts.map +1 -0
- package/dist/calendar/calendars.js +28 -0
- package/dist/calendar/calendars.js.map +1 -0
- package/dist/calendar/create.d.ts +3 -0
- package/dist/calendar/create.d.ts.map +1 -0
- package/dist/calendar/create.js +31 -0
- package/dist/calendar/create.js.map +1 -0
- package/dist/calendar/delete.d.ts +3 -0
- package/dist/calendar/delete.d.ts.map +1 -0
- package/dist/calendar/delete.js +21 -0
- package/dist/calendar/delete.js.map +1 -0
- package/dist/calendar/events.d.ts +3 -0
- package/dist/calendar/events.d.ts.map +1 -0
- package/dist/calendar/events.js +37 -0
- package/dist/calendar/events.js.map +1 -0
- package/dist/calendar/get.d.ts +3 -0
- package/dist/calendar/get.d.ts.map +1 -0
- package/dist/calendar/get.js +21 -0
- package/dist/calendar/get.js.map +1 -0
- package/dist/calendar/update.d.ts +3 -0
- package/dist/calendar/update.d.ts.map +1 -0
- package/dist/calendar/update.js +31 -0
- package/dist/calendar/update.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +37 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +107 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/calendar.d.ts +9 -0
- package/dist/commands/calendar.d.ts.map +1 -0
- package/dist/commands/calendar.js +104 -0
- package/dist/commands/calendar.js.map +1 -0
- package/dist/commands/cron.d.ts +6 -0
- package/dist/commands/cron.d.ts.map +1 -0
- package/dist/commands/cron.js +103 -0
- package/dist/commands/cron.js.map +1 -0
- package/dist/commands/forms.d.ts +9 -0
- package/dist/commands/forms.d.ts.map +1 -0
- package/dist/commands/forms.js +139 -0
- package/dist/commands/forms.js.map +1 -0
- package/dist/commands/gdrive.d.ts +9 -0
- package/dist/commands/gdrive.d.ts.map +1 -0
- package/dist/commands/gdrive.js +198 -0
- package/dist/commands/gdrive.js.map +1 -0
- package/dist/commands/gmail.d.ts +9 -0
- package/dist/commands/gmail.d.ts.map +1 -0
- package/dist/commands/gmail.js +231 -0
- package/dist/commands/gmail.js.map +1 -0
- package/dist/commands/gslides.d.ts +9 -0
- package/dist/commands/gslides.d.ts.map +1 -0
- package/dist/commands/gslides.js +167 -0
- package/dist/commands/gslides.js.map +1 -0
- package/dist/commands/sheets.d.ts +9 -0
- package/dist/commands/sheets.d.ts.map +1 -0
- package/dist/commands/sheets.js +174 -0
- package/dist/commands/sheets.js.map +1 -0
- package/dist/cron/daemon.d.ts +3 -0
- package/dist/cron/daemon.d.ts.map +1 -0
- package/dist/cron/daemon.js +127 -0
- package/dist/cron/daemon.js.map +1 -0
- package/dist/cron/handlers.d.ts +6 -0
- package/dist/cron/handlers.d.ts.map +1 -0
- package/dist/cron/handlers.js +58 -0
- package/dist/cron/handlers.js.map +1 -0
- package/dist/cron/logger.d.ts +4 -0
- package/dist/cron/logger.d.ts.map +1 -0
- package/dist/cron/logger.js +11 -0
- package/dist/cron/logger.js.map +1 -0
- package/dist/cron/progress.d.ts +25 -0
- package/dist/cron/progress.d.ts.map +1 -0
- package/dist/cron/progress.js +71 -0
- package/dist/cron/progress.js.map +1 -0
- package/dist/cron/ralph-wiggum.d.ts +18 -0
- package/dist/cron/ralph-wiggum.d.ts.map +1 -0
- package/dist/cron/ralph-wiggum.js +143 -0
- package/dist/cron/ralph-wiggum.js.map +1 -0
- package/dist/forms/accounts.d.ts +3 -0
- package/dist/forms/accounts.d.ts.map +1 -0
- package/dist/forms/accounts.js +20 -0
- package/dist/forms/accounts.js.map +1 -0
- package/dist/forms/auth.d.ts +3 -0
- package/dist/forms/auth.d.ts.map +1 -0
- package/dist/forms/auth.js +41 -0
- package/dist/forms/auth.js.map +1 -0
- package/dist/forms/create.d.ts +3 -0
- package/dist/forms/create.d.ts.map +1 -0
- package/dist/forms/create.js +28 -0
- package/dist/forms/create.js.map +1 -0
- package/dist/forms/get.d.ts +3 -0
- package/dist/forms/get.d.ts.map +1 -0
- package/dist/forms/get.js +21 -0
- package/dist/forms/get.js.map +1 -0
- package/dist/forms/list.d.ts +3 -0
- package/dist/forms/list.d.ts.map +1 -0
- package/dist/forms/list.js +33 -0
- package/dist/forms/list.js.map +1 -0
- package/dist/forms/questions.d.ts +6 -0
- package/dist/forms/questions.d.ts.map +1 -0
- package/dist/forms/questions.js +179 -0
- package/dist/forms/questions.js.map +1 -0
- package/dist/forms/responses.d.ts +4 -0
- package/dist/forms/responses.d.ts.map +1 -0
- package/dist/forms/responses.js +40 -0
- package/dist/forms/responses.js.map +1 -0
- package/dist/forms/update.d.ts +3 -0
- package/dist/forms/update.d.ts.map +1 -0
- package/dist/forms/update.js +44 -0
- package/dist/forms/update.js.map +1 -0
- package/dist/gdrive/about.d.ts +3 -0
- package/dist/gdrive/about.d.ts.map +1 -0
- package/dist/gdrive/about.js +31 -0
- package/dist/gdrive/about.js.map +1 -0
- package/dist/gdrive/accounts.d.ts +3 -0
- package/dist/gdrive/accounts.d.ts.map +1 -0
- package/dist/gdrive/accounts.js +20 -0
- package/dist/gdrive/accounts.js.map +1 -0
- package/dist/gdrive/auth.d.ts +3 -0
- package/dist/gdrive/auth.d.ts.map +1 -0
- package/dist/gdrive/auth.js +51 -0
- package/dist/gdrive/auth.js.map +1 -0
- package/dist/gdrive/files.d.ts +12 -0
- package/dist/gdrive/files.d.ts.map +1 -0
- package/dist/gdrive/files.js +174 -0
- package/dist/gdrive/files.js.map +1 -0
- package/dist/gdrive/folders.d.ts +4 -0
- package/dist/gdrive/folders.d.ts.map +1 -0
- package/dist/gdrive/folders.js +46 -0
- package/dist/gdrive/folders.js.map +1 -0
- package/dist/gdrive/search.d.ts +3 -0
- package/dist/gdrive/search.d.ts.map +1 -0
- package/dist/gdrive/search.js +23 -0
- package/dist/gdrive/search.js.map +1 -0
- package/dist/gdrive/sharing.d.ts +5 -0
- package/dist/gdrive/sharing.d.ts.map +1 -0
- package/dist/gdrive/sharing.js +54 -0
- package/dist/gdrive/sharing.js.map +1 -0
- package/dist/gmail/accounts.d.ts +3 -0
- package/dist/gmail/accounts.d.ts.map +1 -0
- package/dist/gmail/accounts.js +18 -0
- package/dist/gmail/accounts.js.map +1 -0
- package/dist/gmail/auth.d.ts +3 -0
- package/dist/gmail/auth.d.ts.map +1 -0
- package/dist/gmail/auth.js +50 -0
- package/dist/gmail/auth.js.map +1 -0
- package/dist/gmail/drafts.d.ts +7 -0
- package/dist/gmail/drafts.d.ts.map +1 -0
- package/dist/gmail/drafts.js +103 -0
- package/dist/gmail/drafts.js.map +1 -0
- package/dist/gmail/get.d.ts +4 -0
- package/dist/gmail/get.d.ts.map +1 -0
- package/dist/gmail/get.js +140 -0
- package/dist/gmail/get.js.map +1 -0
- package/dist/gmail/inbox.d.ts +4 -0
- package/dist/gmail/inbox.d.ts.map +1 -0
- package/dist/gmail/inbox.js +45 -0
- package/dist/gmail/inbox.js.map +1 -0
- package/dist/gmail/labels.d.ts +5 -0
- package/dist/gmail/labels.d.ts.map +1 -0
- package/dist/gmail/labels.js +45 -0
- package/dist/gmail/labels.js.map +1 -0
- package/dist/gmail/modify.d.ts +5 -0
- package/dist/gmail/modify.d.ts.map +1 -0
- package/dist/gmail/modify.js +68 -0
- package/dist/gmail/modify.js.map +1 -0
- package/dist/gmail/send.d.ts +5 -0
- package/dist/gmail/send.d.ts.map +1 -0
- package/dist/gmail/send.js +310 -0
- package/dist/gmail/send.js.map +1 -0
- package/dist/gmail/threads.d.ts +4 -0
- package/dist/gmail/threads.d.ts.map +1 -0
- package/dist/gmail/threads.js +47 -0
- package/dist/gmail/threads.js.map +1 -0
- package/dist/gslides/accounts.d.ts +3 -0
- package/dist/gslides/accounts.d.ts.map +1 -0
- package/dist/gslides/accounts.js +20 -0
- package/dist/gslides/accounts.js.map +1 -0
- package/dist/gslides/auth.d.ts +3 -0
- package/dist/gslides/auth.d.ts.map +1 -0
- package/dist/gslides/auth.js +50 -0
- package/dist/gslides/auth.js.map +1 -0
- package/dist/gslides/presentations.d.ts +29 -0
- package/dist/gslides/presentations.d.ts.map +1 -0
- package/dist/gslides/presentations.js +320 -0
- package/dist/gslides/presentations.js.map +1 -0
- package/dist/lib/config.d.ts +6 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +12 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/media-utils.d.ts +5 -0
- package/dist/lib/media-utils.d.ts.map +1 -0
- package/dist/lib/media-utils.js +79 -0
- package/dist/lib/media-utils.js.map +1 -0
- package/dist/lib/output.d.ts +4 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +12 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/sheets/accounts.d.ts +3 -0
- package/dist/sheets/accounts.d.ts.map +1 -0
- package/dist/sheets/accounts.js +20 -0
- package/dist/sheets/accounts.js.map +1 -0
- package/dist/sheets/auth.d.ts +3 -0
- package/dist/sheets/auth.d.ts.map +1 -0
- package/dist/sheets/auth.js +56 -0
- package/dist/sheets/auth.js.map +1 -0
- package/dist/sheets/cells.d.ts +6 -0
- package/dist/sheets/cells.d.ts.map +1 -0
- package/dist/sheets/cells.js +89 -0
- package/dist/sheets/cells.js.map +1 -0
- package/dist/sheets/format.d.ts +8 -0
- package/dist/sheets/format.d.ts.map +1 -0
- package/dist/sheets/format.js +97 -0
- package/dist/sheets/format.js.map +1 -0
- package/dist/sheets/sheets-tab.d.ts +6 -0
- package/dist/sheets/sheets-tab.d.ts.map +1 -0
- package/dist/sheets/sheets-tab.js +88 -0
- package/dist/sheets/sheets-tab.js.map +1 -0
- package/dist/sheets/spreadsheets.d.ts +6 -0
- package/dist/sheets/spreadsheets.d.ts.map +1 -0
- package/dist/sheets/spreadsheets.js +88 -0
- package/dist/sheets/spreadsheets.js.map +1 -0
- package/package.json +33 -0
- package/src/__tests__/calendar.integration.test.ts +152 -0
- package/src/__tests__/forms.integration.test.ts +403 -0
- package/src/__tests__/gdrive.integration.test.ts +253 -0
- package/src/__tests__/gmail.integration.test.ts +294 -0
- package/src/__tests__/gslides.integration.test.ts +195 -0
- package/src/__tests__/sheets.integration.test.ts +234 -0
- package/src/agent/crud.ts +54 -0
- package/src/agent/memory.ts +95 -0
- package/src/agent/permissions.ts +54 -0
- package/src/calendar/accounts.ts +25 -0
- package/src/calendar/auth.ts +45 -0
- package/src/calendar/calendars.ts +32 -0
- package/src/calendar/create.ts +44 -0
- package/src/calendar/delete.ts +27 -0
- package/src/calendar/events.ts +45 -0
- package/src/calendar/get.ts +27 -0
- package/src/calendar/update.ts +44 -0
- package/src/cli.ts +41 -0
- package/src/commands/agent.ts +128 -0
- package/src/commands/calendar.ts +127 -0
- package/src/commands/cron.ts +126 -0
- package/src/commands/forms.ts +158 -0
- package/src/commands/gdrive.ts +225 -0
- package/src/commands/gmail.ts +290 -0
- package/src/commands/gslides.ts +188 -0
- package/src/commands/sheets.ts +193 -0
- package/src/cron/daemon.ts +143 -0
- package/src/cron/handlers.ts +78 -0
- package/src/cron/logger.ts +20 -0
- package/src/cron/progress.ts +90 -0
- package/src/cron/ralph-wiggum.ts +172 -0
- package/src/forms/accounts.ts +24 -0
- package/src/forms/auth.ts +45 -0
- package/src/forms/create.ts +34 -0
- package/src/forms/get.ts +26 -0
- package/src/forms/list.ts +38 -0
- package/src/forms/questions.ts +209 -0
- package/src/forms/responses.ts +50 -0
- package/src/forms/update.ts +57 -0
- package/src/gdrive/about.ts +36 -0
- package/src/gdrive/accounts.ts +24 -0
- package/src/gdrive/auth.ts +55 -0
- package/src/gdrive/files.ts +237 -0
- package/src/gdrive/folders.ts +58 -0
- package/src/gdrive/search.ts +30 -0
- package/src/gdrive/sharing.ts +72 -0
- package/src/gmail/accounts.ts +22 -0
- package/src/gmail/auth.ts +53 -0
- package/src/gmail/drafts.ts +166 -0
- package/src/gmail/get.ts +195 -0
- package/src/gmail/inbox.ts +78 -0
- package/src/gmail/labels.ts +69 -0
- package/src/gmail/modify.ts +89 -0
- package/src/gmail/send.ts +424 -0
- package/src/gmail/threads.ts +68 -0
- package/src/gslides/accounts.ts +24 -0
- package/src/gslides/auth.ts +54 -0
- package/src/gslides/presentations.ts +384 -0
- package/src/lib/config.ts +14 -0
- package/src/lib/media-utils.ts +82 -0
- package/src/lib/output.ts +13 -0
- package/src/sheets/accounts.ts +24 -0
- package/src/sheets/auth.ts +60 -0
- package/src/sheets/cells.ts +112 -0
- package/src/sheets/format.ts +114 -0
- package/src/sheets/sheets-tab.ts +109 -0
- package/src/sheets/spreadsheets.ts +106 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { getAgentsDir } from "@digitalpresence/cliclaw-auth";
|
|
4
|
+
import type { TranscriptBlock } from "./ralph-wiggum.js";
|
|
5
|
+
|
|
6
|
+
export interface CronRunLog {
|
|
7
|
+
jobId: string;
|
|
8
|
+
agentName: string;
|
|
9
|
+
startedAt: string;
|
|
10
|
+
finishedAt: string;
|
|
11
|
+
iterations: number;
|
|
12
|
+
completed: boolean;
|
|
13
|
+
totalCostUsd: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
transcript?: TranscriptBlock[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getCronDir(agentName: string, jobId: string): string {
|
|
19
|
+
return join(getAgentsDir(), agentName, "cron", jobId);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getProgressFilePath(agentName: string, jobId: string): string {
|
|
23
|
+
return join(getCronDir(agentName, jobId), "progress.md");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ensureCronDirs(agentName: string, jobId: string): void {
|
|
27
|
+
const cronDir = getCronDir(agentName, jobId);
|
|
28
|
+
const runsDir = join(cronDir, "runs");
|
|
29
|
+
if (!existsSync(runsDir)) {
|
|
30
|
+
mkdirSync(runsDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function writeRunLog(agentName: string, jobId: string, log: CronRunLog): void {
|
|
35
|
+
ensureCronDirs(agentName, jobId);
|
|
36
|
+
const runsDir = join(getCronDir(agentName, jobId), "runs");
|
|
37
|
+
const filename = `${log.startedAt.replace(/[:.]/g, "-")}.json`;
|
|
38
|
+
writeFileSync(join(runsDir, filename), JSON.stringify(log, null, 2), "utf-8");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RunningMarker {
|
|
42
|
+
startedAt: string;
|
|
43
|
+
pid: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function writeRunningMarker(agentName: string, jobId: string, startedAt: string): void {
|
|
47
|
+
ensureCronDirs(agentName, jobId);
|
|
48
|
+
const marker: RunningMarker = { startedAt, pid: process.pid };
|
|
49
|
+
writeFileSync(join(getCronDir(agentName, jobId), "running.json"), JSON.stringify(marker), "utf-8");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function clearRunningMarker(agentName: string, jobId: string): void {
|
|
53
|
+
const path = join(getCronDir(agentName, jobId), "running.json");
|
|
54
|
+
if (existsSync(path)) unlinkSync(path);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getRunningMarker(agentName: string, jobId: string): RunningMarker | null {
|
|
58
|
+
const path = join(getCronDir(agentName, jobId), "running.json");
|
|
59
|
+
if (!existsSync(path)) return null;
|
|
60
|
+
try {
|
|
61
|
+
const marker = JSON.parse(readFileSync(path, "utf-8")) as RunningMarker;
|
|
62
|
+
// Verify the process is still alive
|
|
63
|
+
try {
|
|
64
|
+
process.kill(marker.pid, 0);
|
|
65
|
+
return marker;
|
|
66
|
+
} catch {
|
|
67
|
+
// Process is dead — stale marker, clean up
|
|
68
|
+
unlinkSync(path);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function listRunLogs(agentName: string, jobId: string): CronRunLog[] {
|
|
77
|
+
const runsDir = join(getCronDir(agentName, jobId), "runs");
|
|
78
|
+
if (!existsSync(runsDir)) return [];
|
|
79
|
+
return readdirSync(runsDir)
|
|
80
|
+
.filter((f) => f.endsWith(".json"))
|
|
81
|
+
.sort()
|
|
82
|
+
.map((f) => {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(readFileSync(join(runsDir, f), "utf-8")) as CronRunLog;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
.filter((l): l is CronRunLog => l !== null);
|
|
90
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, realpathSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import type { AgentStore, CronJobConfig } from "@digitalpresence/cliclaw-auth";
|
|
6
|
+
import { getProgressFilePath, ensureCronDirs, writeRunningMarker, clearRunningMarker } from "./progress.js";
|
|
7
|
+
import { cronLog } from "./logger.js";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const CONTINUE_MARKER = "NEEDS_MORE_ITERATIONS";
|
|
13
|
+
|
|
14
|
+
export type TranscriptBlock =
|
|
15
|
+
| { type: "assistant"; content: string }
|
|
16
|
+
| { type: "tool"; name: string; input?: string; done: boolean };
|
|
17
|
+
|
|
18
|
+
export interface RalphWiggumResult {
|
|
19
|
+
completed: boolean;
|
|
20
|
+
iterations: number;
|
|
21
|
+
totalCostUsd: number;
|
|
22
|
+
transcript: TranscriptBlock[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function executeRalphWiggumLoop(
|
|
26
|
+
store: AgentStore,
|
|
27
|
+
agentName: string,
|
|
28
|
+
job: CronJobConfig,
|
|
29
|
+
startedAt?: string,
|
|
30
|
+
): Promise<RalphWiggumResult> {
|
|
31
|
+
ensureCronDirs(agentName, job.id);
|
|
32
|
+
writeRunningMarker(agentName, job.id, startedAt ?? new Date().toISOString());
|
|
33
|
+
|
|
34
|
+
const workspacePath = store.workspacePath(agentName);
|
|
35
|
+
const progressFile = getProgressFilePath(agentName, job.id);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Ensure cliclaw CLI is in PATH
|
|
39
|
+
const cleanEnv = { ...process.env };
|
|
40
|
+
delete cleanEnv.CLAUDECODE;
|
|
41
|
+
|
|
42
|
+
const monorepoRoot = join(__dirname, "..", "..", "..");
|
|
43
|
+
const binScript = join(monorepoRoot, "packages", "cliclaw", "dist", "cli.js");
|
|
44
|
+
if (existsSync(binScript)) {
|
|
45
|
+
const resolvedBin = realpathSync(binScript);
|
|
46
|
+
const localBin = join(workspacePath, ".bin");
|
|
47
|
+
if (!existsSync(localBin)) mkdirSync(localBin, { recursive: true });
|
|
48
|
+
const wrapper = join(localBin, "cliclaw");
|
|
49
|
+
writeFileSync(wrapper, `#!/bin/sh\nexec node "${resolvedBin}" "$@"\n`, { mode: 0o755 });
|
|
50
|
+
cleanEnv.PATH = `${localBin}:${cleanEnv.PATH ?? ""}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let totalCostUsd = 0;
|
|
54
|
+
let sessionId: string | undefined;
|
|
55
|
+
const transcript: TranscriptBlock[] = [];
|
|
56
|
+
|
|
57
|
+
for (let iteration = 1; iteration <= job.maxIterations; iteration++) {
|
|
58
|
+
cronLog("info", `Iteration ${iteration}/${job.maxIterations}`, agentName, job.id);
|
|
59
|
+
|
|
60
|
+
// Clear the continue marker before each iteration
|
|
61
|
+
if (existsSync(progressFile)) {
|
|
62
|
+
const content = readFileSync(progressFile, "utf-8");
|
|
63
|
+
if (content.includes(CONTINUE_MARKER)) {
|
|
64
|
+
writeFileSync(progressFile, content.replace(CONTINUE_MARKER, "").trim() + "\n", "utf-8");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let prompt: string;
|
|
69
|
+
if (iteration === 1) {
|
|
70
|
+
prompt = [
|
|
71
|
+
`You are executing a scheduled task. Here are your instructions:`,
|
|
72
|
+
``,
|
|
73
|
+
`${job.task}`,
|
|
74
|
+
``,
|
|
75
|
+
`Write your output/progress to: ${progressFile}`,
|
|
76
|
+
``,
|
|
77
|
+
`If you CANNOT finish the task in this iteration and need to be re-invoked, write "${CONTINUE_MARKER}" anywhere in ${progressFile}. Otherwise, just complete the task normally — no special signal is needed.`,
|
|
78
|
+
].join("\n");
|
|
79
|
+
} else {
|
|
80
|
+
prompt = [
|
|
81
|
+
`You are continuing a scheduled task. Read your progress file at: ${progressFile}`,
|
|
82
|
+
`Continue from where you left off.`,
|
|
83
|
+
``,
|
|
84
|
+
`Original task: ${job.task}`,
|
|
85
|
+
``,
|
|
86
|
+
`If you CANNOT finish the task in this iteration and need to be re-invoked, write "${CONTINUE_MARKER}" anywhere in ${progressFile}. Otherwise, just complete the task normally — no special signal is needed.`,
|
|
87
|
+
].join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const conversation = query({
|
|
91
|
+
prompt,
|
|
92
|
+
options: {
|
|
93
|
+
cwd: workspacePath,
|
|
94
|
+
env: cleanEnv,
|
|
95
|
+
systemPrompt: { type: "preset" as const, preset: "claude_code" as const },
|
|
96
|
+
settingSources: ["project"],
|
|
97
|
+
includePartialMessages: true,
|
|
98
|
+
allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
|
|
99
|
+
...(sessionId ? { resume: sessionId } : {}),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
let currentToolInput = "";
|
|
104
|
+
|
|
105
|
+
for await (const event of conversation) {
|
|
106
|
+
const msg = event as SDKMessage;
|
|
107
|
+
|
|
108
|
+
if (msg.type === "stream_event" && (msg as any).event) {
|
|
109
|
+
const streamEvent = (msg as any).event as {
|
|
110
|
+
type: string;
|
|
111
|
+
content_block?: { type: string; name?: string; id?: string };
|
|
112
|
+
delta?: { type: string; text?: string; partial_json?: string };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (streamEvent.type === "content_block_start" && streamEvent.content_block?.type === "tool_use") {
|
|
116
|
+
transcript.push({ type: "tool", name: streamEvent.content_block.name ?? "unknown", done: false });
|
|
117
|
+
currentToolInput = "";
|
|
118
|
+
} else if (streamEvent.type === "content_block_delta") {
|
|
119
|
+
if (streamEvent.delta?.type === "text_delta" && streamEvent.delta.text) {
|
|
120
|
+
const last = transcript[transcript.length - 1];
|
|
121
|
+
if (last?.type === "assistant") {
|
|
122
|
+
last.content += streamEvent.delta.text;
|
|
123
|
+
} else {
|
|
124
|
+
transcript.push({ type: "assistant", content: streamEvent.delta.text });
|
|
125
|
+
}
|
|
126
|
+
} else if (streamEvent.delta?.type === "input_json_delta" && streamEvent.delta.partial_json) {
|
|
127
|
+
currentToolInput += streamEvent.delta.partial_json;
|
|
128
|
+
}
|
|
129
|
+
} else if (streamEvent.type === "content_block_stop" && currentToolInput) {
|
|
130
|
+
const lastTool = [...transcript].reverse().find((b) => b.type === "tool" && !b.done);
|
|
131
|
+
if (lastTool && lastTool.type === "tool") {
|
|
132
|
+
try {
|
|
133
|
+
lastTool.input = JSON.stringify(JSON.parse(currentToolInput));
|
|
134
|
+
} catch {
|
|
135
|
+
// incomplete JSON
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
currentToolInput = "";
|
|
139
|
+
}
|
|
140
|
+
} else if ((msg as any).type === "tool_result") {
|
|
141
|
+
const lastTool = [...transcript].reverse().find((b) => b.type === "tool" && !b.done);
|
|
142
|
+
if (lastTool && lastTool.type === "tool") {
|
|
143
|
+
lastTool.done = true;
|
|
144
|
+
}
|
|
145
|
+
} else if (msg.type === "result") {
|
|
146
|
+
if ("total_cost_usd" in msg) {
|
|
147
|
+
totalCostUsd += (msg.total_cost_usd as number) ?? 0;
|
|
148
|
+
}
|
|
149
|
+
if (msg.session_id) {
|
|
150
|
+
sessionId = msg.session_id;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if the agent needs more iterations by reading the progress file
|
|
156
|
+
const needsMore = existsSync(progressFile) &&
|
|
157
|
+
readFileSync(progressFile, "utf-8").includes(CONTINUE_MARKER);
|
|
158
|
+
|
|
159
|
+
if (!needsMore) {
|
|
160
|
+
cronLog("info", `Task completed at iteration ${iteration}`, agentName, job.id);
|
|
161
|
+
return { completed: true, iterations: iteration, totalCostUsd, transcript };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
cronLog("info", `Agent requested continuation (${CONTINUE_MARKER} found in progress file)`, agentName, job.id);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
cronLog("warn", `Max iterations (${job.maxIterations}) reached`, agentName, job.id);
|
|
168
|
+
return { completed: false, iterations: job.maxIterations, totalCostUsd, transcript };
|
|
169
|
+
} finally {
|
|
170
|
+
clearRunningMarker(agentName, job.id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { google } from "googleapis";
|
|
2
|
+
import type { OAuthClientManager } from "@digitalpresence/cliclaw-auth";
|
|
3
|
+
import { outputJson } from "../lib/output.js";
|
|
4
|
+
|
|
5
|
+
export async function handleAccounts(clientManager: OAuthClientManager): Promise<void> {
|
|
6
|
+
const allAccounts = clientManager.listAccounts();
|
|
7
|
+
const formsAccounts = allAccounts.filter((a) => a.startsWith("forms:"));
|
|
8
|
+
|
|
9
|
+
const accounts = await Promise.all(
|
|
10
|
+
formsAccounts.map(async (tokenKey) => {
|
|
11
|
+
const account = tokenKey.replace("forms:", "");
|
|
12
|
+
try {
|
|
13
|
+
const client = clientManager.getClient(tokenKey);
|
|
14
|
+
const oauth2 = google.oauth2({ version: "v2", auth: client });
|
|
15
|
+
const res = await oauth2.userinfo.get();
|
|
16
|
+
return { account, email: res.data.email ?? null };
|
|
17
|
+
} catch {
|
|
18
|
+
return { account, email: null };
|
|
19
|
+
}
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
outputJson({ accounts });
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { google } from "googleapis";
|
|
2
|
+
import type { OAuthClientManager } from "@digitalpresence/cliclaw-auth";
|
|
3
|
+
import { waitForOAuthCallback, getFormsAuthUrl } from "@digitalpresence/cliclaw-auth";
|
|
4
|
+
import { outputJson, outputError } from "../lib/output.js";
|
|
5
|
+
|
|
6
|
+
export async function handleAuth(clientManager: OAuthClientManager, port: number, account: string): Promise<void> {
|
|
7
|
+
const tokenKey = `forms:${account}`;
|
|
8
|
+
|
|
9
|
+
// Check if already authenticated
|
|
10
|
+
try {
|
|
11
|
+
const client = clientManager.getClient(tokenKey);
|
|
12
|
+
const creds = client.credentials;
|
|
13
|
+
if (creds && (creds.refresh_token || creds.access_token)) {
|
|
14
|
+
const drive = google.drive({ version: "v3", auth: client });
|
|
15
|
+
await drive.files.list({ pageSize: 1, q: "mimeType='application/vnd.google-apps.form'" });
|
|
16
|
+
outputJson({
|
|
17
|
+
status: "already_authenticated",
|
|
18
|
+
account,
|
|
19
|
+
message: "Existing session is still valid. No re-authentication needed.",
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// Tokens invalid — proceed with re-auth
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const open = (await import("open")).default;
|
|
29
|
+
const rawClient = clientManager.getRawClient();
|
|
30
|
+
|
|
31
|
+
const tokens = await waitForOAuthCallback(rawClient, port, (url) => {
|
|
32
|
+
console.error(`Opening browser for Google Forms authentication...`);
|
|
33
|
+
console.error(url);
|
|
34
|
+
open(url).catch(() => {
|
|
35
|
+
console.error("Could not open browser. Please visit the URL above manually.");
|
|
36
|
+
});
|
|
37
|
+
}, getFormsAuthUrl);
|
|
38
|
+
|
|
39
|
+
clientManager.setCredentials(tokenKey, tokens);
|
|
40
|
+
|
|
41
|
+
outputJson({ status: "authenticated", account });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
outputError("auth_failed", err instanceof Error ? err.message : String(err));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { google } from "googleapis";
|
|
2
|
+
import type { OAuthClientManager } from "@digitalpresence/cliclaw-auth";
|
|
3
|
+
import { outputJson, outputError, outputAuthRequired } from "../lib/output.js";
|
|
4
|
+
|
|
5
|
+
function getForms(clientManager: OAuthClientManager, tokenKey: string) {
|
|
6
|
+
const client = clientManager.getClient(tokenKey);
|
|
7
|
+
if (!client.credentials?.access_token && !client.credentials?.refresh_token) {
|
|
8
|
+
outputAuthRequired("forms");
|
|
9
|
+
}
|
|
10
|
+
return google.forms({ version: "v1", auth: client });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function handleCreateForm(
|
|
14
|
+
clientManager: OAuthClientManager,
|
|
15
|
+
account: string,
|
|
16
|
+
title: string,
|
|
17
|
+
documentTitle?: string,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const tokenKey = `forms:${account}`;
|
|
20
|
+
try {
|
|
21
|
+
const forms = getForms(clientManager, tokenKey);
|
|
22
|
+
const res = await forms.forms.create({
|
|
23
|
+
requestBody: {
|
|
24
|
+
info: {
|
|
25
|
+
title,
|
|
26
|
+
documentTitle: documentTitle ?? title,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
outputJson({ success: true, ...res.data });
|
|
31
|
+
} catch (err) {
|
|
32
|
+
outputError("create_form_failed", err instanceof Error ? err.message : String(err));
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/forms/get.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { google } from "googleapis";
|
|
2
|
+
import type { OAuthClientManager } from "@digitalpresence/cliclaw-auth";
|
|
3
|
+
import { outputJson, outputError, outputAuthRequired } from "../lib/output.js";
|
|
4
|
+
|
|
5
|
+
function getForms(clientManager: OAuthClientManager, tokenKey: string) {
|
|
6
|
+
const client = clientManager.getClient(tokenKey);
|
|
7
|
+
if (!client.credentials?.access_token && !client.credentials?.refresh_token) {
|
|
8
|
+
outputAuthRequired("forms");
|
|
9
|
+
}
|
|
10
|
+
return google.forms({ version: "v1", auth: client });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function handleGetForm(
|
|
14
|
+
clientManager: OAuthClientManager,
|
|
15
|
+
account: string,
|
|
16
|
+
formId: string,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const tokenKey = `forms:${account}`;
|
|
19
|
+
try {
|
|
20
|
+
const forms = getForms(clientManager, tokenKey);
|
|
21
|
+
const res = await forms.forms.get({ formId });
|
|
22
|
+
outputJson(res.data);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
outputError("get_form_failed", err instanceof Error ? err.message : String(err));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { google } from "googleapis";
|
|
2
|
+
import type { OAuthClientManager } from "@digitalpresence/cliclaw-auth";
|
|
3
|
+
import { outputJson, outputError, outputAuthRequired } from "../lib/output.js";
|
|
4
|
+
|
|
5
|
+
function getDrive(clientManager: OAuthClientManager, tokenKey: string) {
|
|
6
|
+
const client = clientManager.getClient(tokenKey);
|
|
7
|
+
if (!client.credentials?.access_token && !client.credentials?.refresh_token) {
|
|
8
|
+
outputAuthRequired("forms");
|
|
9
|
+
}
|
|
10
|
+
return google.drive({ version: "v3", auth: client });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function handleList(
|
|
14
|
+
clientManager: OAuthClientManager,
|
|
15
|
+
account: string,
|
|
16
|
+
maxResults: number,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const tokenKey = `forms:${account}`;
|
|
19
|
+
try {
|
|
20
|
+
const drive = getDrive(clientManager, tokenKey);
|
|
21
|
+
const res = await drive.files.list({
|
|
22
|
+
pageSize: maxResults,
|
|
23
|
+
q: "mimeType='application/vnd.google-apps.form' and trashed=false",
|
|
24
|
+
fields: "files(id,name,createdTime,modifiedTime,webViewLink)",
|
|
25
|
+
orderBy: "modifiedTime desc",
|
|
26
|
+
});
|
|
27
|
+
const forms = (res.data.files ?? []).map((f) => ({
|
|
28
|
+
id: f.id,
|
|
29
|
+
name: f.name,
|
|
30
|
+
createdTime: f.createdTime,
|
|
31
|
+
modifiedTime: f.modifiedTime,
|
|
32
|
+
webViewLink: f.webViewLink,
|
|
33
|
+
}));
|
|
34
|
+
outputJson(forms);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
outputError("list_forms_failed", err instanceof Error ? err.message : String(err));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { google } from "googleapis";
|
|
2
|
+
import type { OAuthClientManager } from "@digitalpresence/cliclaw-auth";
|
|
3
|
+
import { outputJson, outputError, outputAuthRequired } from "../lib/output.js";
|
|
4
|
+
|
|
5
|
+
function getForms(clientManager: OAuthClientManager, tokenKey: string) {
|
|
6
|
+
const client = clientManager.getClient(tokenKey);
|
|
7
|
+
if (!client.credentials?.access_token && !client.credentials?.refresh_token) {
|
|
8
|
+
outputAuthRequired("forms");
|
|
9
|
+
}
|
|
10
|
+
return google.forms({ version: "v1", auth: client });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function handleQuestions(
|
|
14
|
+
clientManager: OAuthClientManager,
|
|
15
|
+
account: string,
|
|
16
|
+
formId: string,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const tokenKey = `forms:${account}`;
|
|
19
|
+
try {
|
|
20
|
+
const forms = getForms(clientManager, tokenKey);
|
|
21
|
+
const res = await forms.forms.get({ formId });
|
|
22
|
+
const items = res.data.items ?? [];
|
|
23
|
+
const questions = items
|
|
24
|
+
.filter((item) => item.questionItem)
|
|
25
|
+
.map((item) => ({
|
|
26
|
+
itemId: item.itemId,
|
|
27
|
+
title: item.title,
|
|
28
|
+
description: item.description,
|
|
29
|
+
questionId: item.questionItem?.question?.questionId,
|
|
30
|
+
required: item.questionItem?.question?.required ?? false,
|
|
31
|
+
type: getQuestionType(item.questionItem?.question),
|
|
32
|
+
}));
|
|
33
|
+
outputJson(questions);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
outputError("questions_failed", err instanceof Error ? err.message : String(err));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getQuestionType(question: unknown): string {
|
|
40
|
+
if (!question || typeof question !== "object") return "unknown";
|
|
41
|
+
const q = question as Record<string, unknown>;
|
|
42
|
+
if (q.choiceQuestion) return "choice";
|
|
43
|
+
if (q.textQuestion) return "text";
|
|
44
|
+
if (q.scaleQuestion) return "scale";
|
|
45
|
+
if (q.dateQuestion) return "date";
|
|
46
|
+
if (q.timeQuestion) return "time";
|
|
47
|
+
if (q.fileUploadQuestion) return "fileUpload";
|
|
48
|
+
return "unknown";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function handleAddQuestion(
|
|
52
|
+
clientManager: OAuthClientManager,
|
|
53
|
+
account: string,
|
|
54
|
+
formId: string,
|
|
55
|
+
title: string,
|
|
56
|
+
type: string,
|
|
57
|
+
required: boolean,
|
|
58
|
+
options?: string[],
|
|
59
|
+
index?: number,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const tokenKey = `forms:${account}`;
|
|
62
|
+
try {
|
|
63
|
+
const forms = getForms(clientManager, tokenKey);
|
|
64
|
+
|
|
65
|
+
const question: Record<string, unknown> = { required };
|
|
66
|
+
|
|
67
|
+
switch (type) {
|
|
68
|
+
case "text":
|
|
69
|
+
question.textQuestion = { paragraph: false };
|
|
70
|
+
break;
|
|
71
|
+
case "paragraph":
|
|
72
|
+
question.textQuestion = { paragraph: true };
|
|
73
|
+
break;
|
|
74
|
+
case "choice":
|
|
75
|
+
question.choiceQuestion = {
|
|
76
|
+
type: "RADIO",
|
|
77
|
+
options: (options ?? ["Option 1"]).map((o) => ({ value: o })),
|
|
78
|
+
};
|
|
79
|
+
break;
|
|
80
|
+
case "checkbox":
|
|
81
|
+
question.choiceQuestion = {
|
|
82
|
+
type: "CHECKBOX",
|
|
83
|
+
options: (options ?? ["Option 1"]).map((o) => ({ value: o })),
|
|
84
|
+
};
|
|
85
|
+
break;
|
|
86
|
+
case "dropdown":
|
|
87
|
+
question.choiceQuestion = {
|
|
88
|
+
type: "DROP_DOWN",
|
|
89
|
+
options: (options ?? ["Option 1"]).map((o) => ({ value: o })),
|
|
90
|
+
};
|
|
91
|
+
break;
|
|
92
|
+
case "scale":
|
|
93
|
+
question.scaleQuestion = { low: 1, high: 5 };
|
|
94
|
+
break;
|
|
95
|
+
case "date":
|
|
96
|
+
question.dateQuestion = {};
|
|
97
|
+
break;
|
|
98
|
+
case "time":
|
|
99
|
+
question.timeQuestion = {};
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
question.textQuestion = { paragraph: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const res = await forms.forms.batchUpdate({
|
|
106
|
+
formId,
|
|
107
|
+
requestBody: {
|
|
108
|
+
requests: [
|
|
109
|
+
{
|
|
110
|
+
createItem: {
|
|
111
|
+
item: {
|
|
112
|
+
title,
|
|
113
|
+
questionItem: { question },
|
|
114
|
+
},
|
|
115
|
+
location: { index: index ?? 0 },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
outputJson({ success: true, ...res.data });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
outputError("add_question_failed", err instanceof Error ? err.message : String(err));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function handleUpdateQuestion(
|
|
129
|
+
clientManager: OAuthClientManager,
|
|
130
|
+
account: string,
|
|
131
|
+
formId: string,
|
|
132
|
+
itemIndex: number,
|
|
133
|
+
title?: string,
|
|
134
|
+
required?: boolean,
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
const tokenKey = `forms:${account}`;
|
|
137
|
+
try {
|
|
138
|
+
const forms = getForms(clientManager, tokenKey);
|
|
139
|
+
|
|
140
|
+
// First get the current form to find the item
|
|
141
|
+
const current = await forms.forms.get({ formId });
|
|
142
|
+
const items = current.data.items ?? [];
|
|
143
|
+
if (itemIndex >= items.length) {
|
|
144
|
+
outputError("update_question_failed", `Item index ${itemIndex} out of range (form has ${items.length} items)`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const item = items[itemIndex];
|
|
148
|
+
const updateMasks: string[] = [];
|
|
149
|
+
|
|
150
|
+
if (title !== undefined) {
|
|
151
|
+
item.title = title;
|
|
152
|
+
updateMasks.push("title");
|
|
153
|
+
}
|
|
154
|
+
if (required !== undefined && item.questionItem?.question) {
|
|
155
|
+
item.questionItem.question.required = required;
|
|
156
|
+
updateMasks.push("questionItem.question.required");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (updateMasks.length === 0) {
|
|
160
|
+
outputError("update_question_failed", "No fields to update.");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const res = await forms.forms.batchUpdate({
|
|
164
|
+
formId,
|
|
165
|
+
requestBody: {
|
|
166
|
+
requests: [
|
|
167
|
+
{
|
|
168
|
+
updateItem: {
|
|
169
|
+
item,
|
|
170
|
+
location: { index: itemIndex },
|
|
171
|
+
updateMask: updateMasks.join(","),
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
outputJson({ success: true, ...res.data });
|
|
179
|
+
} catch (err) {
|
|
180
|
+
outputError("update_question_failed", err instanceof Error ? err.message : String(err));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function handleDeleteQuestion(
|
|
185
|
+
clientManager: OAuthClientManager,
|
|
186
|
+
account: string,
|
|
187
|
+
formId: string,
|
|
188
|
+
itemIndex: number,
|
|
189
|
+
): Promise<void> {
|
|
190
|
+
const tokenKey = `forms:${account}`;
|
|
191
|
+
try {
|
|
192
|
+
const forms = getForms(clientManager, tokenKey);
|
|
193
|
+
const res = await forms.forms.batchUpdate({
|
|
194
|
+
formId,
|
|
195
|
+
requestBody: {
|
|
196
|
+
requests: [
|
|
197
|
+
{
|
|
198
|
+
deleteItem: {
|
|
199
|
+
location: { index: itemIndex },
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
outputJson({ success: true, ...res.data });
|
|
206
|
+
} catch (err) {
|
|
207
|
+
outputError("delete_question_failed", err instanceof Error ? err.message : String(err));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { google } from "googleapis";
|
|
2
|
+
import type { OAuthClientManager } from "@digitalpresence/cliclaw-auth";
|
|
3
|
+
import { outputJson, outputError, outputAuthRequired } from "../lib/output.js";
|
|
4
|
+
|
|
5
|
+
function getForms(clientManager: OAuthClientManager, tokenKey: string) {
|
|
6
|
+
const client = clientManager.getClient(tokenKey);
|
|
7
|
+
if (!client.credentials?.access_token && !client.credentials?.refresh_token) {
|
|
8
|
+
outputAuthRequired("forms");
|
|
9
|
+
}
|
|
10
|
+
return google.forms({ version: "v1", auth: client });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function handleResponses(
|
|
14
|
+
clientManager: OAuthClientManager,
|
|
15
|
+
account: string,
|
|
16
|
+
formId: string,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const tokenKey = `forms:${account}`;
|
|
19
|
+
try {
|
|
20
|
+
const forms = getForms(clientManager, tokenKey);
|
|
21
|
+
const res = await forms.forms.responses.list({ formId });
|
|
22
|
+
const responses = (res.data.responses ?? []).map((r) => ({
|
|
23
|
+
responseId: r.responseId,
|
|
24
|
+
createTime: r.createTime,
|
|
25
|
+
lastSubmittedTime: r.lastSubmittedTime,
|
|
26
|
+
respondentEmail: r.respondentEmail,
|
|
27
|
+
totalScore: r.totalScore,
|
|
28
|
+
answers: r.answers,
|
|
29
|
+
}));
|
|
30
|
+
outputJson(responses);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
outputError("responses_failed", err instanceof Error ? err.message : String(err));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function handleGetResponse(
|
|
37
|
+
clientManager: OAuthClientManager,
|
|
38
|
+
account: string,
|
|
39
|
+
formId: string,
|
|
40
|
+
responseId: string,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const tokenKey = `forms:${account}`;
|
|
43
|
+
try {
|
|
44
|
+
const forms = getForms(clientManager, tokenKey);
|
|
45
|
+
const res = await forms.forms.responses.get({ formId, responseId });
|
|
46
|
+
outputJson(res.data);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
outputError("get_response_failed", err instanceof Error ? err.message : String(err));
|
|
49
|
+
}
|
|
50
|
+
}
|