@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,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
keepSecretForRedaction
|
|
4
|
+
} from "./chunk-ITITLNAR.js";
|
|
5
|
+
import {
|
|
6
|
+
lookupModel
|
|
7
|
+
} from "./chunk-W7BPYQMW.js";
|
|
8
|
+
import {
|
|
9
|
+
isLoopbackHost
|
|
10
|
+
} from "./chunk-ATW7BNIG.js";
|
|
11
|
+
|
|
12
|
+
// src/model/providers.ts
|
|
13
|
+
import { readFile } from "fs/promises";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
var MODELS_FILE = join(".garuda", "models.json");
|
|
18
|
+
var priceSchema = z.strictObject({
|
|
19
|
+
input: z.number().min(0),
|
|
20
|
+
output: z.number().min(0),
|
|
21
|
+
cacheRead: z.number().min(0),
|
|
22
|
+
cacheWrite: z.number().min(0)
|
|
23
|
+
});
|
|
24
|
+
var providerSchema = z.strictObject({
|
|
25
|
+
type: z.enum(["anthropic", "openai-compatible"]),
|
|
26
|
+
/** For openai-compatible: the URL that ends before /chat/completions. */
|
|
27
|
+
baseUrl: z.url().optional(),
|
|
28
|
+
/** The environment variable that holds the API key. */
|
|
29
|
+
apiKeyEnv: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).optional(),
|
|
30
|
+
/** A server on this machine: price 0 unless a model entry says otherwise. */
|
|
31
|
+
local: z.boolean().optional(),
|
|
32
|
+
/** Allow plain http to a host that is not this machine. Off: an API key could travel in clear text. */
|
|
33
|
+
allowInsecureHttp: z.boolean().optional()
|
|
34
|
+
});
|
|
35
|
+
var modelSchema = z.strictObject({
|
|
36
|
+
contextWindow: z.number().int().min(4096).optional(),
|
|
37
|
+
price: priceSchema.optional(),
|
|
38
|
+
maxTokens: z.number().int().min(256).optional(),
|
|
39
|
+
/**
|
|
40
|
+
* Tool calls that the model writes as JSON text (openai-compatible only): "whole" (default) when
|
|
41
|
+
* the whole reply is calls, "lines" when calls stand on their own lines between prose, "off".
|
|
42
|
+
*/
|
|
43
|
+
textToolCalls: z.enum(["off", "whole", "lines"]).optional()
|
|
44
|
+
});
|
|
45
|
+
var fileSchema = z.strictObject({
|
|
46
|
+
providers: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,31}$/), providerSchema).default({}),
|
|
47
|
+
/** Keyed by the full spec, for example "ollama/qwen3-coder:30b". */
|
|
48
|
+
models: z.record(z.string(), modelSchema).default({})
|
|
49
|
+
});
|
|
50
|
+
var BUILTIN_PROVIDERS = {
|
|
51
|
+
anthropic: { type: "anthropic" },
|
|
52
|
+
ollama: { type: "openai-compatible", baseUrl: "http://localhost:11434/v1", local: true },
|
|
53
|
+
lmstudio: { type: "openai-compatible", baseUrl: "http://localhost:1234/v1", local: true },
|
|
54
|
+
llamacpp: { type: "openai-compatible", baseUrl: "http://localhost:8080/v1", local: true },
|
|
55
|
+
vllm: { type: "openai-compatible", baseUrl: "http://localhost:8000/v1", local: true },
|
|
56
|
+
openrouter: {
|
|
57
|
+
type: "openai-compatible",
|
|
58
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
59
|
+
apiKeyEnv: "OPENROUTER_API_KEY"
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var OPEN_MODEL_DEFAULT_WINDOW = 32768;
|
|
63
|
+
var ZERO_PRICE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
64
|
+
async function loadModelsConfig(home = homedir()) {
|
|
65
|
+
const file = join(home, MODELS_FILE);
|
|
66
|
+
let text;
|
|
67
|
+
try {
|
|
68
|
+
text = await readFile(file, "utf8");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error.code === "ENOENT")
|
|
71
|
+
return { config: { providers: {}, models: {} } };
|
|
72
|
+
return {
|
|
73
|
+
config: { providers: {}, models: {} },
|
|
74
|
+
problem: `${file}: ${error.message}`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const parsed = fileSchema.safeParse(JSON.parse(text));
|
|
79
|
+
if (!parsed.success) {
|
|
80
|
+
return {
|
|
81
|
+
config: { providers: {}, models: {} },
|
|
82
|
+
problem: `${file}: ${z.prettifyError(parsed.error)}`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { config: parsed.data };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return {
|
|
88
|
+
config: { providers: {}, models: {} },
|
|
89
|
+
problem: `${file}: invalid JSON: ${error.message}`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
var keptKeys = /* @__PURE__ */ new Map();
|
|
94
|
+
function keepProviderKey(name) {
|
|
95
|
+
const value = process.env[name];
|
|
96
|
+
if (value !== void 0 && value !== "") {
|
|
97
|
+
keptKeys.set(name, value);
|
|
98
|
+
keepSecretForRedaction(name, value);
|
|
99
|
+
delete process.env[name];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function hasProviderKey(name) {
|
|
103
|
+
return keptKeys.has(name) || (process.env[name] ?? "") !== "";
|
|
104
|
+
}
|
|
105
|
+
function providerKey(env, name) {
|
|
106
|
+
if (name === void 0) return void 0;
|
|
107
|
+
return keptKeys.get(name) ?? env[name];
|
|
108
|
+
}
|
|
109
|
+
function resolveModel(spec, config = { providers: {}, models: {} }) {
|
|
110
|
+
const providers = { ...BUILTIN_PROVIDERS, ...config.providers };
|
|
111
|
+
const slash = spec.indexOf("/");
|
|
112
|
+
const [provider, model] = slash === -1 ? ["anthropic", spec] : [spec.slice(0, slash), spec.slice(slash + 1)];
|
|
113
|
+
const def = providers[provider];
|
|
114
|
+
if (def === void 0) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Unknown model provider "${provider}" in "${spec}". Known providers: ${Object.keys(providers).join(", ")}. Add others in ~/${MODELS_FILE}.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (model === "") throw new Error(`"${spec}" names no model. Use <provider>/<model>.`);
|
|
120
|
+
if (def.type === "openai-compatible") checkBaseUrl(provider, def);
|
|
121
|
+
const entry = config.models[spec] ?? (provider === "anthropic" ? config.models[model] : void 0);
|
|
122
|
+
const base = def.type === "anthropic" ? lookupModel(model) : { contextWindow: OPEN_MODEL_DEFAULT_WINDOW, ...def.local ? { price: ZERO_PRICE } : {} };
|
|
123
|
+
const info = {
|
|
124
|
+
contextWindow: entry?.contextWindow ?? base.contextWindow,
|
|
125
|
+
...entry?.price !== void 0 ? { price: entry.price } : base.price !== void 0 ? { price: base.price } : {},
|
|
126
|
+
// Thinking by model (0.9): only Claude models on the Anthropic provider have it.
|
|
127
|
+
...base.thinking === void 0 ? {} : { thinking: base.thinking }
|
|
128
|
+
};
|
|
129
|
+
const notes = [];
|
|
130
|
+
if (def.type === "openai-compatible" && entry?.contextWindow === void 0) {
|
|
131
|
+
notes.push(
|
|
132
|
+
`Garuda assumes a ${OPEN_MODEL_DEFAULT_WINDOW}-token context window for ${spec}. Set the real value ("contextWindow") for "${spec}" in ~/${MODELS_FILE}; the server must allow it too (for Ollama: OLLAMA_CONTEXT_LENGTH).`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
spec,
|
|
137
|
+
provider,
|
|
138
|
+
model,
|
|
139
|
+
def,
|
|
140
|
+
info,
|
|
141
|
+
notes,
|
|
142
|
+
...entry?.maxTokens === void 0 ? {} : { maxTokens: entry.maxTokens },
|
|
143
|
+
...def.type === "anthropic" ? {
|
|
144
|
+
async createBatch(env = process.env, options = {}) {
|
|
145
|
+
const apiKey = providerKey(env, def.apiKeyEnv ?? "ANTHROPIC_API_KEY");
|
|
146
|
+
const { AnthropicBatchClient } = await import("./anthropic-NQDBCHJU.js");
|
|
147
|
+
return new AnthropicBatchClient({ model, ...apiKey ? { apiKey } : {}, ...options });
|
|
148
|
+
}
|
|
149
|
+
} : {},
|
|
150
|
+
async create(env = process.env) {
|
|
151
|
+
const apiKey = providerKey(env, def.apiKeyEnv);
|
|
152
|
+
if (def.apiKeyEnv !== void 0 && (apiKey === void 0 || apiKey === "")) {
|
|
153
|
+
throw new Error(`Set ${def.apiKeyEnv} to use the ${provider} provider.`);
|
|
154
|
+
}
|
|
155
|
+
if (def.type === "anthropic") {
|
|
156
|
+
const key = providerKey(env, def.apiKeyEnv ?? "ANTHROPIC_API_KEY");
|
|
157
|
+
const { AnthropicClient } = await import("./anthropic-NQDBCHJU.js");
|
|
158
|
+
return new AnthropicClient({ model, ...key ? { apiKey: key } : {} });
|
|
159
|
+
}
|
|
160
|
+
const { OpenAICompatibleClient } = await import("./openaiCompatible-CJWVJP5M.js");
|
|
161
|
+
return new OpenAICompatibleClient({
|
|
162
|
+
provider,
|
|
163
|
+
baseUrl: def.baseUrl,
|
|
164
|
+
model,
|
|
165
|
+
...apiKey === void 0 ? {} : { apiKey },
|
|
166
|
+
...entry?.textToolCalls === void 0 ? {} : { textToolCalls: entry.textToolCalls }
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function checkBaseUrl(provider, def) {
|
|
172
|
+
if (def.baseUrl === void 0) throw new Error(`Provider "${provider}" needs a baseUrl.`);
|
|
173
|
+
const url = new URL(def.baseUrl);
|
|
174
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
175
|
+
throw new Error(`Provider "${provider}": the baseUrl must use http or https.`);
|
|
176
|
+
}
|
|
177
|
+
if (url.username !== "" || url.password !== "") {
|
|
178
|
+
throw new Error(`Provider "${provider}": put credentials in apiKeyEnv, not in the baseUrl.`);
|
|
179
|
+
}
|
|
180
|
+
if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && def.allowInsecureHttp !== true) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Provider "${provider}" uses plain http to ${url.hostname}. Use https, or set "allowInsecureHttp": true for a trusted network.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/permissions/autoApprover.ts
|
|
188
|
+
var AutoApprover = class {
|
|
189
|
+
requests = [];
|
|
190
|
+
answer;
|
|
191
|
+
constructor(answer = "once") {
|
|
192
|
+
this.answer = typeof answer === "function" ? answer : () => answer;
|
|
193
|
+
}
|
|
194
|
+
async ask(request) {
|
|
195
|
+
this.requests.push(request);
|
|
196
|
+
return this.answer(request);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
loadModelsConfig,
|
|
202
|
+
keepProviderKey,
|
|
203
|
+
hasProviderKey,
|
|
204
|
+
resolveModel,
|
|
205
|
+
AutoApprover
|
|
206
|
+
};
|
|
207
|
+
//# sourceMappingURL=chunk-V7WPGQLB.js.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
cleanLine
|
|
4
|
+
} from "./chunk-PZS3V6P3.js";
|
|
5
|
+
import {
|
|
6
|
+
EFFORTS
|
|
7
|
+
} from "./chunk-D5O5CIWK.js";
|
|
8
|
+
import {
|
|
9
|
+
formatRule
|
|
10
|
+
} from "./chunk-3NJNVZTK.js";
|
|
11
|
+
|
|
12
|
+
// src/model/pricing.ts
|
|
13
|
+
var DEFAULT_CONTEXT_WINDOW = 2e5;
|
|
14
|
+
var M = 1e6;
|
|
15
|
+
var ALWAYS = { mode: "always", efforts: EFFORTS };
|
|
16
|
+
var OPTIONAL = { mode: "optional", efforts: EFFORTS };
|
|
17
|
+
var OPTIONAL_46 = {
|
|
18
|
+
mode: "optional",
|
|
19
|
+
efforts: EFFORTS.filter((e) => e !== "xhigh")
|
|
20
|
+
};
|
|
21
|
+
var KNOWN = [
|
|
22
|
+
["claude-fable-5-1", { price: p(10, 50, 0.25, 12.5), contextWindow: M, thinking: ALWAYS }],
|
|
23
|
+
["claude-fable-5", { price: p(10, 50, 1, 12.5), contextWindow: M, thinking: ALWAYS }],
|
|
24
|
+
["claude-opus-5-5", { price: p(4, 20, 0.2, 5), contextWindow: M, thinking: ALWAYS }],
|
|
25
|
+
["claude-opus-5", { price: p(5, 25, 0.5, 6.25), contextWindow: M, thinking: ALWAYS }],
|
|
26
|
+
["claude-opus-4-8", { price: p(5, 25, 0.5, 6.25), contextWindow: M, thinking: OPTIONAL }],
|
|
27
|
+
["claude-opus-4-7", { price: p(5, 25, 0.5, 6.25), contextWindow: M, thinking: OPTIONAL }],
|
|
28
|
+
["claude-opus-4-6", { price: p(5, 25, 0.5, 6.25), contextWindow: M, thinking: OPTIONAL_46 }],
|
|
29
|
+
["claude-opus-4-5", { price: p(5, 25, 0.5, 6.25), contextWindow: 2e5 }],
|
|
30
|
+
["claude-sonnet-5", { price: p(2, 10, 0.2, 2.5), contextWindow: M, thinking: ALWAYS }],
|
|
31
|
+
["claude-sonnet-4-6", { price: p(3, 15, 0.3, 3.75), contextWindow: M, thinking: OPTIONAL_46 }],
|
|
32
|
+
["claude-sonnet-4-5", { price: p(3, 15, 0.3, 3.75), contextWindow: 2e5 }],
|
|
33
|
+
["claude-haiku-4-5", { price: p(1, 5, 0.1, 1.25), contextWindow: 2e5 }]
|
|
34
|
+
];
|
|
35
|
+
function p(input, output, cacheRead, cacheWrite) {
|
|
36
|
+
return { input, output, cacheRead, cacheWrite };
|
|
37
|
+
}
|
|
38
|
+
function aliasModel(alias) {
|
|
39
|
+
if (!/^(haiku|sonnet|opus|fable)$/.test(alias)) return void 0;
|
|
40
|
+
return KNOWN.find(([prefix]) => prefix.startsWith(`claude-${alias}-`))?.[0];
|
|
41
|
+
}
|
|
42
|
+
function knownModels() {
|
|
43
|
+
return KNOWN.map(([id, info]) => ({ id, info }));
|
|
44
|
+
}
|
|
45
|
+
function lookupModel(modelId) {
|
|
46
|
+
let best;
|
|
47
|
+
for (const [prefix, info] of KNOWN) {
|
|
48
|
+
if (modelId.startsWith(prefix) && prefix.length > (best?.prefix.length ?? 0)) {
|
|
49
|
+
best = { prefix, info };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return best?.info ?? { contextWindow: DEFAULT_CONTEXT_WINDOW };
|
|
53
|
+
}
|
|
54
|
+
var WEB_SEARCH_USD = 0.01;
|
|
55
|
+
function costOf(usage, price, factor = 1) {
|
|
56
|
+
return (usage.inputTokens * price.input + usage.outputTokens * price.output + usage.cacheReadTokens * price.cacheRead + usage.cacheWriteTokens * price.cacheWrite) / M * factor + (usage.webSearches ?? 0) * WEB_SEARCH_USD;
|
|
57
|
+
}
|
|
58
|
+
function responseCost(response, price) {
|
|
59
|
+
return costOf(response.usage, price, response.priceFactor ?? 1);
|
|
60
|
+
}
|
|
61
|
+
function totalTokens(usage) {
|
|
62
|
+
return usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
63
|
+
}
|
|
64
|
+
function contextSize(usage) {
|
|
65
|
+
return totalTokens(usage);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/permissions/projectSettings.ts
|
|
69
|
+
import { createHash } from "crypto";
|
|
70
|
+
function settingsRisk(settings) {
|
|
71
|
+
const risk = {
|
|
72
|
+
executorHost: settings.executor === "host",
|
|
73
|
+
allow: settings.allow.map(formatRule),
|
|
74
|
+
writePaths: [...settings.sandbox?.writePaths ?? []],
|
|
75
|
+
envAllow: [...settings.envAllow],
|
|
76
|
+
allowLocalhost: settings.web?.allowLocalhost === true
|
|
77
|
+
};
|
|
78
|
+
const formatterCommands = Object.entries(settings.formatters?.commands ?? {}).flatMap(
|
|
79
|
+
([name, f]) => f === false ? [] : [`${name}: ${f.command.join(" ")}`]
|
|
80
|
+
);
|
|
81
|
+
if (formatterCommands.length > 0) risk.formatterCommands = formatterCommands;
|
|
82
|
+
const none = !risk.executorHost && risk.allow.length === 0 && risk.writePaths.length === 0 && risk.envAllow.length === 0 && !risk.allowLocalhost && risk.formatterCommands === void 0;
|
|
83
|
+
return none ? void 0 : risk;
|
|
84
|
+
}
|
|
85
|
+
function settingsRiskHash(risk) {
|
|
86
|
+
return createHash("sha256").update(JSON.stringify(risk)).digest("hex");
|
|
87
|
+
}
|
|
88
|
+
function settingsRiskLines(risk) {
|
|
89
|
+
const lines = [];
|
|
90
|
+
if (risk.executorHost) lines.push('executor: "host" (commands run with no OS sandbox)');
|
|
91
|
+
if (risk.allow.length > 0) {
|
|
92
|
+
lines.push(`permissions.allow (no question for these): ${risk.allow.join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
if (risk.writePaths.length > 0) {
|
|
95
|
+
lines.push(`sandbox.writePaths (commands may write there): ${risk.writePaths.join(", ")}`);
|
|
96
|
+
}
|
|
97
|
+
if (risk.envAllow.length > 0) {
|
|
98
|
+
lines.push(`env.allow (commands see these variables): ${risk.envAllow.join(", ")}`);
|
|
99
|
+
}
|
|
100
|
+
if (risk.allowLocalhost) lines.push("web.allowLocalhost (web_fetch may reach this machine)");
|
|
101
|
+
if (risk.formatterCommands !== void 0) {
|
|
102
|
+
lines.push(
|
|
103
|
+
`formatters.commands (run after each edit): ${risk.formatterCommands.map(cleanLine).join("; ")}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return lines;
|
|
107
|
+
}
|
|
108
|
+
function withoutRisk(settings) {
|
|
109
|
+
const { sandbox, web, formatters, ...rest } = settings;
|
|
110
|
+
const { writePaths: _writePaths, ...sandboxRest } = sandbox ?? {};
|
|
111
|
+
const keptCommands = Object.fromEntries(
|
|
112
|
+
Object.entries(formatters?.commands ?? {}).filter(([, f]) => f === false)
|
|
113
|
+
);
|
|
114
|
+
return {
|
|
115
|
+
...rest,
|
|
116
|
+
...formatters === void 0 ? {} : { formatters: { ...formatters, commands: keptCommands } },
|
|
117
|
+
executor: settings.executor === "host" ? "auto" : settings.executor,
|
|
118
|
+
allow: [],
|
|
119
|
+
envAllow: [],
|
|
120
|
+
...sandbox === void 0 ? {} : { sandbox: sandboxRest },
|
|
121
|
+
...web === void 0 ? {} : { web: { ...web, allowLocalhost: false } }
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function gateProjectSettings(options) {
|
|
125
|
+
const risk = settingsRisk(options.settings);
|
|
126
|
+
if (risk === void 0) return { settings: options.settings };
|
|
127
|
+
const hash = settingsRiskHash(risk);
|
|
128
|
+
const known = options.trust?.settingsHash(options.root);
|
|
129
|
+
if (known === hash) return { settings: options.settings };
|
|
130
|
+
const lines = settingsRiskLines(risk);
|
|
131
|
+
if (options.ask !== void 0) {
|
|
132
|
+
const choice = await options.ask({
|
|
133
|
+
tool: "settings",
|
|
134
|
+
target: { kind: "input", json: "" },
|
|
135
|
+
title: "Project settings that loosen safety",
|
|
136
|
+
preview: [
|
|
137
|
+
`This project's .garuda/settings.json ${known === void 0 ? "asks for" : "changed and now asks for"}:`,
|
|
138
|
+
...lines.map((line) => ` - ${line}`),
|
|
139
|
+
"A cloned repository can ship this file. Without your yes, Garuda uses safe defaults for these",
|
|
140
|
+
"parts; the rest of the file applies."
|
|
141
|
+
].join("\n"),
|
|
142
|
+
isolation: options.isolation ?? "os",
|
|
143
|
+
question: "Use these project settings?",
|
|
144
|
+
choices: ["once", "session", "deny"],
|
|
145
|
+
labels: {
|
|
146
|
+
once: "Yes, for this run",
|
|
147
|
+
session: "Yes, and remember (asks again when they change)",
|
|
148
|
+
deny: "No, use safe defaults"
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
if (choice === "session") await options.trust?.setSettingsHash(options.root, hash);
|
|
152
|
+
if (choice !== "deny") return { settings: options.settings };
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
settings: withoutRisk(options.settings),
|
|
156
|
+
notice: [
|
|
157
|
+
`Project settings not applied (not approved): ${lines.join("; ")}.`,
|
|
158
|
+
"Start the chat in a terminal and answer yes to use them."
|
|
159
|
+
].join(" ")
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
aliasModel,
|
|
165
|
+
knownModels,
|
|
166
|
+
lookupModel,
|
|
167
|
+
WEB_SEARCH_USD,
|
|
168
|
+
responseCost,
|
|
169
|
+
totalTokens,
|
|
170
|
+
contextSize,
|
|
171
|
+
gateProjectSettings
|
|
172
|
+
};
|
|
173
|
+
//# sourceMappingURL=chunk-W7BPYQMW.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/permissions/pathGuard.ts
|
|
4
|
+
import { realpath } from "fs/promises";
|
|
5
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
6
|
+
var PathOutsideRootError = class extends Error {
|
|
7
|
+
constructor(input) {
|
|
8
|
+
super(`Path "${input}" is outside the working root.`);
|
|
9
|
+
this.name = "PathOutsideRootError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function isInside(parent, child) {
|
|
13
|
+
const rel = relative(parent, child);
|
|
14
|
+
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
|
|
15
|
+
}
|
|
16
|
+
async function resolveInRoot(root, input) {
|
|
17
|
+
const absolute = resolve(root, input === "" ? "." : input);
|
|
18
|
+
if (!isInside(root, absolute)) throw new PathOutsideRootError(input);
|
|
19
|
+
const realRoot = await realpath(root);
|
|
20
|
+
const realTarget = await realpathOfNearestExisting(absolute);
|
|
21
|
+
if (!isInside(realRoot, realTarget)) throw new PathOutsideRootError(input);
|
|
22
|
+
return absolute;
|
|
23
|
+
}
|
|
24
|
+
async function realRelative(root, path) {
|
|
25
|
+
try {
|
|
26
|
+
const realRoot = await realpath(root);
|
|
27
|
+
const real = await realpathOfNearestExisting(resolve(root, path));
|
|
28
|
+
if (!isInside(realRoot, real)) return void 0;
|
|
29
|
+
const shown = displayPath(realRoot, real);
|
|
30
|
+
return shown === path ? void 0 : shown;
|
|
31
|
+
} catch {
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function realpathOfNearestExisting(path) {
|
|
36
|
+
let current = path;
|
|
37
|
+
for (; ; ) {
|
|
38
|
+
try {
|
|
39
|
+
const real = await realpath(current);
|
|
40
|
+
return resolve(real, relative(current, path));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code !== "ENOENT") throw error;
|
|
43
|
+
const parent = dirname(current);
|
|
44
|
+
if (parent === current) return path;
|
|
45
|
+
current = parent;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function displayPath(root, absolute) {
|
|
50
|
+
const rel = relative(root, absolute);
|
|
51
|
+
return rel === "" ? "." : rel.split(sep).join("/");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
PathOutsideRootError,
|
|
56
|
+
isInside,
|
|
57
|
+
resolveInRoot,
|
|
58
|
+
realRelative,
|
|
59
|
+
displayPath
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=chunk-WMOLXTBU.js.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
JobGitError,
|
|
4
|
+
git
|
|
5
|
+
} from "./chunk-ZGZ2ZADL.js";
|
|
6
|
+
|
|
7
|
+
// src/jobs/worktree.ts
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import { existsSync, lstatSync, realpathSync } from "fs";
|
|
10
|
+
import { mkdir, symlink } from "fs/promises";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { basename, join } from "path";
|
|
13
|
+
var LINK_CANDIDATES = ["node_modules", ".venv", "venv"];
|
|
14
|
+
var NEVER_COMMIT = [
|
|
15
|
+
".garuda/sessions",
|
|
16
|
+
".garuda/index",
|
|
17
|
+
".garuda/evals",
|
|
18
|
+
".garuda/jobs",
|
|
19
|
+
".garuda/audit.jsonl"
|
|
20
|
+
];
|
|
21
|
+
function worktreeDir(root, id, home = homedir()) {
|
|
22
|
+
const hash = createHash("sha256").update(root).digest("hex").slice(0, 8);
|
|
23
|
+
return join(home, ".garuda", "worktrees", `${basename(root)}-${hash}`, id);
|
|
24
|
+
}
|
|
25
|
+
async function jobBase(executor, root) {
|
|
26
|
+
const head = await git(executor, root, ["rev-parse", "--verify", "HEAD"], { check: false });
|
|
27
|
+
if (head.exitCode !== 0) {
|
|
28
|
+
throw new JobGitError("A job needs a git repository with at least one commit.");
|
|
29
|
+
}
|
|
30
|
+
const commit = head.stdout.trim();
|
|
31
|
+
const stash = (await git(executor, root, ["stash", "create"])).stdout.trim();
|
|
32
|
+
const untracked = (await git(executor, root, ["ls-files", "--others", "--exclude-standard"])).stdout.split("\n").filter((l) => l !== "" && !l.startsWith(".garuda/")).length;
|
|
33
|
+
if (stash === "") return { commit, uncommitted: false, untracked };
|
|
34
|
+
const tree = `${stash}^{tree}`;
|
|
35
|
+
const base = await git(executor, root, [
|
|
36
|
+
"commit-tree",
|
|
37
|
+
tree,
|
|
38
|
+
"-p",
|
|
39
|
+
commit,
|
|
40
|
+
"-m",
|
|
41
|
+
"Garuda job base: uncommitted changes at scheduling time"
|
|
42
|
+
]);
|
|
43
|
+
return { commit: base.stdout.trim(), uncommitted: true, untracked };
|
|
44
|
+
}
|
|
45
|
+
function cleanArgs(links = LINK_CANDIDATES) {
|
|
46
|
+
return ["clean", "-fdq", ...links.flatMap((name) => ["-e", `/${name}`])];
|
|
47
|
+
}
|
|
48
|
+
function linkCandidates(root) {
|
|
49
|
+
return LINK_CANDIDATES.filter((name) => {
|
|
50
|
+
try {
|
|
51
|
+
return lstatSync(join(root, name)).isDirectory();
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function prepareWorktree(executor, job, signal) {
|
|
58
|
+
if (!existsSync(join(job.worktree, ".git"))) {
|
|
59
|
+
await mkdir(join(job.worktree, ".."), { recursive: true, mode: 448 });
|
|
60
|
+
const branch = await git(
|
|
61
|
+
executor,
|
|
62
|
+
job.root,
|
|
63
|
+
["rev-parse", "--verify", `refs/heads/${job.branch}`],
|
|
64
|
+
{ check: false, ...signal === void 0 ? {} : { signal } }
|
|
65
|
+
);
|
|
66
|
+
const args = branch.exitCode === 0 ? ["worktree", "add", job.worktree, job.branch] : ["worktree", "add", "-b", job.branch, job.worktree, job.base];
|
|
67
|
+
await git(executor, job.root, args, signal === void 0 ? {} : { signal });
|
|
68
|
+
}
|
|
69
|
+
const links = [];
|
|
70
|
+
for (const name of job.links) {
|
|
71
|
+
const target = join(job.root, name);
|
|
72
|
+
const link = join(job.worktree, name);
|
|
73
|
+
if (!existsSync(target)) continue;
|
|
74
|
+
if (!existsSync(link)) await symlink(target, link, "dir");
|
|
75
|
+
links.push(realpathSync(target));
|
|
76
|
+
}
|
|
77
|
+
return { links };
|
|
78
|
+
}
|
|
79
|
+
async function commitJob(executor, job, message) {
|
|
80
|
+
const exclude = [...NEVER_COMMIT, ...job.links].map((p) => `:(exclude)${p}`);
|
|
81
|
+
await git(executor, job.worktree, ["add", "-A", "--", ".", ...exclude]);
|
|
82
|
+
const staged = await git(executor, job.worktree, ["diff", "--cached", "--quiet"], {
|
|
83
|
+
check: false
|
|
84
|
+
});
|
|
85
|
+
if (staged.exitCode === 0) return void 0;
|
|
86
|
+
await git(executor, job.worktree, ["commit", "--no-verify", "-q", "-m", message]);
|
|
87
|
+
return (await git(executor, job.worktree, ["rev-parse", "HEAD"])).stdout.trim();
|
|
88
|
+
}
|
|
89
|
+
async function jobChanges(executor, job) {
|
|
90
|
+
const range = [job.base, job.branch];
|
|
91
|
+
const names = await git(executor, job.root, [
|
|
92
|
+
"diff",
|
|
93
|
+
"--no-renames",
|
|
94
|
+
"--name-status",
|
|
95
|
+
"-z",
|
|
96
|
+
...range
|
|
97
|
+
]);
|
|
98
|
+
const counts = await git(executor, job.root, [
|
|
99
|
+
"diff",
|
|
100
|
+
"--no-renames",
|
|
101
|
+
"--numstat",
|
|
102
|
+
"-z",
|
|
103
|
+
...range
|
|
104
|
+
]);
|
|
105
|
+
return parseChanges(names, counts);
|
|
106
|
+
}
|
|
107
|
+
function parseChanges(names, counts) {
|
|
108
|
+
const lines = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const entry of counts.stdout.split("\0")) {
|
|
110
|
+
const m = /^(\d+|-)\t(\d+|-)\t(.*)$/s.exec(entry);
|
|
111
|
+
if (m === null) continue;
|
|
112
|
+
lines.set(m[3], m[1] === "-" ? {} : { added: Number(m[1]), removed: Number(m[2]) });
|
|
113
|
+
}
|
|
114
|
+
const parts = names.stdout.split("\0").filter((p) => p !== "");
|
|
115
|
+
const files = [];
|
|
116
|
+
for (let i = 0; i + 1 < parts.length; i += 2) {
|
|
117
|
+
const code = parts[i];
|
|
118
|
+
const path = parts[i + 1];
|
|
119
|
+
files.push({
|
|
120
|
+
status: code === "A" ? "added" : code === "D" ? "deleted" : "modified",
|
|
121
|
+
path,
|
|
122
|
+
...lines.get(path)
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return files;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export {
|
|
129
|
+
LINK_CANDIDATES,
|
|
130
|
+
worktreeDir,
|
|
131
|
+
jobBase,
|
|
132
|
+
cleanArgs,
|
|
133
|
+
linkCandidates,
|
|
134
|
+
prepareWorktree,
|
|
135
|
+
commitJob,
|
|
136
|
+
jobChanges
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=chunk-YIZWYUBG.js.map
|