@garuda-agent/garuda 0.16.1 → 0.18.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/CHANGELOG.md +319 -0
- package/README.md +56 -1074
- package/SECURITY.md +9 -2
- package/dist/{acpCommand-OTAYW7BS.js → acpCommand-IUZPLPLI.js} +25 -23
- package/dist/{anthropic-NQDBCHJU.js → anthropic-NQIZLD64.js} +12 -6
- package/dist/{chunk-NP6RIC25.js → chunk-2FIV2VZP.js} +11 -7
- package/dist/{chunk-YQTGMCU6.js → chunk-3MRQJKUO.js} +29 -5
- package/dist/{chunk-HE2K657U.js → chunk-4BVCYCQT.js} +47 -16
- package/dist/{chunk-WMOLXTBU.js → chunk-4LFJFDCF.js} +2 -1
- package/dist/{chunk-44QN5SWZ.js → chunk-55CFEEIN.js} +22 -2
- package/dist/{chunk-USCUXCAU.js → chunk-5B5HDTUR.js} +3 -3
- package/dist/chunk-65NUEOI7.js +143 -0
- package/dist/{chunk-TUUEBHG2.js → chunk-6XHON3Q6.js} +2 -2
- package/dist/chunk-7KBOIPUT.js +157 -0
- package/dist/{chunk-B6FA4I7Q.js → chunk-7TH3NHCQ.js} +2 -2
- package/dist/{chunk-OUCV3FCT.js → chunk-AOGFYUKG.js} +3 -3
- package/dist/{chunk-GXZACJUI.js → chunk-B4SJY7KT.js} +322 -39
- package/dist/{chunk-3EQU2GUX.js → chunk-BHZ6G65V.js} +5 -1
- package/dist/chunk-C57GHLKP.js +9 -0
- package/dist/chunk-DOCDCQVM.js +484 -0
- package/dist/chunk-FOPBOB7W.js +332 -0
- package/dist/{chunk-TILFPYWO.js → chunk-NHWHG3SW.js} +1930 -992
- package/dist/{chunk-BQTUOO4G.js → chunk-OQ26W5L4.js} +81 -32
- package/dist/{chunk-HUIK3WXO.js → chunk-P6XWWIEK.js} +20 -3
- package/dist/chunk-Q4P3W657.js +6372 -0
- package/dist/{chunk-ZAE25JM2.js → chunk-QANRZW7W.js} +8 -1
- package/dist/chunk-UIRJOQQS.js +118 -0
- package/dist/chunk-UKIE4AAM.js +472 -0
- package/dist/{chunk-KKB5W47S.js → chunk-VTMD5IHD.js} +26 -2
- package/dist/cli/index.js +148 -57
- package/dist/config-WV2H64TF.js +23 -0
- package/dist/{create-VXIH2GUZ.js → create-W6BV75QX.js} +18 -12
- package/dist/{deadline-F5RJJ2QW.js → deadline-TP7ZGLT2.js} +4 -2
- package/dist/evalCommand-SKJQ6CW2.js +686 -0
- package/dist/evalTune-CZRCPSZ3.js +295 -0
- package/dist/{export-HN3CT7AV.js → export-SJ3DUP6W.js} +2 -2
- package/dist/{inkChat-WEDGQMCY.js → inkChat-WYYAFZXD.js} +20 -19
- package/dist/install-TKWN5AYH.js +19 -0
- package/dist/{job-3NYDMP2M.js → job-7JOI4YIQ.js} +6 -2
- package/dist/jobCommand-2ODQLHB3.js +33 -0
- package/dist/{launchd-SSX362VG.js → launchd-3A3ONEW5.js} +9 -8
- package/dist/{lspCommand-JRXKRSL7.js → lspCommand-ZZ6R4PA2.js} +7 -6
- package/dist/{manager-773PUDFE.js → manager-TCYTA5XU.js} +15 -14
- package/dist/{nightCommand-65MUEV32.js → nightCommand-WJU6VZA6.js} +26 -20
- package/dist/{openaiCompatible-CJWVJP5M.js → openaiCompatible-6TLDDW5U.js} +29 -15
- package/dist/{policy-EJPLEEXP.js → policy-ZJ7RMOFN.js} +10 -4
- package/dist/{remove-6GDSTAIO.js → remove-42WQNVPX.js} +8 -7
- package/dist/{run-ML6GSPN4.js → run-PTMRQCIU.js} +5 -5
- package/dist/{server-NGKVIQEV.js → server-VEYIJHEU.js} +2 -2
- package/dist/{setupCommand-VA7D5MZK.js → setupCommand-YMF6I3MD.js} +45 -13
- package/dist/text-LS35WSRL.js +17 -0
- package/dist/tune-YVI2KWQF.js +31 -0
- package/native/bin/garuda-reap +0 -0
- package/native/garuda-reap.c +113 -0
- package/package.json +39 -13
- package/dist/chunk-EIXUSVFX.js +0 -116
- package/dist/chunk-R7I2NBPX.js +0 -71
- package/dist/chunk-ULAJPMY7.js +0 -107
- package/dist/chunk-VBZCNMUD.js +0 -9
- package/dist/evalCommand-RGMZRYDR.js +0 -2812
- package/dist/install-EWDSPFFZ.js +0 -18
- package/dist/jobCommand-ACSGSBQ4.js +0 -32
- package/dist/text-J2GD647J.js +0 -11
|
@@ -57,6 +57,12 @@ async function loadMcpConfig({ home, root } = { home: homedir(), root: process.c
|
|
|
57
57
|
problems.push(`${file}: invalid JSON: ${error.message}`);
|
|
58
58
|
return [];
|
|
59
59
|
}
|
|
60
|
+
if (Array.isArray(json?.servers)) {
|
|
61
|
+
problems.push(
|
|
62
|
+
`${file}: "servers" must be an object of name \u2192 server, not a list: { "servers": { "graphify": { "command": "\u2026" }, "linear": { "url": "\u2026" } } }`
|
|
63
|
+
);
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
60
66
|
const parsed = fileSchema.safeParse(json);
|
|
61
67
|
if (!parsed.success) {
|
|
62
68
|
problems.push(`${file}: ${z.prettifyError(parsed.error)}`);
|
|
@@ -147,6 +153,7 @@ function commandLine(def) {
|
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
export {
|
|
156
|
+
MCP_FILE,
|
|
150
157
|
SERVER_NAME,
|
|
151
158
|
isHttp,
|
|
152
159
|
loadMcpConfig,
|
|
@@ -155,4 +162,4 @@ export {
|
|
|
155
162
|
expandEnv,
|
|
156
163
|
commandLine
|
|
157
164
|
};
|
|
158
|
-
//# sourceMappingURL=chunk-
|
|
165
|
+
//# sourceMappingURL=chunk-QANRZW7W.js.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
realpathOfNearestExisting
|
|
4
|
+
} from "./chunk-4LFJFDCF.js";
|
|
5
|
+
|
|
6
|
+
// src/permissions/writeScope.ts
|
|
7
|
+
import { existsSync, lstatSync, mkdirSync, realpathSync } from "fs";
|
|
8
|
+
import { realpath } from "fs/promises";
|
|
9
|
+
import { isAbsolute, join, relative, resolve, sep } from "path";
|
|
10
|
+
function scopeEntryProblem(entry) {
|
|
11
|
+
if (entry === "" || entry === "/" || entry === "." || entry === "./") {
|
|
12
|
+
return `"${entry}" is the whole worktree, not a scope`;
|
|
13
|
+
}
|
|
14
|
+
if (entry.startsWith("/") || /^[A-Za-z]:/.test(entry)) return `"${entry}" is not relative`;
|
|
15
|
+
const control = [...entry].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
|
|
16
|
+
if (entry.includes("\\") || control) {
|
|
17
|
+
return `"${entry}" has a backslash or a control character`;
|
|
18
|
+
}
|
|
19
|
+
const parts = entry.replace(/\/$/, "").split("/");
|
|
20
|
+
if (parts.some((p) => p === "" || p === "." || p === "..")) {
|
|
21
|
+
return `"${entry}" has an empty, "." or ".." part`;
|
|
22
|
+
}
|
|
23
|
+
if (parts[0] === ".git" || parts[0] === ".garuda") return `"${entry}" is a protected folder`;
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
function inWriteScope(path, scope) {
|
|
27
|
+
const clean = path.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
28
|
+
return scope.some(
|
|
29
|
+
(entry) => entry.endsWith("/") ? clean === entry.slice(0, -1) || clean.startsWith(entry) : clean === entry
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
function scopePaths(root, scope) {
|
|
33
|
+
return scope.map((entry) => join(root, ...entry.replace(/\/$/, "").split("/")));
|
|
34
|
+
}
|
|
35
|
+
function prepareWriteScope(root, scope) {
|
|
36
|
+
for (const entry of scope) {
|
|
37
|
+
const problem = scopeEntryProblem(entry);
|
|
38
|
+
if (problem !== void 0) return problem;
|
|
39
|
+
}
|
|
40
|
+
const realRoot = realpathSync(root);
|
|
41
|
+
for (const [i, path] of scopePaths(root, scope).entries()) {
|
|
42
|
+
const entry = scope[i];
|
|
43
|
+
let current = root;
|
|
44
|
+
for (const part of entry.replace(/\/$/, "").split("/")) {
|
|
45
|
+
current = join(current, part);
|
|
46
|
+
if (existsSync(current) && lstatSync(current).isSymbolicLink()) {
|
|
47
|
+
return `"${entry}" goes through a symbolic link (${current})`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!existsSync(path) && entry.endsWith("/")) mkdirSync(path, { recursive: true });
|
|
51
|
+
if (existsSync(path)) {
|
|
52
|
+
const real = realpathSync(path);
|
|
53
|
+
if (real !== realRoot && !real.startsWith(`${realRoot}${sep}`)) {
|
|
54
|
+
return `"${entry}" is outside the worktree`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
var BUILD_OUTPUTS = [
|
|
61
|
+
{ marker: "pom.xml", folders: ["target/"] },
|
|
62
|
+
{ marker: "build.gradle", folders: ["build/", ".gradle/"] },
|
|
63
|
+
{ marker: "build.gradle.kts", folders: ["build/", ".gradle/"] },
|
|
64
|
+
{ marker: "package.json", folders: ["dist/", "coverage/"] },
|
|
65
|
+
{ marker: "pyproject.toml", folders: ["build/", "dist/", ".pytest_cache/"] },
|
|
66
|
+
{ marker: "setup.py", folders: ["build/", "dist/", ".pytest_cache/"] },
|
|
67
|
+
{ marker: "Cargo.toml", folders: ["target/"] },
|
|
68
|
+
{ marker: "go.mod", folders: [] }
|
|
69
|
+
];
|
|
70
|
+
function proposeWriteScope(rules, root) {
|
|
71
|
+
const entries = /* @__PURE__ */ new Set();
|
|
72
|
+
for (const rule of rules) {
|
|
73
|
+
const m = /^(edit_file|write_file)\((.*)\)$/.exec(rule.trim());
|
|
74
|
+
if (m === null) continue;
|
|
75
|
+
const pattern = m[2].replace(/^\.\//, "");
|
|
76
|
+
const glob = pattern.search(/[*?[{]/);
|
|
77
|
+
const entry = glob === -1 ? pattern : pattern.slice(0, pattern.lastIndexOf("/", glob) + 1);
|
|
78
|
+
if (entry === "" || scopeEntryProblem(entry) !== void 0) {
|
|
79
|
+
return { scope: void 0, reason: `the rule ${rule} names no folder` };
|
|
80
|
+
}
|
|
81
|
+
entries.add(entry);
|
|
82
|
+
}
|
|
83
|
+
for (const { marker, folders } of BUILD_OUTPUTS) {
|
|
84
|
+
if (existsSync(join(root, marker))) for (const f of folders) entries.add(f);
|
|
85
|
+
}
|
|
86
|
+
return { scope: [...entries].sort() };
|
|
87
|
+
}
|
|
88
|
+
async function scopeDenial(root, path, scope) {
|
|
89
|
+
const absolute = isAbsolute(path) ? path : resolve(root, path);
|
|
90
|
+
const named = relative(root, absolute).split(sep).join("/");
|
|
91
|
+
const outside = (shown) => `${shown} is outside this job's write scope (${scope.join(", ")})`;
|
|
92
|
+
if (named.startsWith("..") || !inWriteScope(named, scope)) return outside(named);
|
|
93
|
+
let realRoot;
|
|
94
|
+
let real;
|
|
95
|
+
try {
|
|
96
|
+
realRoot = await realpath(root);
|
|
97
|
+
real = await realpathOfNearestExisting(absolute);
|
|
98
|
+
} catch {
|
|
99
|
+
return `${named}: its real place cannot be checked`;
|
|
100
|
+
}
|
|
101
|
+
const reached = relative(realRoot, real).split(sep).join("/");
|
|
102
|
+
if (reached === "" || reached.startsWith("..") || isAbsolute(reached)) {
|
|
103
|
+
return `${named} leads outside the worktree`;
|
|
104
|
+
}
|
|
105
|
+
if (!inWriteScope(reached, scope))
|
|
106
|
+
return `${named} leads to ${reached}, which ${outside(reached).slice(reached.length + 1)}`;
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export {
|
|
111
|
+
scopeEntryProblem,
|
|
112
|
+
inWriteScope,
|
|
113
|
+
scopePaths,
|
|
114
|
+
prepareWriteScope,
|
|
115
|
+
proposeWriteScope,
|
|
116
|
+
scopeDenial
|
|
117
|
+
};
|
|
118
|
+
//# sourceMappingURL=chunk-UIRJOQQS.js.map
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Redactor,
|
|
4
|
+
contextSize,
|
|
5
|
+
totalTokens
|
|
6
|
+
} from "./chunk-7KBOIPUT.js";
|
|
7
|
+
import {
|
|
8
|
+
cleanLine
|
|
9
|
+
} from "./chunk-PZS3V6P3.js";
|
|
10
|
+
import {
|
|
11
|
+
ZERO_USAGE,
|
|
12
|
+
addUsage,
|
|
13
|
+
reportedUsage
|
|
14
|
+
} from "./chunk-BHZ6G65V.js";
|
|
15
|
+
import {
|
|
16
|
+
formatRule
|
|
17
|
+
} from "./chunk-3NJNVZTK.js";
|
|
18
|
+
|
|
19
|
+
// src/session/store.ts
|
|
20
|
+
import { randomBytes } from "crypto";
|
|
21
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
22
|
+
import { readdir, readFile, rm, stat, utimes } from "fs/promises";
|
|
23
|
+
import { basename, join } from "path";
|
|
24
|
+
var SESSIONS_DIR = join(".garuda", "sessions");
|
|
25
|
+
var FileSessionStore = class {
|
|
26
|
+
dir;
|
|
27
|
+
redactor;
|
|
28
|
+
constructor(root, redactor = new Redactor()) {
|
|
29
|
+
this.dir = join(root, SESSIONS_DIR);
|
|
30
|
+
this.redactor = redactor;
|
|
31
|
+
}
|
|
32
|
+
path(sessionId) {
|
|
33
|
+
if (!SESSION_ID.test(sessionId)) throw new Error(`"${sessionId}" is not a session id.`);
|
|
34
|
+
return join(this.dir, `${sessionId}.jsonl`);
|
|
35
|
+
}
|
|
36
|
+
open(sessionId) {
|
|
37
|
+
return this.journal(this.dir, this.path(sessionId));
|
|
38
|
+
}
|
|
39
|
+
/** `.garuda/sessions/<session id>/<child id>.jsonl`. */
|
|
40
|
+
childPath(sessionId, childId) {
|
|
41
|
+
return join(this.dir, safeId(sessionId), `${safeId(childId)}.jsonl`);
|
|
42
|
+
}
|
|
43
|
+
openChild(sessionId, childId) {
|
|
44
|
+
return this.journal(join(this.dir, safeId(sessionId)), this.childPath(sessionId, childId));
|
|
45
|
+
}
|
|
46
|
+
journal(dir, file) {
|
|
47
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
48
|
+
return {
|
|
49
|
+
write: (record) => {
|
|
50
|
+
const line = { t: (/* @__PURE__ */ new Date()).toISOString(), ...record };
|
|
51
|
+
appendFileSync(file, `${JSON.stringify(this.redactor.value(line))}
|
|
52
|
+
`, { mode: 384 });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async read(sessionId) {
|
|
57
|
+
return parseRecords(await readFile(this.path(sessionId), "utf8"));
|
|
58
|
+
}
|
|
59
|
+
async setTitle(sessionId, title) {
|
|
60
|
+
const file = this.path(sessionId);
|
|
61
|
+
const before = await stat(file);
|
|
62
|
+
this.open(sessionId).write({ type: "title", title });
|
|
63
|
+
await utimes(file, before.atime, before.mtime);
|
|
64
|
+
}
|
|
65
|
+
async remove(sessionId) {
|
|
66
|
+
await rm(this.path(sessionId));
|
|
67
|
+
await rm(join(this.dir, safeId(sessionId)), { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
async latest() {
|
|
70
|
+
return (await this.list())[0]?.id;
|
|
71
|
+
}
|
|
72
|
+
async list() {
|
|
73
|
+
let names;
|
|
74
|
+
try {
|
|
75
|
+
names = (await readdir(this.dir)).filter((name) => name.endsWith(".jsonl"));
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const name of names) {
|
|
81
|
+
const info = await stat(join(this.dir, name)).catch(() => void 0);
|
|
82
|
+
if (info?.isFile()) out.push({ id: basename(name, ".jsonl"), updated: info.mtime });
|
|
83
|
+
}
|
|
84
|
+
return out.sort((a, b) => b.updated.getTime() - a.updated.getTime());
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var SESSION_ID = /^[A-Za-z0-9_-]{1,80}$/;
|
|
88
|
+
function safeId(id) {
|
|
89
|
+
return id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80) || "_";
|
|
90
|
+
}
|
|
91
|
+
function parseRecords(text) {
|
|
92
|
+
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
93
|
+
const records = [];
|
|
94
|
+
lines.forEach((line, i) => {
|
|
95
|
+
try {
|
|
96
|
+
records.push(JSON.parse(line));
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (i < lines.length - 1) throw new Error(`Session file line ${i + 1} is not valid JSON.`);
|
|
99
|
+
void error;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return records;
|
|
103
|
+
}
|
|
104
|
+
function newSessionId(now = /* @__PURE__ */ new Date()) {
|
|
105
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
106
|
+
const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
|
|
107
|
+
const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
108
|
+
return `${date}-${time}-${randomBytes(2).toString("hex")}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/session/fileTracker.ts
|
|
112
|
+
import { createHash } from "crypto";
|
|
113
|
+
var FileTracker = class {
|
|
114
|
+
seen = /* @__PURE__ */ new Map();
|
|
115
|
+
/** "path|offset|limit" → hash of the whole file at that read. */
|
|
116
|
+
reads = /* @__PURE__ */ new Map();
|
|
117
|
+
/** Record the content the agent now knows for `path` (absolute). */
|
|
118
|
+
record(path, content) {
|
|
119
|
+
this.seen.set(path, hash(content));
|
|
120
|
+
}
|
|
121
|
+
/** "unread", "changed" or "current". */
|
|
122
|
+
status(path, content) {
|
|
123
|
+
const known = this.seen.get(path);
|
|
124
|
+
if (known === void 0) return "unread";
|
|
125
|
+
return known === hash(content) ? "current" : "changed";
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Record a read of some lines. Returns true when the agent already read the same lines
|
|
129
|
+
* of the same content, so the output is still in its context.
|
|
130
|
+
*/
|
|
131
|
+
noteRead(path, content, offset, limit) {
|
|
132
|
+
const key = `${path}|${offset}|${limit}`;
|
|
133
|
+
const digest = hash(content);
|
|
134
|
+
const repeat = this.reads.get(key) === digest;
|
|
135
|
+
this.reads.set(key, digest);
|
|
136
|
+
return repeat;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Compaction cuts or summarises old tool outputs, so earlier reads may no longer be in the context.
|
|
140
|
+
* The edit freshness records stay: they are about the file, not the context.
|
|
141
|
+
*/
|
|
142
|
+
forgetReads() {
|
|
143
|
+
this.reads.clear();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
function hash(content) {
|
|
147
|
+
return createHash("sha256").update(content).digest("hex");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/session/undo.ts
|
|
151
|
+
var MAX_UNDO_POINTS = 50;
|
|
152
|
+
var PROMPT_CHARS = 80;
|
|
153
|
+
function emptyUndoState() {
|
|
154
|
+
return { points: [], redo: [] };
|
|
155
|
+
}
|
|
156
|
+
function pushPoint(state, tree, messages, prompt) {
|
|
157
|
+
const line = prompt.replace(/\s+/g, " ").trim();
|
|
158
|
+
state.points.push({
|
|
159
|
+
tree,
|
|
160
|
+
messages,
|
|
161
|
+
conversation: true,
|
|
162
|
+
prompt: line.length > PROMPT_CHARS ? `${line.slice(0, PROMPT_CHARS)}\u2026` : line
|
|
163
|
+
});
|
|
164
|
+
if (state.points.length > MAX_UNDO_POINTS) state.points.shift();
|
|
165
|
+
state.redo = [];
|
|
166
|
+
}
|
|
167
|
+
function popPoint(state, messages, after) {
|
|
168
|
+
const point = state.points.pop();
|
|
169
|
+
if (point === void 0) return void 0;
|
|
170
|
+
const removed = point.conversation ? messages.splice(point.messages) : [];
|
|
171
|
+
state.redo.push({ point, after, removed });
|
|
172
|
+
return point;
|
|
173
|
+
}
|
|
174
|
+
function redoPoint(state, messages) {
|
|
175
|
+
const entry = state.redo.pop();
|
|
176
|
+
if (entry === void 0) return void 0;
|
|
177
|
+
messages.push(...entry.removed);
|
|
178
|
+
state.points.push(entry.point);
|
|
179
|
+
return entry;
|
|
180
|
+
}
|
|
181
|
+
function compacted(state) {
|
|
182
|
+
for (const point of state.points) point.conversation = false;
|
|
183
|
+
for (const entry of state.redo) {
|
|
184
|
+
entry.point.conversation = false;
|
|
185
|
+
entry.removed = [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function newUserMessage(state) {
|
|
189
|
+
state.redo = [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/session/session.ts
|
|
193
|
+
function createSession(root, id = newSessionId(), journal) {
|
|
194
|
+
const session = {
|
|
195
|
+
id,
|
|
196
|
+
root,
|
|
197
|
+
messages: [],
|
|
198
|
+
usage: { ...ZERO_USAGE },
|
|
199
|
+
costUsd: 0,
|
|
200
|
+
costIncomplete: false,
|
|
201
|
+
contextTokens: 0,
|
|
202
|
+
unreportedTokens: 0,
|
|
203
|
+
childDebits: /* @__PURE__ */ new Map(),
|
|
204
|
+
files: new FileTracker(),
|
|
205
|
+
undo: emptyUndoState()
|
|
206
|
+
};
|
|
207
|
+
if (journal !== void 0) session.journal = journal;
|
|
208
|
+
return session;
|
|
209
|
+
}
|
|
210
|
+
function addUserMessage(session, text, notes = [], attachments = []) {
|
|
211
|
+
const message = {
|
|
212
|
+
role: "user",
|
|
213
|
+
content: [
|
|
214
|
+
{ type: "text", text },
|
|
215
|
+
...attachments.map((a) => ({ type: "text", text: a })),
|
|
216
|
+
...notes.map((note) => ({
|
|
217
|
+
type: "text",
|
|
218
|
+
text: `<garuda_note>${note}</garuda_note>`
|
|
219
|
+
}))
|
|
220
|
+
]
|
|
221
|
+
};
|
|
222
|
+
session.messages.push(message);
|
|
223
|
+
newUserMessage(session.undo);
|
|
224
|
+
session.journal?.write({ type: "user", message });
|
|
225
|
+
}
|
|
226
|
+
function addSnapshot(session, tree, prompt, durationMs) {
|
|
227
|
+
const messages = session.messages.length;
|
|
228
|
+
pushPoint(session.undo, tree, messages, prompt);
|
|
229
|
+
const point = session.undo.points.at(-1);
|
|
230
|
+
session.journal?.write({ type: "snapshot", tree, messages, prompt: point.prompt, durationMs });
|
|
231
|
+
}
|
|
232
|
+
function undoTurn(session, after) {
|
|
233
|
+
const point = popPoint(session.undo, session.messages, after);
|
|
234
|
+
if (point !== void 0) {
|
|
235
|
+
session.journal?.write({ type: "undo", after });
|
|
236
|
+
session.files.forgetReads();
|
|
237
|
+
}
|
|
238
|
+
return point;
|
|
239
|
+
}
|
|
240
|
+
function redoTurn(session) {
|
|
241
|
+
const entry = redoPoint(session.undo, session.messages);
|
|
242
|
+
if (entry !== void 0) {
|
|
243
|
+
session.journal?.write({ type: "redo" });
|
|
244
|
+
session.files.forgetReads();
|
|
245
|
+
}
|
|
246
|
+
return entry;
|
|
247
|
+
}
|
|
248
|
+
function addAssistantResponse(session, response, step, costUsd) {
|
|
249
|
+
if (response.usageReported === false) {
|
|
250
|
+
session.costIncomplete = true;
|
|
251
|
+
costUsd = void 0;
|
|
252
|
+
addCost(session, reportedUsage(response), session.costUsd === void 0 ? void 0 : 0);
|
|
253
|
+
} else addCost(session, reportedUsage(response), costUsd);
|
|
254
|
+
session.contextTokens = contextSize(response.usage);
|
|
255
|
+
if (response.content.length > 0) {
|
|
256
|
+
session.messages.push({ role: "assistant", content: response.content });
|
|
257
|
+
}
|
|
258
|
+
session.journal?.write({
|
|
259
|
+
type: "assistant",
|
|
260
|
+
step,
|
|
261
|
+
response,
|
|
262
|
+
...costUsd === void 0 ? {} : { costUsd }
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function addToolResults(session, results, calls, synthetic = false) {
|
|
266
|
+
const message = { role: "user", content: results };
|
|
267
|
+
session.messages.push(message);
|
|
268
|
+
session.journal?.write({
|
|
269
|
+
type: "tool_results",
|
|
270
|
+
message,
|
|
271
|
+
calls,
|
|
272
|
+
...synthetic ? { synthetic } : {}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function addContinuation(session, text, maxTokens) {
|
|
276
|
+
const message = { role: "user", content: [{ type: "text", text }] };
|
|
277
|
+
session.messages.push(message);
|
|
278
|
+
session.journal?.write({ type: "continue", message, maxTokens });
|
|
279
|
+
}
|
|
280
|
+
function addCost(session, usage, costUsd) {
|
|
281
|
+
session.usage = addUsage(session.usage, usage);
|
|
282
|
+
session.costUsd = session.costUsd === void 0 || costUsd === void 0 ? void 0 : session.costUsd + costUsd;
|
|
283
|
+
}
|
|
284
|
+
function closeOpenToolCalls(session) {
|
|
285
|
+
const last = session.messages.at(-1);
|
|
286
|
+
if (last?.role !== "assistant") return 0;
|
|
287
|
+
const open = last.content.filter((b) => b.type === "tool_use");
|
|
288
|
+
if (open.length === 0) return 0;
|
|
289
|
+
session.files.forgetReads();
|
|
290
|
+
addToolResults(
|
|
291
|
+
session,
|
|
292
|
+
open.map((call) => ({
|
|
293
|
+
type: "tool_result",
|
|
294
|
+
toolUseId: call.id,
|
|
295
|
+
content: "Error: this call was interrupted. It may or may not have run. Check before you retry.",
|
|
296
|
+
isError: true
|
|
297
|
+
})),
|
|
298
|
+
open.map((call) => ({ toolUseId: call.id, name: call.name, durationMs: 0 })),
|
|
299
|
+
true
|
|
300
|
+
);
|
|
301
|
+
return open.length;
|
|
302
|
+
}
|
|
303
|
+
function addUnreported(session, tokens) {
|
|
304
|
+
if (tokens <= 0) return;
|
|
305
|
+
session.unreportedTokens += tokens;
|
|
306
|
+
session.costIncomplete = true;
|
|
307
|
+
session.journal?.write({ type: "unreported", tokens });
|
|
308
|
+
}
|
|
309
|
+
function budgetSpent(session) {
|
|
310
|
+
return totalTokens(session.usage) + session.unreportedTokens + pendingChildTokens(session);
|
|
311
|
+
}
|
|
312
|
+
function pendingChildTokens(session) {
|
|
313
|
+
let tokens = 0;
|
|
314
|
+
for (const value of session.childDebits.values()) tokens += value;
|
|
315
|
+
return tokens;
|
|
316
|
+
}
|
|
317
|
+
function isCostIncomplete(session) {
|
|
318
|
+
return session.costIncomplete || pendingChildTokens(session) > 0;
|
|
319
|
+
}
|
|
320
|
+
function addChildDebit(session, childSessionId, tokens) {
|
|
321
|
+
if (tokens <= 0) return;
|
|
322
|
+
session.childDebits.set(childSessionId, (session.childDebits.get(childSessionId) ?? 0) + tokens);
|
|
323
|
+
session.journal?.write({ type: "child_debit", childSessionId, tokens });
|
|
324
|
+
}
|
|
325
|
+
function addChildReport(session, report) {
|
|
326
|
+
addCost(session, report.usage, report.costUsd);
|
|
327
|
+
if (report.costIncomplete === true) session.costIncomplete = true;
|
|
328
|
+
settleChildDebits(session.childDebits, report);
|
|
329
|
+
}
|
|
330
|
+
function settleChildDebits(debits, report) {
|
|
331
|
+
const rest = Math.max(0, (debits.get(report.sessionId) ?? 0) - totalTokens(report.usage));
|
|
332
|
+
if (rest === 0) debits.delete(report.sessionId);
|
|
333
|
+
else debits.set(report.sessionId, rest);
|
|
334
|
+
}
|
|
335
|
+
function addSideUsage(session, purpose, response, costUsd) {
|
|
336
|
+
if (response.usageReported === false) return;
|
|
337
|
+
addCost(session, response.usage, costUsd);
|
|
338
|
+
session.journal?.write({
|
|
339
|
+
type: "side_usage",
|
|
340
|
+
purpose,
|
|
341
|
+
usage: response.usage,
|
|
342
|
+
...costUsd === void 0 ? {} : { costUsd }
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/permissions/projectSettings.ts
|
|
347
|
+
import { createHash as createHash2 } from "crypto";
|
|
348
|
+
function settingsRisk(settings) {
|
|
349
|
+
const risk = {
|
|
350
|
+
executorHost: settings.executor === "host",
|
|
351
|
+
allow: settings.allow.map(formatRule),
|
|
352
|
+
writePaths: [...settings.sandbox?.writePaths ?? []],
|
|
353
|
+
envAllow: [...settings.envAllow],
|
|
354
|
+
allowLocalhost: settings.web?.allowLocalhost === true
|
|
355
|
+
};
|
|
356
|
+
const formatterCommands = Object.entries(settings.formatters?.commands ?? {}).flatMap(
|
|
357
|
+
([name, f]) => f === false ? [] : [`${name}: ${f.command.join(" ")}`]
|
|
358
|
+
);
|
|
359
|
+
if (formatterCommands.length > 0) risk.formatterCommands = formatterCommands;
|
|
360
|
+
const none = !risk.executorHost && risk.allow.length === 0 && risk.writePaths.length === 0 && risk.envAllow.length === 0 && !risk.allowLocalhost && risk.formatterCommands === void 0;
|
|
361
|
+
return none ? void 0 : risk;
|
|
362
|
+
}
|
|
363
|
+
function settingsRiskHash(risk) {
|
|
364
|
+
return createHash2("sha256").update(JSON.stringify(risk)).digest("hex");
|
|
365
|
+
}
|
|
366
|
+
function settingsRiskLines(risk) {
|
|
367
|
+
const lines = [];
|
|
368
|
+
if (risk.executorHost) lines.push('executor: "host" (commands run with no OS sandbox)');
|
|
369
|
+
if (risk.allow.length > 0) {
|
|
370
|
+
lines.push(`permissions.allow (no question for these): ${risk.allow.join(", ")}`);
|
|
371
|
+
}
|
|
372
|
+
if (risk.writePaths.length > 0) {
|
|
373
|
+
lines.push(`sandbox.writePaths (commands may write there): ${risk.writePaths.join(", ")}`);
|
|
374
|
+
}
|
|
375
|
+
if (risk.envAllow.length > 0) {
|
|
376
|
+
lines.push(`env.allow (commands see these variables): ${risk.envAllow.join(", ")}`);
|
|
377
|
+
}
|
|
378
|
+
if (risk.allowLocalhost) lines.push("web.allowLocalhost (web_fetch may reach this machine)");
|
|
379
|
+
if (risk.formatterCommands !== void 0) {
|
|
380
|
+
lines.push(
|
|
381
|
+
`formatters.commands (run after each edit): ${risk.formatterCommands.map(cleanLine).join("; ")}`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
return lines;
|
|
385
|
+
}
|
|
386
|
+
function withoutRisk(settings) {
|
|
387
|
+
const { sandbox, web, formatters, ...rest } = settings;
|
|
388
|
+
const { writePaths: _writePaths, ...sandboxRest } = sandbox ?? {};
|
|
389
|
+
const keptCommands = Object.fromEntries(
|
|
390
|
+
Object.entries(formatters?.commands ?? {}).filter(([, f]) => f === false)
|
|
391
|
+
);
|
|
392
|
+
return {
|
|
393
|
+
...rest,
|
|
394
|
+
...formatters === void 0 ? {} : { formatters: { ...formatters, commands: keptCommands } },
|
|
395
|
+
executor: settings.executor === "host" ? "auto" : settings.executor,
|
|
396
|
+
allow: [],
|
|
397
|
+
envAllow: [],
|
|
398
|
+
...sandbox === void 0 ? {} : { sandbox: sandboxRest },
|
|
399
|
+
...web === void 0 ? {} : { web: { ...web, allowLocalhost: false } }
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async function gateProjectSettings(options) {
|
|
403
|
+
const risk = settingsRisk(options.settings);
|
|
404
|
+
if (risk === void 0) return { settings: options.settings };
|
|
405
|
+
const hash2 = settingsRiskHash(risk);
|
|
406
|
+
const known = options.trust?.settingsHash(options.root);
|
|
407
|
+
if (known === hash2) return { settings: options.settings };
|
|
408
|
+
const lines = settingsRiskLines(risk);
|
|
409
|
+
if (options.ask !== void 0) {
|
|
410
|
+
const choice = await options.ask({
|
|
411
|
+
tool: "settings",
|
|
412
|
+
target: { kind: "input", json: "" },
|
|
413
|
+
title: "Project settings that loosen safety",
|
|
414
|
+
preview: [
|
|
415
|
+
`This project's .garuda/settings.json ${known === void 0 ? "asks for" : "changed and now asks for"}:`,
|
|
416
|
+
...lines.map((line) => ` - ${line}`),
|
|
417
|
+
"A cloned repository can ship this file. Without your yes, Garuda uses safe defaults for these",
|
|
418
|
+
"parts; the rest of the file applies."
|
|
419
|
+
].join("\n"),
|
|
420
|
+
isolation: options.isolation ?? "os",
|
|
421
|
+
question: "Use these project settings?",
|
|
422
|
+
choices: ["once", "session", "deny"],
|
|
423
|
+
labels: {
|
|
424
|
+
once: "Yes, for this run",
|
|
425
|
+
session: "Yes, and remember (asks again when they change)",
|
|
426
|
+
deny: "No, use safe defaults"
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
if (choice === "session") await options.trust?.setSettingsHash(options.root, hash2);
|
|
430
|
+
if (choice !== "deny") return { settings: options.settings };
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
settings: withoutRisk(options.settings),
|
|
434
|
+
notice: [
|
|
435
|
+
`Project settings not applied (not approved): ${lines.join("; ")}.`,
|
|
436
|
+
"Start the chat in a terminal and answer yes to use them."
|
|
437
|
+
].join(" ")
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export {
|
|
442
|
+
FileTracker,
|
|
443
|
+
FileSessionStore,
|
|
444
|
+
parseRecords,
|
|
445
|
+
newSessionId,
|
|
446
|
+
emptyUndoState,
|
|
447
|
+
pushPoint,
|
|
448
|
+
popPoint,
|
|
449
|
+
redoPoint,
|
|
450
|
+
compacted,
|
|
451
|
+
newUserMessage,
|
|
452
|
+
createSession,
|
|
453
|
+
addUserMessage,
|
|
454
|
+
addSnapshot,
|
|
455
|
+
undoTurn,
|
|
456
|
+
redoTurn,
|
|
457
|
+
addAssistantResponse,
|
|
458
|
+
addToolResults,
|
|
459
|
+
addContinuation,
|
|
460
|
+
addCost,
|
|
461
|
+
closeOpenToolCalls,
|
|
462
|
+
addUnreported,
|
|
463
|
+
budgetSpent,
|
|
464
|
+
pendingChildTokens,
|
|
465
|
+
isCostIncomplete,
|
|
466
|
+
addChildDebit,
|
|
467
|
+
addChildReport,
|
|
468
|
+
settleChildDebits,
|
|
469
|
+
addSideUsage,
|
|
470
|
+
gateProjectSettings
|
|
471
|
+
};
|
|
472
|
+
//# sourceMappingURL=chunk-UKIE4AAM.js.map
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
scopeEntryProblem
|
|
4
|
+
} from "./chunk-UIRJOQQS.js";
|
|
2
5
|
import {
|
|
3
6
|
parseRule
|
|
4
7
|
} from "./chunk-3NJNVZTK.js";
|
|
@@ -29,6 +32,8 @@ var resultSchema = z.object({
|
|
|
29
32
|
steps: z.number(),
|
|
30
33
|
tokens: z.number(),
|
|
31
34
|
costUsd: z.number().optional(),
|
|
35
|
+
/** The cost leaves out some spending: `costUsd` is a minimum (review of 1370184). */
|
|
36
|
+
costIncomplete: z.boolean().optional(),
|
|
32
37
|
durationMs: z.number(),
|
|
33
38
|
sessionId: z.string().optional(),
|
|
34
39
|
commit: z.string().optional(),
|
|
@@ -55,7 +60,12 @@ var resultSchema = z.object({
|
|
|
55
60
|
text: z.string(),
|
|
56
61
|
costUsd: z.number().optional()
|
|
57
62
|
}).optional(),
|
|
58
|
-
reviewError: z.string().optional()
|
|
63
|
+
reviewError: z.string().optional(),
|
|
64
|
+
/**
|
|
65
|
+
* The review spent tokens with no reported usage (a failed attempt, an estimate): its cost is
|
|
66
|
+
* a minimum or unrecorded (review of 3a9f51b).
|
|
67
|
+
*/
|
|
68
|
+
reviewCostIncomplete: z.boolean().optional()
|
|
59
69
|
}).optional()
|
|
60
70
|
});
|
|
61
71
|
var jobSchema = z.object({
|
|
@@ -76,6 +86,16 @@ var jobSchema = z.object({
|
|
|
76
86
|
model: z.string().optional(),
|
|
77
87
|
/** Permission rules for this job only, in the settings format ("edit_file(src/**)"). */
|
|
78
88
|
allow: z.array(z.string()),
|
|
89
|
+
/**
|
|
90
|
+
* The write scope (0.17, review T6): the paths in the worktree that the job's commands and edits
|
|
91
|
+
* may change ("src/" is a folder, "README.md" one file). The OS sandbox enforces it for commands.
|
|
92
|
+
* Absent (a job made before 0.17, or a plan whose rules name no folder): the whole worktree.
|
|
93
|
+
*/
|
|
94
|
+
writeScope: z.array(
|
|
95
|
+
z.string().refine((entry) => scopeEntryProblem(entry) === void 0, {
|
|
96
|
+
message: 'a scope entry is a path in the worktree, such as "src/" or "README.md"'
|
|
97
|
+
})
|
|
98
|
+
).optional(),
|
|
79
99
|
onUnapproved: z.literal("deny-and-continue"),
|
|
80
100
|
/** "01:00" (local time) for `garuda run --at`, when the user gave one. */
|
|
81
101
|
at: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).optional(),
|
|
@@ -114,6 +134,9 @@ var jobSchema = z.object({
|
|
|
114
134
|
endedAt: z.string().optional(),
|
|
115
135
|
result: resultSchema.optional()
|
|
116
136
|
});
|
|
137
|
+
function parseJob(json) {
|
|
138
|
+
return jobSchema.parse(json);
|
|
139
|
+
}
|
|
117
140
|
function newJobId(now = /* @__PURE__ */ new Date()) {
|
|
118
141
|
const pad = (n) => String(n).padStart(2, "0");
|
|
119
142
|
const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
|
|
@@ -218,6 +241,7 @@ export {
|
|
|
218
241
|
JOBS_DIR,
|
|
219
242
|
DEFAULT_JOB_MAX_STEPS,
|
|
220
243
|
JOB_STATUSES,
|
|
244
|
+
parseJob,
|
|
221
245
|
newJobId,
|
|
222
246
|
jobPath,
|
|
223
247
|
failJobSync,
|
|
@@ -228,4 +252,4 @@ export {
|
|
|
228
252
|
planPermissions,
|
|
229
253
|
planTitle
|
|
230
254
|
};
|
|
231
|
-
//# sourceMappingURL=chunk-
|
|
255
|
+
//# sourceMappingURL=chunk-VTMD5IHD.js.map
|