@garuda-agent/garuda 0.0.1 → 0.14.1
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/CHANGELOG.md +58 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +1040 -0
- package/SECURITY.md +32 -0
- package/dist/anthropic-NQDBCHJU.js +327 -0
- package/dist/chunk-35GMLLJW.js +157 -0
- package/dist/chunk-36GQIVGT.js +526 -0
- package/dist/chunk-3CEPHS4C.js +231 -0
- package/dist/chunk-3NJNVZTK.js +123 -0
- package/dist/chunk-42DSA4EA.js +116 -0
- package/dist/chunk-4P26OKTP.js +100 -0
- package/dist/chunk-5DEWP3YX.js +139 -0
- package/dist/chunk-6TWBYMY7.js +9 -0
- package/dist/chunk-A2QAIRQL.js +45 -0
- package/dist/chunk-ATW7BNIG.js +31 -0
- package/dist/chunk-D5O5CIWK.js +1339 -0
- package/dist/chunk-EU7PA45O.js +324 -0
- package/dist/chunk-FARS4R67.js +206 -0
- package/dist/chunk-G2AX7MGS.js +36 -0
- package/dist/chunk-HUIK3WXO.js +234 -0
- package/dist/chunk-ITITLNAR.js +9341 -0
- package/dist/chunk-KKB5W47S.js +231 -0
- package/dist/chunk-KWRJREUH.js +283 -0
- package/dist/chunk-N4I6YS3E.js +73 -0
- package/dist/chunk-OUCV3FCT.js +205 -0
- package/dist/chunk-PZS3V6P3.js +46 -0
- package/dist/chunk-QOW5TK4L.js +320 -0
- package/dist/chunk-U4OQVT2T.js +73 -0
- package/dist/chunk-V7WPGQLB.js +207 -0
- package/dist/chunk-W7BPYQMW.js +173 -0
- package/dist/chunk-WMOLXTBU.js +61 -0
- package/dist/chunk-YIZWYUBG.js +138 -0
- package/dist/chunk-YM6T4J5V.js +1350 -0
- package/dist/chunk-YQTGMCU6.js +111 -0
- package/dist/chunk-ZAE25JM2.js +158 -0
- package/dist/chunk-ZGZ2ZADL.js +49 -0
- package/dist/cli/index.js +1191 -0
- package/dist/create-Y5SXJUXO.js +223 -0
- package/dist/deadline-F5RJJ2QW.js +86 -0
- package/dist/detect-LARDSLLZ.js +10 -0
- package/dist/evalCommand-5TG2CKZY.js +2795 -0
- package/dist/export-HN3CT7AV.js +158 -0
- package/dist/inkChat-3J26R24L.js +1190 -0
- package/dist/install-XWLU22OH.js +17 -0
- package/dist/job-3NYDMP2M.js +31 -0
- package/dist/jobCommand-IYWZWTSI.js +30 -0
- package/dist/launchd-2FL5UN2U.js +40 -0
- package/dist/lspCommand-TJEZBV2E.js +99 -0
- package/dist/manager-4PMSXT47.js +14 -0
- package/dist/manager-EBTYO2GR.js +847 -0
- package/dist/nightCommand-DUNWJASI.js +247 -0
- package/dist/oauth-W6OK6XXO.js +23 -0
- package/dist/openaiCompatible-CJWVJP5M.js +501 -0
- package/dist/policy-EJPLEEXP.js +31 -0
- package/dist/proxy-XJAHCZB2.js +192 -0
- package/dist/remove-AENULZZE.js +59 -0
- package/dist/run-66Y6R2KU.js +886 -0
- package/dist/servers-6YDGWOZ6.js +24 -0
- package/dist/text-J2GD647J.js +11 -0
- package/package.json +83 -3
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
jobBase,
|
|
4
|
+
linkCandidates,
|
|
5
|
+
worktreeDir
|
|
6
|
+
} from "./chunk-YIZWYUBG.js";
|
|
7
|
+
import {
|
|
8
|
+
agentLabel,
|
|
9
|
+
defaultAgentEnv,
|
|
10
|
+
installAgent,
|
|
11
|
+
installSpec,
|
|
12
|
+
nextTime,
|
|
13
|
+
nightAgentTime,
|
|
14
|
+
nightSpec
|
|
15
|
+
} from "./chunk-35GMLLJW.js";
|
|
16
|
+
import {
|
|
17
|
+
jobPrompt
|
|
18
|
+
} from "./chunk-YQTGMCU6.js";
|
|
19
|
+
import "./chunk-OUCV3FCT.js";
|
|
20
|
+
import {
|
|
21
|
+
JobGitError
|
|
22
|
+
} from "./chunk-ZGZ2ZADL.js";
|
|
23
|
+
import "./chunk-D5O5CIWK.js";
|
|
24
|
+
import "./chunk-WMOLXTBU.js";
|
|
25
|
+
import "./chunk-HUIK3WXO.js";
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_JOB_MAX_STEPS,
|
|
28
|
+
JOBS_DIR,
|
|
29
|
+
jobPath,
|
|
30
|
+
newJobId,
|
|
31
|
+
planPermissions,
|
|
32
|
+
planTitle,
|
|
33
|
+
saveJob
|
|
34
|
+
} from "./chunk-KKB5W47S.js";
|
|
35
|
+
import "./chunk-3NJNVZTK.js";
|
|
36
|
+
|
|
37
|
+
// src/jobs/create.ts
|
|
38
|
+
var DEFAULT_FINISH_BY = "07:00";
|
|
39
|
+
var SWITCH_BEFORE_MS = 15 * 6e4;
|
|
40
|
+
var DEFAULT_STEP_LIMIT_MINUTES = 20;
|
|
41
|
+
function switchTime(finishBy) {
|
|
42
|
+
const [h, m] = finishBy.split(":").map(Number);
|
|
43
|
+
const minutes = (h * 60 + m - SWITCH_BEFORE_MS / 6e4 + 24 * 60) % (24 * 60);
|
|
44
|
+
return `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`;
|
|
45
|
+
}
|
|
46
|
+
async function createJob(options) {
|
|
47
|
+
if (options.executor.isolation === "none") {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
text: "A job needs the OS sandbox (Seatbelt or bubblewrap): without it, every command would need an approval, and nobody is there to give it."
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (options.at !== void 0 && !/^([01]\d|2[0-3]):[0-5]\d$/.test(options.at)) {
|
|
54
|
+
return { ok: false, text: `"${options.at}" is not a time. Use HH:MM, for example 01:00.` };
|
|
55
|
+
}
|
|
56
|
+
let base;
|
|
57
|
+
try {
|
|
58
|
+
base = await jobBase(options.executor, options.root);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof JobGitError) return { ok: false, text: error.message };
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const { rules, problems } = planPermissions(options.plan.plan);
|
|
64
|
+
const id = newJobId(options.now);
|
|
65
|
+
const links = linkCandidates(options.root);
|
|
66
|
+
const job = {
|
|
67
|
+
version: 1,
|
|
68
|
+
id,
|
|
69
|
+
title: planTitle(options.plan.request, options.plan.plan),
|
|
70
|
+
createdAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
71
|
+
root: options.root,
|
|
72
|
+
base: base.commit,
|
|
73
|
+
branch: `garuda/job-${id}`,
|
|
74
|
+
worktree: worktreeDir(options.root, id, options.home),
|
|
75
|
+
prompt: jobPrompt(options.plan.request, options.plan.plan),
|
|
76
|
+
planSession: options.plan.sessionId,
|
|
77
|
+
model: options.modelId,
|
|
78
|
+
allow: rules,
|
|
79
|
+
onUnapproved: "deny-and-continue",
|
|
80
|
+
...options.at === void 0 ? {} : { at: options.at },
|
|
81
|
+
maxSteps: options.maxSteps ?? DEFAULT_JOB_MAX_STEPS,
|
|
82
|
+
links,
|
|
83
|
+
...options.test === void 0 ? {} : { test: options.test },
|
|
84
|
+
...options.network === void 0 || options.network.length === 0 ? {} : { network: [...options.network] },
|
|
85
|
+
queue: true,
|
|
86
|
+
status: "scheduled"
|
|
87
|
+
};
|
|
88
|
+
const preview = [
|
|
89
|
+
`Job: ${job.title}`,
|
|
90
|
+
`Model: ${options.modelId} \xB7 up to ${job.maxSteps} steps`,
|
|
91
|
+
`Starts from: ${base.uncommitted ? "your last commit plus your uncommitted changes" : "your last commit"}${base.untracked > 0 ? ` (${base.untracked} untracked file(s) are not included)` : ""}`,
|
|
92
|
+
`Works on: branch ${job.branch}, in its own worktree; your checkout does not change.`,
|
|
93
|
+
...links.length === 0 ? [] : [`Links: ${links.join(", ")} from your checkout.`],
|
|
94
|
+
"",
|
|
95
|
+
"Approved for this job, on top of the sandbox (reads, and commands that stay in the project):",
|
|
96
|
+
...rules.length === 0 ? [" (nothing: the plan listed no permissions)"] : rules.map((r) => ` ${r}`),
|
|
97
|
+
...problems.length === 0 ? [] : ["", "Not rules, left out:", ...problems.map((p) => ` ${p}`)],
|
|
98
|
+
"",
|
|
99
|
+
"Any other call that would ask is denied at run time; the job goes on and reports it.",
|
|
100
|
+
job.network === void 0 ? "Network: none for commands." : `Network: ${job.network.join(", ")}, through Garuda's proxy; other hosts are denied and reported.`,
|
|
101
|
+
"",
|
|
102
|
+
options.test === void 0 ? 'Proof of work: no test command found (set "test" in the job file); a principal-engineer review of the diff (one model call).' : `Proof of work: \`${options.test}\` runs in the sandbox before and after the job; a principal-engineer review of the diff (one model call).`,
|
|
103
|
+
"Night shift: the job joins this project's queue; `garuda night` runs the queue (up to 3 jobs at a time) and writes one digest."
|
|
104
|
+
].join("\n");
|
|
105
|
+
const choice = await options.approver.ask(
|
|
106
|
+
{
|
|
107
|
+
tool: "schedule",
|
|
108
|
+
target: { kind: "input", json: "{}" },
|
|
109
|
+
preview,
|
|
110
|
+
isolation: options.executor.isolation,
|
|
111
|
+
title: "Schedule this plan as a job?",
|
|
112
|
+
question: "Create the job?",
|
|
113
|
+
choices: ["once", "deny"],
|
|
114
|
+
labels: { once: "Yes, create the job", deny: "No" }
|
|
115
|
+
},
|
|
116
|
+
options.signal
|
|
117
|
+
);
|
|
118
|
+
if (choice === "deny") return { ok: false, text: "No job was created." };
|
|
119
|
+
if (options.batchCapable === true) {
|
|
120
|
+
const batch = await options.approver.ask(
|
|
121
|
+
{
|
|
122
|
+
tool: "schedule",
|
|
123
|
+
target: { kind: "input", json: "{}" },
|
|
124
|
+
preview: [
|
|
125
|
+
"Half the token price. Each step waits for its batch: about 3 minutes in Garuda's first measurement, but some batches wait hours.",
|
|
126
|
+
`A step that waits more than ${DEFAULT_STEP_LIMIT_MINUTES} minutes runs on the normal API (full price); the next step tries the Batch API again.`,
|
|
127
|
+
`At ${switchTime(DEFAULT_FINISH_BY)} (15 minutes before the finish-by time ${DEFAULT_FINISH_BY}), a job that still runs goes on with the normal API. You can change "finishBy" and "stepLimitMinutes" in the job file.`
|
|
128
|
+
].join("\n"),
|
|
129
|
+
isolation: options.executor.isolation,
|
|
130
|
+
title: "Use the Batch API for this job?",
|
|
131
|
+
question: "Use the Batch API?",
|
|
132
|
+
choices: ["once", "deny"],
|
|
133
|
+
labels: { once: "Yes, half price, slower", deny: "No, the normal API" }
|
|
134
|
+
},
|
|
135
|
+
options.signal
|
|
136
|
+
);
|
|
137
|
+
if (batch !== "deny") {
|
|
138
|
+
job.batch = true;
|
|
139
|
+
job.finishBy = DEFAULT_FINISH_BY;
|
|
140
|
+
job.stepLimitMinutes = DEFAULT_STEP_LIMIT_MINUTES;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
await saveJob(job);
|
|
144
|
+
const lines = [
|
|
145
|
+
`Created job ${id}: ${jobPath(options.root, id)}`,
|
|
146
|
+
'You can edit its approval list ("allow") in that file before the run.'
|
|
147
|
+
];
|
|
148
|
+
const platform = options.launchd?.platform ?? process.platform;
|
|
149
|
+
if (options.launchd !== void 0 && options.at !== void 0 && platform === "darwin") {
|
|
150
|
+
const agent = await offerAgent(job, options);
|
|
151
|
+
if (agent !== void 0) return { ok: true, job, text: [...lines, agent].join("\n") };
|
|
152
|
+
}
|
|
153
|
+
const at = options.at === void 0 ? "" : ` --at ${options.at}`;
|
|
154
|
+
lines.push(
|
|
155
|
+
`Run the night queue in a terminal in this folder: garuda night${at} (or this job alone: garuda run ${id}${at})`
|
|
156
|
+
);
|
|
157
|
+
if (options.at !== void 0) {
|
|
158
|
+
lines.push("The terminal waits until then; keep the Mac awake and on power.");
|
|
159
|
+
}
|
|
160
|
+
return { ok: true, job, text: lines.join("\n") };
|
|
161
|
+
}
|
|
162
|
+
async function offerAgent(job, options) {
|
|
163
|
+
const env = options.launchd?.env ?? defaultAgentEnv();
|
|
164
|
+
const at = job.at;
|
|
165
|
+
const when = nextTime(at, options.now);
|
|
166
|
+
const day = when.toDateString();
|
|
167
|
+
const earlier = nightAgentTime(job.root, env);
|
|
168
|
+
const choice = await options.approver.ask(
|
|
169
|
+
{
|
|
170
|
+
tool: "schedule",
|
|
171
|
+
target: { kind: "input", json: "{}" },
|
|
172
|
+
preview: [
|
|
173
|
+
`launchd starts the night queue (every queued job, up to 3 at a time, then one digest) at ${at} on ${day}, also when no terminal is open. Or only this job.`,
|
|
174
|
+
...earlier === void 0 ? [] : [
|
|
175
|
+
`The queue's agent now starts at ${earlier.toTimeString().slice(0, 5)} on ${earlier.toDateString()}: it moves to ${at}.`
|
|
176
|
+
],
|
|
177
|
+
`It runs through your login shell (${env.shell} -lic), so your shell setup gives it the API keys; no key is written to a file.`,
|
|
178
|
+
"caffeinate keeps the Mac from idle sleep while it runs. If the Mac sleeps at that time, it starts at the next wake.",
|
|
179
|
+
`Logs: ${JOBS_DIR}/night.log (the queue) or ${JOBS_DIR}/${job.id}.log (one job). /jobs cancel night (or ${job.id}) removes it; it removes itself after it runs.`
|
|
180
|
+
].join("\n"),
|
|
181
|
+
isolation: options.executor.isolation,
|
|
182
|
+
title: `Start at ${at} with launchd?`,
|
|
183
|
+
question: "Add a launchd agent?",
|
|
184
|
+
choices: ["once", "session", "deny"],
|
|
185
|
+
labels: {
|
|
186
|
+
once: "Yes, the whole night queue",
|
|
187
|
+
session: "Only this job",
|
|
188
|
+
deny: "No, I will run it myself"
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
options.signal
|
|
192
|
+
);
|
|
193
|
+
if (choice === "deny") return void 0;
|
|
194
|
+
const executor = options.launchd?.executor ?? options.executor;
|
|
195
|
+
if (choice === "once") {
|
|
196
|
+
try {
|
|
197
|
+
await installSpec(executor, nightSpec(job.root), env, when);
|
|
198
|
+
return `launchd starts the night queue at ${at} on ${day}. Keep the Mac on power. Log: ${JOBS_DIR}/night.log. Cancel: /jobs cancel night.`;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return `The launchd agent failed (${error.message}). Run the queue in a terminal instead: garuda night --at ${at}`;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const installed = await installAgent(executor, job, env, options.now);
|
|
205
|
+
job.launchd = {
|
|
206
|
+
label: agentLabel(job.id),
|
|
207
|
+
plist: installed.plist,
|
|
208
|
+
when: installed.when.toISOString()
|
|
209
|
+
};
|
|
210
|
+
await saveJob(job);
|
|
211
|
+
return `launchd starts it at ${at} on ${day}; it leaves the night queue. Keep the Mac on power. Log: ${JOBS_DIR}/${job.id}.log. Cancel: /jobs cancel ${job.id}.`;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return `The launchd agent failed (${error.message}). Run it in a terminal instead: garuda run ${job.id} --at ${at}`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export {
|
|
217
|
+
DEFAULT_FINISH_BY,
|
|
218
|
+
DEFAULT_STEP_LIMIT_MINUTES,
|
|
219
|
+
SWITCH_BEFORE_MS,
|
|
220
|
+
createJob,
|
|
221
|
+
switchTime
|
|
222
|
+
};
|
|
223
|
+
//# sourceMappingURL=create-Y5SXJUXO.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/model/deadline.ts
|
|
4
|
+
var DeadlineClient = class {
|
|
5
|
+
constructor(primary, fallback, switchAt, options = {}) {
|
|
6
|
+
this.primary = primary;
|
|
7
|
+
this.fallback = fallback;
|
|
8
|
+
this.switchAt = switchAt;
|
|
9
|
+
this.stepLimitMs = options.stepLimitMs ?? Number.POSITIVE_INFINITY;
|
|
10
|
+
this.now = options.now ?? Date.now;
|
|
11
|
+
if (primary.serverTools !== void 0) this.serverTools = primary.serverTools;
|
|
12
|
+
}
|
|
13
|
+
primary;
|
|
14
|
+
fallback;
|
|
15
|
+
switchAt;
|
|
16
|
+
serverTools;
|
|
17
|
+
/** Requests answered by each client, and when the switch happened. */
|
|
18
|
+
calls = {
|
|
19
|
+
primary: 0,
|
|
20
|
+
fallback: 0,
|
|
21
|
+
/** Requests that waited past the step limit and went to the fallback. */
|
|
22
|
+
slow: 0,
|
|
23
|
+
switchedAt: void 0
|
|
24
|
+
};
|
|
25
|
+
fallbackClient;
|
|
26
|
+
stepLimitMs;
|
|
27
|
+
now;
|
|
28
|
+
async *stream(request, options) {
|
|
29
|
+
const left = this.switchAt.getTime() - this.now();
|
|
30
|
+
if (this.calls.switchedAt !== void 0 || left <= 0) {
|
|
31
|
+
yield* this.viaFallback(request, options);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const timer = new AbortController();
|
|
35
|
+
const stepOnly = this.stepLimitMs < left;
|
|
36
|
+
const handle = setTimeout(
|
|
37
|
+
() => timer.abort(stepOnly ? new StepTooSlow() : new DeadlineReached()),
|
|
38
|
+
Math.min(left, this.stepLimitMs)
|
|
39
|
+
);
|
|
40
|
+
const signals = [timer.signal, ...options?.signal === void 0 ? [] : [options.signal]];
|
|
41
|
+
try {
|
|
42
|
+
const events = [];
|
|
43
|
+
for await (const event of this.primary.stream(request, {
|
|
44
|
+
signal: AbortSignal.any(signals)
|
|
45
|
+
})) {
|
|
46
|
+
events.push(event);
|
|
47
|
+
}
|
|
48
|
+
this.calls.primary++;
|
|
49
|
+
yield* events;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (options?.signal?.aborted === true || !timer.signal.aborted) throw error;
|
|
52
|
+
if (stepOnly) {
|
|
53
|
+
this.calls.slow++;
|
|
54
|
+
this.fallbackClient ??= await this.fallback();
|
|
55
|
+
this.calls.fallback++;
|
|
56
|
+
yield* this.fallbackClient.stream(request, options);
|
|
57
|
+
} else yield* this.viaFallback(request, options);
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(handle);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async *viaFallback(request, options) {
|
|
63
|
+
if (this.calls.switchedAt === void 0) this.calls.switchedAt = new Date(this.now());
|
|
64
|
+
this.fallbackClient ??= await this.fallback();
|
|
65
|
+
this.calls.fallback++;
|
|
66
|
+
yield* this.fallbackClient.stream(request, options);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var DeadlineReached = class extends Error {
|
|
70
|
+
constructor() {
|
|
71
|
+
super("The finish-by time is near: the request goes to the normal API.");
|
|
72
|
+
this.name = "DeadlineReached";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var StepTooSlow = class extends Error {
|
|
76
|
+
constructor() {
|
|
77
|
+
super("The batch took longer than the step limit: this request goes to the normal API.");
|
|
78
|
+
this.name = "StepTooSlow";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
export {
|
|
82
|
+
DeadlineClient,
|
|
83
|
+
DeadlineReached,
|
|
84
|
+
StepTooSlow
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=deadline-F5RJJ2QW.js.map
|