@a5c-ai/babysitter-paperclip 0.0.2-staging.02a0ee21
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/BABYSITTER.md +46 -0
- package/README.md +229 -0
- package/esbuild.config.mjs +12 -0
- package/package.json +29 -0
- package/src/__tests__/delegating-adapter.test.ts +85 -0
- package/src/__tests__/types.test.ts +29 -0
- package/src/babysitter-bridge.ts +313 -0
- package/src/delegating-adapter.ts +97 -0
- package/src/harness-plugin-installer.ts +202 -0
- package/src/manifest.ts +101 -0
- package/src/types.ts +130 -0
- package/src/ui/BabysitterDashboard.tsx +142 -0
- package/src/ui/BabysitterSidebar.tsx +123 -0
- package/src/ui/BreakpointApproval.tsx +212 -0
- package/src/ui/RunDetailTab.tsx +169 -0
- package/src/ui/index.tsx +9 -0
- package/src/ui/styles.ts +75 -0
- package/src/worker.ts +595 -0
- package/tsconfig.json +19 -0
- package/versions.json +3 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI bridge to the babysitter SDK.
|
|
3
|
+
*
|
|
4
|
+
* Wraps babysitter CLI commands as typed async functions for use by the
|
|
5
|
+
* Paperclip plugin worker. All operations shell out to the `babysitter` CLI
|
|
6
|
+
* to maintain a clean process boundary.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import type {
|
|
12
|
+
RunDetail,
|
|
13
|
+
RunEvent,
|
|
14
|
+
PendingEffect,
|
|
15
|
+
PendingBreakpoint,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
const exec = promisify(execFile);
|
|
19
|
+
|
|
20
|
+
const CLI = "babysitter";
|
|
21
|
+
|
|
22
|
+
/** Execute a babysitter CLI command and return parsed JSON. */
|
|
23
|
+
async function runCli<T>(
|
|
24
|
+
args: string[],
|
|
25
|
+
options?: { cwd?: string }
|
|
26
|
+
): Promise<T> {
|
|
27
|
+
const { stdout } = await exec(CLI, [...args, "--json"], {
|
|
28
|
+
cwd: options?.cwd,
|
|
29
|
+
timeout: 60_000,
|
|
30
|
+
});
|
|
31
|
+
return JSON.parse(stdout) as T;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Create a new babysitter run. */
|
|
35
|
+
export async function createRun(opts: {
|
|
36
|
+
processId: string;
|
|
37
|
+
entry: string;
|
|
38
|
+
inputsFile: string;
|
|
39
|
+
runsDir?: string;
|
|
40
|
+
cwd?: string;
|
|
41
|
+
}): Promise<{ runId: string; runDir: string }> {
|
|
42
|
+
const args = [
|
|
43
|
+
"run:create",
|
|
44
|
+
"--process-id",
|
|
45
|
+
opts.processId,
|
|
46
|
+
"--entry",
|
|
47
|
+
opts.entry,
|
|
48
|
+
"--inputs",
|
|
49
|
+
opts.inputsFile,
|
|
50
|
+
];
|
|
51
|
+
if (opts.runsDir) args.push("--runs-dir", opts.runsDir);
|
|
52
|
+
return runCli(args, { cwd: opts.cwd });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Iterate a run until pending effects or completion. */
|
|
56
|
+
export async function iterateRun(
|
|
57
|
+
runDir: string,
|
|
58
|
+
options?: { cwd?: string }
|
|
59
|
+
): Promise<{
|
|
60
|
+
status: string;
|
|
61
|
+
nextActions: Array<{
|
|
62
|
+
effectId: string;
|
|
63
|
+
kind: string;
|
|
64
|
+
label: string;
|
|
65
|
+
taskId: string;
|
|
66
|
+
taskDef?: Record<string, unknown>;
|
|
67
|
+
}>;
|
|
68
|
+
metadata: Record<string, unknown>;
|
|
69
|
+
}> {
|
|
70
|
+
return runCli(["run:iterate", runDir], options);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Get run status. */
|
|
74
|
+
export async function getRunStatus(
|
|
75
|
+
runDir: string,
|
|
76
|
+
options?: { cwd?: string }
|
|
77
|
+
): Promise<{
|
|
78
|
+
state: string;
|
|
79
|
+
pendingByKind: Record<string, number>;
|
|
80
|
+
completionProof: string | null;
|
|
81
|
+
}> {
|
|
82
|
+
return runCli(["run:status", runDir], options);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Get run events. */
|
|
86
|
+
export async function getRunEvents(
|
|
87
|
+
runDir: string,
|
|
88
|
+
limit?: number,
|
|
89
|
+
options?: { cwd?: string }
|
|
90
|
+
): Promise<RunEvent[]> {
|
|
91
|
+
const args = ["run:events", runDir];
|
|
92
|
+
if (limit) args.push("--limit", String(limit));
|
|
93
|
+
return runCli(args, options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** List pending tasks. */
|
|
97
|
+
export async function listPendingTasks(
|
|
98
|
+
runDir: string,
|
|
99
|
+
options?: { cwd?: string }
|
|
100
|
+
): Promise<PendingEffect[]> {
|
|
101
|
+
return runCli(["task:list", runDir, "--pending"], options);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Show a specific task. */
|
|
105
|
+
export async function showTask(
|
|
106
|
+
runDir: string,
|
|
107
|
+
effectId: string,
|
|
108
|
+
options?: { cwd?: string }
|
|
109
|
+
): Promise<{ effect: PendingEffect; task: Record<string, unknown> | null }> {
|
|
110
|
+
return runCli(["task:show", runDir, effectId], options);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Post a task result (approve/reject breakpoint or post effect result). */
|
|
114
|
+
export async function postTaskResult(
|
|
115
|
+
runDir: string,
|
|
116
|
+
effectId: string,
|
|
117
|
+
result: {
|
|
118
|
+
status: "ok" | "error";
|
|
119
|
+
value: Record<string, unknown>;
|
|
120
|
+
},
|
|
121
|
+
options?: { cwd?: string }
|
|
122
|
+
): Promise<{ status: string }> {
|
|
123
|
+
return runCli(
|
|
124
|
+
[
|
|
125
|
+
"task:post",
|
|
126
|
+
runDir,
|
|
127
|
+
effectId,
|
|
128
|
+
"--status",
|
|
129
|
+
result.status,
|
|
130
|
+
"--value-inline",
|
|
131
|
+
JSON.stringify(result.value),
|
|
132
|
+
],
|
|
133
|
+
options
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Approve a breakpoint. */
|
|
138
|
+
export async function approveBreakpoint(
|
|
139
|
+
runDir: string,
|
|
140
|
+
effectId: string,
|
|
141
|
+
response?: string,
|
|
142
|
+
options?: { cwd?: string }
|
|
143
|
+
): Promise<{ status: string }> {
|
|
144
|
+
return postTaskResult(
|
|
145
|
+
runDir,
|
|
146
|
+
effectId,
|
|
147
|
+
{
|
|
148
|
+
status: "ok",
|
|
149
|
+
value: { approved: true, response: response ?? "Approved via Paperclip UI" },
|
|
150
|
+
},
|
|
151
|
+
options
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Reject a breakpoint. Note: uses --status ok with approved: false. */
|
|
156
|
+
export async function rejectBreakpoint(
|
|
157
|
+
runDir: string,
|
|
158
|
+
effectId: string,
|
|
159
|
+
feedback: string,
|
|
160
|
+
options?: { cwd?: string }
|
|
161
|
+
): Promise<{ status: string }> {
|
|
162
|
+
return postTaskResult(
|
|
163
|
+
runDir,
|
|
164
|
+
effectId,
|
|
165
|
+
{
|
|
166
|
+
status: "ok",
|
|
167
|
+
value: { approved: false, feedback },
|
|
168
|
+
},
|
|
169
|
+
options
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Extract pending breakpoints from a run with full metadata.
|
|
175
|
+
*
|
|
176
|
+
* This reads the task.json for each breakpoint effect to get the full payload
|
|
177
|
+
* including question, options, expert routing, tags, and strategy. This is the
|
|
178
|
+
* same metadata that the underlying harness (Claude Code, OpenClaw) uses to
|
|
179
|
+
* present breakpoints to users.
|
|
180
|
+
*
|
|
181
|
+
* The breakpoint lifecycle:
|
|
182
|
+
* 1. Process calls ctx.breakpoint(payload) in SDK
|
|
183
|
+
* 2. SDK writes task.json with kind:"breakpoint" + metadata.payload
|
|
184
|
+
* 3. run:iterate returns "waiting" with the breakpoint as a pending action
|
|
185
|
+
* 4. Underlying harness stop hook detects only-breakpoints-pending → allows exit
|
|
186
|
+
* 5. Paperclip polls run:status, sees pending breakpoint
|
|
187
|
+
* 6. Paperclip reads task.json metadata, surfaces in UI
|
|
188
|
+
* 7. User approves/rejects in Paperclip UI
|
|
189
|
+
* 8. Paperclip posts via task:post --status ok (ALWAYS ok, even for rejection)
|
|
190
|
+
* 9. Next run:iterate resolves the cached breakpoint result
|
|
191
|
+
*/
|
|
192
|
+
export async function getPendingBreakpoints(
|
|
193
|
+
runDir: string,
|
|
194
|
+
options?: { cwd?: string }
|
|
195
|
+
): Promise<PendingBreakpoint[]> {
|
|
196
|
+
const tasks = await listPendingTasks(runDir, options);
|
|
197
|
+
const breakpoints: PendingBreakpoint[] = [];
|
|
198
|
+
|
|
199
|
+
for (const t of tasks) {
|
|
200
|
+
if (t.kind !== "breakpoint") continue;
|
|
201
|
+
|
|
202
|
+
// Try to get full task metadata including question/options
|
|
203
|
+
let question: string | undefined;
|
|
204
|
+
let taskOptions: string[] | undefined;
|
|
205
|
+
let expert: string | string[] | undefined;
|
|
206
|
+
let tags: string[] | undefined;
|
|
207
|
+
let strategy: string | undefined;
|
|
208
|
+
let previousFeedback: string | undefined;
|
|
209
|
+
let attempt: number | undefined;
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const detail = await showTask(runDir, t.effectId, options);
|
|
213
|
+
const task = detail.task as Record<string, unknown> | null;
|
|
214
|
+
if (task) {
|
|
215
|
+
const metadata = task.metadata as Record<string, unknown> | undefined;
|
|
216
|
+
const payload = metadata?.payload as Record<string, unknown> | undefined;
|
|
217
|
+
if (payload) {
|
|
218
|
+
question = (payload.question ?? payload.title) as string | undefined;
|
|
219
|
+
taskOptions = payload.options as string[] | undefined;
|
|
220
|
+
expert = payload.expert as string | string[] | undefined;
|
|
221
|
+
tags = payload.tags as string[] | undefined;
|
|
222
|
+
strategy = payload.strategy as string | undefined;
|
|
223
|
+
previousFeedback = payload.previousFeedback as string | undefined;
|
|
224
|
+
attempt = payload.attempt as number | undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// Task metadata unavailable - use basic info
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
breakpoints.push({
|
|
232
|
+
effectId: t.effectId,
|
|
233
|
+
title: question ?? t.label,
|
|
234
|
+
question,
|
|
235
|
+
options: taskOptions,
|
|
236
|
+
expert,
|
|
237
|
+
tags,
|
|
238
|
+
strategy: strategy as PendingBreakpoint["strategy"],
|
|
239
|
+
previousFeedback,
|
|
240
|
+
attempt,
|
|
241
|
+
requestedAt: t.requestedAt,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return breakpoints;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Check if a run has ONLY breakpoints pending (no other effect types).
|
|
250
|
+
*
|
|
251
|
+
* This mirrors the check in the Claude Code stop hook (claudeCode.ts:578-598):
|
|
252
|
+
* when only breakpoints are pending, the harness allows exit because human
|
|
253
|
+
* action is required. This is the signal that Paperclip should surface
|
|
254
|
+
* breakpoints in the UI.
|
|
255
|
+
*/
|
|
256
|
+
export async function hasOnlyBreakpointsPending(
|
|
257
|
+
runDir: string,
|
|
258
|
+
options?: { cwd?: string }
|
|
259
|
+
): Promise<{ onlyBreakpoints: boolean; breakpointCount: number; otherCount: number }> {
|
|
260
|
+
const status = await getRunStatus(runDir, options);
|
|
261
|
+
const pending = status.pendingByKind;
|
|
262
|
+
const breakpointCount = pending.breakpoint ?? 0;
|
|
263
|
+
const otherCount = Object.entries(pending)
|
|
264
|
+
.filter(([k]) => k !== "breakpoint")
|
|
265
|
+
.reduce((sum, [, v]) => sum + v, 0);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
onlyBreakpoints: breakpointCount > 0 && otherCount === 0,
|
|
269
|
+
breakpointCount,
|
|
270
|
+
otherCount,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Install the babysitter plugin for a specific harness.
|
|
276
|
+
* Delegates to `babysitter harness:install-plugin <name>`.
|
|
277
|
+
*/
|
|
278
|
+
export async function installHarnessPlugin(
|
|
279
|
+
harnessName: string,
|
|
280
|
+
options?: { cwd?: string }
|
|
281
|
+
): Promise<{ success: boolean; output: string }> {
|
|
282
|
+
try {
|
|
283
|
+
const { stdout } = await exec(
|
|
284
|
+
CLI,
|
|
285
|
+
["harness:install-plugin", harnessName, "--json"],
|
|
286
|
+
{ cwd: options?.cwd, timeout: 120_000 }
|
|
287
|
+
);
|
|
288
|
+
return { success: true, output: stdout };
|
|
289
|
+
} catch (err) {
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
output: err instanceof Error ? err.message : String(err),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Discover available harnesses and their plugin status.
|
|
299
|
+
*/
|
|
300
|
+
export async function discoverHarnesses(
|
|
301
|
+
options?: { cwd?: string }
|
|
302
|
+
): Promise<Array<{ name: string; available: boolean; pluginInstalled?: boolean }>> {
|
|
303
|
+
try {
|
|
304
|
+
return await runCli(["harness:discover"], options);
|
|
305
|
+
} catch {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Build the run directory path from a runs dir and run ID. */
|
|
311
|
+
export function buildRunDir(runsDir: string, runId: string): string {
|
|
312
|
+
return `${runsDir}/${runId}`;
|
|
313
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegating adapter for Paperclip harness detection.
|
|
3
|
+
*
|
|
4
|
+
* Detects which underlying AI harness a Paperclip agent uses and delegates
|
|
5
|
+
* babysitter adapter operations to the appropriate harness adapter.
|
|
6
|
+
*
|
|
7
|
+
* Detection tiers (in priority order):
|
|
8
|
+
* 1. Agent metadata — read adapterType from agent config (e.g., claude_local -> claude-code)
|
|
9
|
+
* 2. Environment probing — check env vars for known harness signatures
|
|
10
|
+
* 3. Explicit config — fall back to plugin settings
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ADAPTER_TYPE_MAP, type HarnessDetectionResult } from "./types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect the underlying harness from a Paperclip agent's adapter type.
|
|
17
|
+
*
|
|
18
|
+
* @param adapterType - The agent's adapterType field (e.g., "claude_local")
|
|
19
|
+
* @param pluginConfig - Plugin settings for fallback detection
|
|
20
|
+
* @returns Detection result with harness name and confidence
|
|
21
|
+
*/
|
|
22
|
+
export function detectHarness(
|
|
23
|
+
adapterType?: string,
|
|
24
|
+
pluginConfig?: { defaultHarness?: string }
|
|
25
|
+
): HarnessDetectionResult {
|
|
26
|
+
// Tier 1: Agent metadata inspection (highest confidence)
|
|
27
|
+
if (adapterType && adapterType in ADAPTER_TYPE_MAP) {
|
|
28
|
+
return {
|
|
29
|
+
harnessName: ADAPTER_TYPE_MAP[adapterType],
|
|
30
|
+
detectionTier: "agent-metadata",
|
|
31
|
+
confidence: "high",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Tier 2: Environment variable probing
|
|
36
|
+
const envHarness = detectFromEnvironment();
|
|
37
|
+
if (envHarness) {
|
|
38
|
+
return {
|
|
39
|
+
harnessName: envHarness,
|
|
40
|
+
detectionTier: "env-probe",
|
|
41
|
+
confidence: "medium",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Tier 3: Explicit plugin configuration
|
|
46
|
+
if (pluginConfig?.defaultHarness) {
|
|
47
|
+
return {
|
|
48
|
+
harnessName: pluginConfig.defaultHarness,
|
|
49
|
+
detectionTier: "config",
|
|
50
|
+
confidence: "medium",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback: default to claude-code
|
|
55
|
+
return {
|
|
56
|
+
harnessName: "claude-code",
|
|
57
|
+
detectionTier: "fallback",
|
|
58
|
+
confidence: "low",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Probe environment variables for known harness signatures.
|
|
64
|
+
*/
|
|
65
|
+
function detectFromEnvironment(): string | undefined {
|
|
66
|
+
const env = process.env;
|
|
67
|
+
|
|
68
|
+
// Claude Code sets CLAUDE_CODE_* env vars
|
|
69
|
+
if (env.CLAUDE_CODE_SESSION || env.CLAUDE_CODE_ENTRYPOINT) {
|
|
70
|
+
return "claude-code";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Codex uses CODEX_* vars
|
|
74
|
+
if (env.CODEX_SESSION || env.CODEX_HOME) {
|
|
75
|
+
return "codex";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Gemini CLI
|
|
79
|
+
if (env.GEMINI_CLI_SESSION || env.GOOGLE_GENAI_API_KEY) {
|
|
80
|
+
return "gemini-cli";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Cursor
|
|
84
|
+
if (env.CURSOR_SESSION) {
|
|
85
|
+
return "cursor";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Map a Paperclip adapter type string to a babysitter harness name.
|
|
93
|
+
* Returns undefined if no mapping exists.
|
|
94
|
+
*/
|
|
95
|
+
export function mapAdapterType(adapterType: string): string | undefined {
|
|
96
|
+
return ADAPTER_TYPE_MAP[adapterType];
|
|
97
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness plugin installer for the Paperclip babysitter integration.
|
|
3
|
+
*
|
|
4
|
+
* When the Paperclip plugin detects an underlying harness (e.g., claude_local),
|
|
5
|
+
* it needs the corresponding babysitter harness plugin installed for that
|
|
6
|
+
* harness to handle the stop-hook iteration loop and breakpoint presentation.
|
|
7
|
+
*
|
|
8
|
+
* This module checks whether the babysitter plugin is installed for a given
|
|
9
|
+
* harness and provides installation commands.
|
|
10
|
+
*
|
|
11
|
+
* The underlying harness plugin is what actually drives the orchestration loop:
|
|
12
|
+
* - Claude Code: stop-hook pauses between iterations, allows exit when only
|
|
13
|
+
* breakpoints are pending (user must approve externally)
|
|
14
|
+
* - OpenClaw: agent_end hook fires async iteration, before_prompt_build
|
|
15
|
+
* injects context
|
|
16
|
+
*
|
|
17
|
+
* The Paperclip plugin SUPPLEMENTS this by:
|
|
18
|
+
* - Monitoring run state for pending breakpoints via run:status / task:list
|
|
19
|
+
* - Surfacing breakpoints in the Paperclip dashboard UI
|
|
20
|
+
* - Allowing approve/reject through Paperclip action handlers
|
|
21
|
+
* - Posting results via task:post, which the underlying harness picks up
|
|
22
|
+
* on next iteration
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { execFile } from "node:child_process";
|
|
26
|
+
import { promisify } from "node:util";
|
|
27
|
+
|
|
28
|
+
const exec = promisify(execFile);
|
|
29
|
+
|
|
30
|
+
/** Maps babysitter harness names to their plugin install commands. */
|
|
31
|
+
const HARNESS_INSTALL_COMMANDS: Record<string, { check: string[]; install: string[] }> = {
|
|
32
|
+
"claude-code": {
|
|
33
|
+
check: ["babysitter", "harness:discover", "--json"],
|
|
34
|
+
install: ["babysitter", "harness:install-plugin", "claude-code"],
|
|
35
|
+
},
|
|
36
|
+
codex: {
|
|
37
|
+
check: ["babysitter", "harness:discover", "--json"],
|
|
38
|
+
install: ["babysitter", "harness:install-plugin", "codex"],
|
|
39
|
+
},
|
|
40
|
+
openclaw: {
|
|
41
|
+
check: ["babysitter", "harness:discover", "--json"],
|
|
42
|
+
install: ["babysitter", "harness:install-plugin", "openclaw"],
|
|
43
|
+
},
|
|
44
|
+
"gemini-cli": {
|
|
45
|
+
check: ["babysitter", "harness:discover", "--json"],
|
|
46
|
+
install: ["babysitter", "harness:install-plugin", "gemini-cli"],
|
|
47
|
+
},
|
|
48
|
+
cursor: {
|
|
49
|
+
check: ["babysitter", "harness:discover", "--json"],
|
|
50
|
+
install: ["babysitter", "harness:install-plugin", "cursor"],
|
|
51
|
+
},
|
|
52
|
+
"github-copilot": {
|
|
53
|
+
check: ["babysitter", "harness:discover", "--json"],
|
|
54
|
+
install: ["babysitter", "harness:install-plugin", "github-copilot"],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Marketplace name for the a5c.ai babysitter plugins. */
|
|
59
|
+
const MARKETPLACE_NAME = "a5c.ai";
|
|
60
|
+
const MARKETPLACE_URL = "https://github.com/a5c-ai/babysitter.git";
|
|
61
|
+
|
|
62
|
+
export interface HarnessPluginStatus {
|
|
63
|
+
harnessName: string;
|
|
64
|
+
cliAvailable: boolean;
|
|
65
|
+
pluginInstalled: boolean;
|
|
66
|
+
installCommand?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if the babysitter CLI is available.
|
|
71
|
+
*/
|
|
72
|
+
export async function isBabysitterCliAvailable(): Promise<boolean> {
|
|
73
|
+
try {
|
|
74
|
+
await exec("babysitter", ["--version"], { timeout: 10_000 });
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if a harness CLI is available and the babysitter plugin is installed.
|
|
83
|
+
*/
|
|
84
|
+
export async function checkHarnessPluginStatus(
|
|
85
|
+
harnessName: string
|
|
86
|
+
): Promise<HarnessPluginStatus> {
|
|
87
|
+
const result: HarnessPluginStatus = {
|
|
88
|
+
harnessName,
|
|
89
|
+
cliAvailable: false,
|
|
90
|
+
pluginInstalled: false,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Check if babysitter CLI is available
|
|
94
|
+
if (!(await isBabysitterCliAvailable())) {
|
|
95
|
+
result.installCommand = "npm install -g @a5c-ai/babysitter-sdk";
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
result.cliAvailable = true;
|
|
100
|
+
|
|
101
|
+
// Check harness discovery to see if the harness CLI is available
|
|
102
|
+
try {
|
|
103
|
+
const { stdout } = await exec("babysitter", ["harness:discover", "--json"], {
|
|
104
|
+
timeout: 15_000,
|
|
105
|
+
});
|
|
106
|
+
const discovery = JSON.parse(stdout) as Array<{
|
|
107
|
+
name: string;
|
|
108
|
+
available: boolean;
|
|
109
|
+
pluginInstalled?: boolean;
|
|
110
|
+
}>;
|
|
111
|
+
|
|
112
|
+
const harness = discovery.find(
|
|
113
|
+
(h) => h.name === harnessName || h.name === harnessName.replace("-", "_")
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (harness) {
|
|
117
|
+
result.cliAvailable = harness.available;
|
|
118
|
+
result.pluginInstalled = harness.pluginInstalled ?? false;
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// Discovery failed - assume not installed
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!result.pluginInstalled) {
|
|
125
|
+
const cmd = HARNESS_INSTALL_COMMANDS[harnessName];
|
|
126
|
+
result.installCommand = cmd
|
|
127
|
+
? cmd.install.join(" ")
|
|
128
|
+
: `babysitter harness:install-plugin ${harnessName}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Attempt to install the babysitter plugin for a given harness.
|
|
136
|
+
* Returns success/failure and any output.
|
|
137
|
+
*/
|
|
138
|
+
export async function installHarnessPlugin(
|
|
139
|
+
harnessName: string
|
|
140
|
+
): Promise<{ success: boolean; output: string }> {
|
|
141
|
+
const cmd = HARNESS_INSTALL_COMMANDS[harnessName];
|
|
142
|
+
if (!cmd) {
|
|
143
|
+
return {
|
|
144
|
+
success: false,
|
|
145
|
+
output: `No install command known for harness: ${harnessName}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
// First ensure marketplace is added
|
|
151
|
+
try {
|
|
152
|
+
await exec("babysitter", [
|
|
153
|
+
"plugin:add-marketplace",
|
|
154
|
+
"--marketplace-url", MARKETPLACE_URL,
|
|
155
|
+
"--global",
|
|
156
|
+
], { timeout: 30_000 });
|
|
157
|
+
} catch {
|
|
158
|
+
// Marketplace may already exist - continue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Install the plugin
|
|
162
|
+
const [binary, ...args] = cmd.install;
|
|
163
|
+
const { stdout, stderr } = await exec(binary, args, { timeout: 60_000 });
|
|
164
|
+
return { success: true, output: stdout || stderr || "Installed successfully" };
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
output: err instanceof Error ? err.message : String(err),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Ensure the babysitter SDK CLI is installed.
|
|
175
|
+
* Attempts npm global install, falls back to providing npx instructions.
|
|
176
|
+
*/
|
|
177
|
+
export async function ensureBabysitterCli(): Promise<{
|
|
178
|
+
available: boolean;
|
|
179
|
+
method: "global" | "npx" | "missing";
|
|
180
|
+
}> {
|
|
181
|
+
if (await isBabysitterCliAvailable()) {
|
|
182
|
+
return { available: true, method: "global" };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Try installing globally
|
|
186
|
+
try {
|
|
187
|
+
await exec("npm", ["install", "-g", "@a5c-ai/babysitter-sdk"], {
|
|
188
|
+
timeout: 60_000,
|
|
189
|
+
});
|
|
190
|
+
return { available: true, method: "global" };
|
|
191
|
+
} catch {
|
|
192
|
+
// Check npx fallback
|
|
193
|
+
try {
|
|
194
|
+
await exec("npx", ["-y", "@a5c-ai/babysitter-sdk", "--version"], {
|
|
195
|
+
timeout: 30_000,
|
|
196
|
+
});
|
|
197
|
+
return { available: true, method: "npx" };
|
|
198
|
+
} catch {
|
|
199
|
+
return { available: false, method: "missing" };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Babysitter Paperclip plugin manifest.
|
|
3
|
+
*
|
|
4
|
+
* Declares capabilities, event subscriptions, UI slots, and settings
|
|
5
|
+
* for the Babysitter orchestration integration with Paperclip.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const manifest = {
|
|
9
|
+
id: "babysitter",
|
|
10
|
+
displayName: "Babysitter Orchestrator",
|
|
11
|
+
description:
|
|
12
|
+
"Deterministic, event-sourced orchestration for Paperclip agents via Babysitter.",
|
|
13
|
+
version: "0.0.1",
|
|
14
|
+
|
|
15
|
+
entrypoints: {
|
|
16
|
+
worker: "dist/worker.js",
|
|
17
|
+
ui: "dist/ui",
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
capabilities: [
|
|
21
|
+
"events.subscribe",
|
|
22
|
+
"events.emit",
|
|
23
|
+
"plugin.state.read",
|
|
24
|
+
"plugin.state.write",
|
|
25
|
+
"agents.read",
|
|
26
|
+
"agent.tools.register",
|
|
27
|
+
"ui.dashboardWidget.register",
|
|
28
|
+
"ui.detailTab.register",
|
|
29
|
+
"ui.action.register",
|
|
30
|
+
"ui.sidebar.register",
|
|
31
|
+
],
|
|
32
|
+
|
|
33
|
+
events: {
|
|
34
|
+
subscribe: [
|
|
35
|
+
"agent.run.started",
|
|
36
|
+
"agent.run.finished",
|
|
37
|
+
"agent.run.failed",
|
|
38
|
+
"agent.run.cancelled",
|
|
39
|
+
],
|
|
40
|
+
emit: [
|
|
41
|
+
"plugin.babysitter.run.created",
|
|
42
|
+
"plugin.babysitter.breakpoint.requested",
|
|
43
|
+
"plugin.babysitter.breakpoint.resolved",
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
settings: {
|
|
48
|
+
runsDir: {
|
|
49
|
+
type: "string" as const,
|
|
50
|
+
default: ".a5c/runs",
|
|
51
|
+
displayName: "Runs Directory",
|
|
52
|
+
description: "Directory where babysitter run data is stored.",
|
|
53
|
+
},
|
|
54
|
+
autoIterate: {
|
|
55
|
+
type: "boolean" as const,
|
|
56
|
+
default: true,
|
|
57
|
+
displayName: "Auto-Iterate",
|
|
58
|
+
description:
|
|
59
|
+
"Automatically iterate runs when effects are resolved.",
|
|
60
|
+
},
|
|
61
|
+
maxIterations: {
|
|
62
|
+
type: "number" as const,
|
|
63
|
+
default: 256,
|
|
64
|
+
displayName: "Max Iterations",
|
|
65
|
+
description: "Maximum orchestration iterations per run.",
|
|
66
|
+
},
|
|
67
|
+
breakpointTimeout: {
|
|
68
|
+
type: "number" as const,
|
|
69
|
+
default: 3600000,
|
|
70
|
+
displayName: "Breakpoint Timeout (ms)",
|
|
71
|
+
description:
|
|
72
|
+
"Time to wait for breakpoint approval before timing out (default 1 hour).",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
ui: {
|
|
77
|
+
slots: [
|
|
78
|
+
{
|
|
79
|
+
type: "dashboardWidget",
|
|
80
|
+
id: "babysitter-dashboard",
|
|
81
|
+
displayName: "Babysitter Runs",
|
|
82
|
+
exportName: "BabysitterDashboard",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: "detailTab",
|
|
86
|
+
id: "babysitter-run-detail",
|
|
87
|
+
displayName: "Babysitter Run",
|
|
88
|
+
exportName: "RunDetailTab",
|
|
89
|
+
entityTypes: ["agent"],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: "sidebarPanel",
|
|
93
|
+
id: "babysitter-sidebar",
|
|
94
|
+
displayName: "Babysitter",
|
|
95
|
+
exportName: "BabysitterSidebar",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
export default manifest;
|