gitclaw 0.3.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/README.md +440 -0
- package/dist/agents.d.ts +8 -0
- package/dist/agents.js +82 -0
- package/dist/audit.d.ts +27 -0
- package/dist/audit.js +55 -0
- package/dist/compliance.d.ts +30 -0
- package/dist/compliance.js +108 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +43 -0
- package/dist/examples.d.ts +6 -0
- package/dist/examples.js +40 -0
- package/dist/exports.d.ts +13 -0
- package/dist/exports.js +6 -0
- package/dist/hooks.d.ts +24 -0
- package/dist/hooks.js +108 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +542 -0
- package/dist/knowledge.d.ts +17 -0
- package/dist/knowledge.js +55 -0
- package/dist/loader.d.ts +64 -0
- package/dist/loader.js +222 -0
- package/dist/sandbox.d.ts +28 -0
- package/dist/sandbox.js +54 -0
- package/dist/sdk-hooks.d.ts +8 -0
- package/dist/sdk-hooks.js +31 -0
- package/dist/sdk-types.d.ts +127 -0
- package/dist/sdk-types.js +1 -0
- package/dist/sdk.d.ts +6 -0
- package/dist/sdk.js +444 -0
- package/dist/session.d.ts +15 -0
- package/dist/session.js +127 -0
- package/dist/skills.d.ts +18 -0
- package/dist/skills.js +104 -0
- package/dist/tool-loader.d.ts +3 -0
- package/dist/tool-loader.js +138 -0
- package/dist/tools/cli.d.ts +3 -0
- package/dist/tools/cli.js +86 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +29 -0
- package/dist/tools/memory.d.ts +3 -0
- package/dist/tools/memory.js +128 -0
- package/dist/tools/read.d.ts +3 -0
- package/dist/tools/read.js +46 -0
- package/dist/tools/sandbox-cli.d.ts +4 -0
- package/dist/tools/sandbox-cli.js +48 -0
- package/dist/tools/sandbox-memory.d.ts +4 -0
- package/dist/tools/sandbox-memory.js +117 -0
- package/dist/tools/sandbox-read.d.ts +4 -0
- package/dist/tools/sandbox-read.js +25 -0
- package/dist/tools/sandbox-write.d.ts +4 -0
- package/dist/tools/sandbox-write.js +26 -0
- package/dist/tools/shared.d.ts +38 -0
- package/dist/tools/shared.js +69 -0
- package/dist/tools/write.d.ts +3 -0
- package/dist/tools/write.js +28 -0
- package/dist/workflows.d.ts +8 -0
- package/dist/workflows.js +81 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from "readline";
|
|
3
|
+
import { Agent } from "@mariozechner/pi-agent-core";
|
|
4
|
+
import { loadAgent } from "./loader.js";
|
|
5
|
+
import { createBuiltinTools } from "./tools/index.js";
|
|
6
|
+
import { createSandboxContext } from "./sandbox.js";
|
|
7
|
+
import { expandSkillCommand } from "./skills.js";
|
|
8
|
+
import { loadHooksConfig, runHooks, wrapToolWithHooks } from "./hooks.js";
|
|
9
|
+
import { loadDeclarativeTools } from "./tool-loader.js";
|
|
10
|
+
import { AuditLogger, isAuditEnabled } from "./audit.js";
|
|
11
|
+
import { formatComplianceWarnings } from "./compliance.js";
|
|
12
|
+
import { readFile, mkdir, writeFile, access } from "fs/promises";
|
|
13
|
+
import { join, resolve } from "path";
|
|
14
|
+
import { execSync } from "child_process";
|
|
15
|
+
import { initLocalSession } from "./session.js";
|
|
16
|
+
// ANSI helpers
|
|
17
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
18
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
19
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
20
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const args = argv.slice(2);
|
|
23
|
+
let model;
|
|
24
|
+
let dir = process.cwd();
|
|
25
|
+
let prompt;
|
|
26
|
+
let env;
|
|
27
|
+
let sandbox = false;
|
|
28
|
+
let sandboxRepo;
|
|
29
|
+
let sandboxToken;
|
|
30
|
+
let repo;
|
|
31
|
+
let pat;
|
|
32
|
+
let session;
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
switch (args[i]) {
|
|
35
|
+
case "--model":
|
|
36
|
+
case "-m":
|
|
37
|
+
model = args[++i];
|
|
38
|
+
break;
|
|
39
|
+
case "--dir":
|
|
40
|
+
case "-d":
|
|
41
|
+
dir = args[++i];
|
|
42
|
+
break;
|
|
43
|
+
case "--prompt":
|
|
44
|
+
case "-p":
|
|
45
|
+
prompt = args[++i];
|
|
46
|
+
break;
|
|
47
|
+
case "--env":
|
|
48
|
+
case "-e":
|
|
49
|
+
env = args[++i];
|
|
50
|
+
break;
|
|
51
|
+
case "--sandbox":
|
|
52
|
+
case "-s":
|
|
53
|
+
sandbox = true;
|
|
54
|
+
break;
|
|
55
|
+
case "--sandbox-repo":
|
|
56
|
+
sandboxRepo = args[++i];
|
|
57
|
+
break;
|
|
58
|
+
case "--sandbox-token":
|
|
59
|
+
sandboxToken = args[++i];
|
|
60
|
+
break;
|
|
61
|
+
case "--repo":
|
|
62
|
+
case "-r":
|
|
63
|
+
repo = args[++i];
|
|
64
|
+
break;
|
|
65
|
+
case "--pat":
|
|
66
|
+
pat = args[++i];
|
|
67
|
+
break;
|
|
68
|
+
case "--session":
|
|
69
|
+
session = args[++i];
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
if (!args[i].startsWith("-")) {
|
|
73
|
+
prompt = args[i];
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { model, dir, prompt, env, sandbox, sandboxRepo, sandboxToken, repo, pat, session };
|
|
79
|
+
}
|
|
80
|
+
function handleEvent(event, hooksConfig, agentDir, sessionId, auditLogger) {
|
|
81
|
+
switch (event.type) {
|
|
82
|
+
case "message_update": {
|
|
83
|
+
const e = event.assistantMessageEvent;
|
|
84
|
+
if (e.type === "text_delta") {
|
|
85
|
+
process.stdout.write(e.delta);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "message_end": {
|
|
90
|
+
process.stdout.write("\n");
|
|
91
|
+
// Fire post_response hooks (non-blocking)
|
|
92
|
+
if (hooksConfig?.hooks.post_response) {
|
|
93
|
+
runHooks(hooksConfig.hooks.post_response, agentDir, {
|
|
94
|
+
event: "post_response",
|
|
95
|
+
session_id: sessionId,
|
|
96
|
+
}).catch(() => { });
|
|
97
|
+
}
|
|
98
|
+
auditLogger?.logResponse().catch(() => { });
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case "tool_execution_start":
|
|
102
|
+
process.stdout.write(dim(`\n▶ ${event.toolName}(${summarizeArgs(event.args)})\n`));
|
|
103
|
+
auditLogger?.logToolUse(event.toolName, event.args || {}).catch(() => { });
|
|
104
|
+
break;
|
|
105
|
+
case "tool_execution_end": {
|
|
106
|
+
if (event.isError) {
|
|
107
|
+
process.stdout.write(red(`✗ ${event.toolName} failed\n`));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const result = event.result;
|
|
111
|
+
const text = result?.content?.[0]?.text || "";
|
|
112
|
+
const preview = text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
113
|
+
if (preview) {
|
|
114
|
+
process.stdout.write(dim(preview) + "\n");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case "agent_end":
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function summarizeArgs(args) {
|
|
124
|
+
if (!args)
|
|
125
|
+
return "";
|
|
126
|
+
const entries = Object.entries(args);
|
|
127
|
+
if (entries.length === 0)
|
|
128
|
+
return "";
|
|
129
|
+
return entries
|
|
130
|
+
.map(([k, v]) => {
|
|
131
|
+
const str = typeof v === "string" ? v : JSON.stringify(v);
|
|
132
|
+
const short = str.length > 60 ? str.slice(0, 60) + "…" : str;
|
|
133
|
+
return `${k}: ${short}`;
|
|
134
|
+
})
|
|
135
|
+
.join(", ");
|
|
136
|
+
}
|
|
137
|
+
function askQuestion(question) {
|
|
138
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
139
|
+
return new Promise((res) => {
|
|
140
|
+
rl.question(question, (answer) => {
|
|
141
|
+
rl.close();
|
|
142
|
+
res(answer.trim());
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function isGitRepo(dir) {
|
|
147
|
+
try {
|
|
148
|
+
execSync("git rev-parse --is-inside-work-tree", { cwd: dir, stdio: "pipe" });
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function fileExists(path) {
|
|
156
|
+
try {
|
|
157
|
+
await access(path);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function ensureRepo(dir, model) {
|
|
165
|
+
const absDir = resolve(dir);
|
|
166
|
+
// Create directory if it doesn't exist
|
|
167
|
+
if (!(await fileExists(absDir))) {
|
|
168
|
+
console.log(dim(`Creating directory: ${absDir}`));
|
|
169
|
+
await mkdir(absDir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
// Git init if not a repo
|
|
172
|
+
if (!isGitRepo(absDir)) {
|
|
173
|
+
console.log(dim("Initializing git repository..."));
|
|
174
|
+
execSync("git init", { cwd: absDir, stdio: "pipe" });
|
|
175
|
+
// Create .gitignore
|
|
176
|
+
const gitignorePath = join(absDir, ".gitignore");
|
|
177
|
+
if (!(await fileExists(gitignorePath))) {
|
|
178
|
+
await writeFile(gitignorePath, "node_modules/\ndist/\n.gitagent/\n", "utf-8");
|
|
179
|
+
}
|
|
180
|
+
// Initial commit so memory saves work
|
|
181
|
+
execSync("git add -A && git commit -m 'Initial commit' --allow-empty", {
|
|
182
|
+
cwd: absDir,
|
|
183
|
+
stdio: "pipe",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Scaffold agent.yaml if missing
|
|
187
|
+
const agentYamlPath = join(absDir, "agent.yaml");
|
|
188
|
+
if (!(await fileExists(agentYamlPath))) {
|
|
189
|
+
const defaultModel = model || "openai:gpt-4o-mini";
|
|
190
|
+
const agentName = absDir.split("/").pop() || "my-agent";
|
|
191
|
+
const yaml = [
|
|
192
|
+
'spec_version: "0.1.0"',
|
|
193
|
+
`name: ${agentName}`,
|
|
194
|
+
"version: 0.1.0",
|
|
195
|
+
`description: Gitclaw agent for ${agentName}`,
|
|
196
|
+
"model:",
|
|
197
|
+
` preferred: "${defaultModel}"`,
|
|
198
|
+
" fallback: []",
|
|
199
|
+
"tools: [cli, read, write, memory]",
|
|
200
|
+
"runtime:",
|
|
201
|
+
" max_turns: 50",
|
|
202
|
+
"",
|
|
203
|
+
].join("\n");
|
|
204
|
+
await writeFile(agentYamlPath, yaml, "utf-8");
|
|
205
|
+
console.log(dim(`Created agent.yaml (model: ${defaultModel})`));
|
|
206
|
+
}
|
|
207
|
+
// Scaffold memory if missing
|
|
208
|
+
const memoryDir = join(absDir, "memory");
|
|
209
|
+
const memoryFile = join(memoryDir, "MEMORY.md");
|
|
210
|
+
if (!(await fileExists(memoryFile))) {
|
|
211
|
+
await mkdir(memoryDir, { recursive: true });
|
|
212
|
+
await writeFile(memoryFile, "# Memory\n", "utf-8");
|
|
213
|
+
}
|
|
214
|
+
// Scaffold SOUL.md if missing
|
|
215
|
+
const soulPath = join(absDir, "SOUL.md");
|
|
216
|
+
if (!(await fileExists(soulPath))) {
|
|
217
|
+
await writeFile(soulPath, [
|
|
218
|
+
"# Identity",
|
|
219
|
+
"",
|
|
220
|
+
"You are a helpful AI agent. You live inside a git repository.",
|
|
221
|
+
"You can run commands, read and write files, and remember things.",
|
|
222
|
+
"Be concise and action-oriented.",
|
|
223
|
+
"",
|
|
224
|
+
].join("\n"), "utf-8");
|
|
225
|
+
}
|
|
226
|
+
// Stage new scaffolded files
|
|
227
|
+
try {
|
|
228
|
+
execSync("git add -A && git diff --cached --quiet || git commit -m 'Scaffold gitclaw agent'", {
|
|
229
|
+
cwd: absDir,
|
|
230
|
+
stdio: "pipe",
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// ok if nothing to commit
|
|
235
|
+
}
|
|
236
|
+
return absDir;
|
|
237
|
+
}
|
|
238
|
+
async function main() {
|
|
239
|
+
const { model, dir: rawDir, prompt, env, sandbox: useSandbox, sandboxRepo, sandboxToken, repo, pat, session: sessionBranch } = parseArgs(process.argv);
|
|
240
|
+
// If --repo is given, derive a default dir from the repo URL (skip interactive prompt)
|
|
241
|
+
let dir = rawDir;
|
|
242
|
+
let localSession;
|
|
243
|
+
if (repo) {
|
|
244
|
+
// Validate mutually exclusive flags
|
|
245
|
+
if (useSandbox) {
|
|
246
|
+
console.error(red("Error: --repo and --sandbox are mutually exclusive"));
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
const token = pat || process.env.GITHUB_TOKEN || process.env.GIT_TOKEN;
|
|
250
|
+
if (!token) {
|
|
251
|
+
console.error(red("Error: --pat, GITHUB_TOKEN, or GIT_TOKEN is required with --repo"));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
// Default dir: /tmp/gitclaw/<repo-name> if no --dir given
|
|
255
|
+
if (dir === process.cwd()) {
|
|
256
|
+
const repoName = repo.split("/").pop()?.replace(/\.git$/, "") || "repo";
|
|
257
|
+
dir = resolve(`/tmp/gitclaw/${repoName}`);
|
|
258
|
+
}
|
|
259
|
+
localSession = initLocalSession({
|
|
260
|
+
url: repo,
|
|
261
|
+
token,
|
|
262
|
+
dir,
|
|
263
|
+
session: sessionBranch,
|
|
264
|
+
});
|
|
265
|
+
dir = localSession.dir;
|
|
266
|
+
console.log(dim(`Local session: ${localSession.branch} (${localSession.dir})`));
|
|
267
|
+
}
|
|
268
|
+
else if (dir === process.cwd() && !prompt) {
|
|
269
|
+
// No --repo: interactive prompt for dir
|
|
270
|
+
const answer = await askQuestion(green("? ") + bold("Repository path") + dim(" (. for current dir)") + green(": "));
|
|
271
|
+
if (answer) {
|
|
272
|
+
dir = resolve(answer === "." ? process.cwd() : answer);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Create sandbox context if --sandbox flag is set
|
|
276
|
+
let sandboxCtx;
|
|
277
|
+
if (useSandbox) {
|
|
278
|
+
const sandboxConfig = {
|
|
279
|
+
provider: "e2b",
|
|
280
|
+
repository: sandboxRepo,
|
|
281
|
+
token: sandboxToken,
|
|
282
|
+
};
|
|
283
|
+
sandboxCtx = await createSandboxContext(sandboxConfig, resolve(dir));
|
|
284
|
+
console.log(dim("Starting sandbox VM..."));
|
|
285
|
+
await sandboxCtx.gitMachine.start();
|
|
286
|
+
console.log(dim(`Sandbox ready (repo: ${sandboxCtx.repoPath})`));
|
|
287
|
+
}
|
|
288
|
+
// Ensure the target is a valid gitclaw repo (skip in sandbox/local-repo mode)
|
|
289
|
+
if (localSession) {
|
|
290
|
+
// Already cloned and scaffolded by initLocalSession
|
|
291
|
+
}
|
|
292
|
+
else if (!useSandbox) {
|
|
293
|
+
dir = await ensureRepo(dir, model);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
dir = resolve(dir);
|
|
297
|
+
}
|
|
298
|
+
let loaded;
|
|
299
|
+
try {
|
|
300
|
+
loaded = await loadAgent(dir, model, env);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
console.error(red(`Error: ${err.message}`));
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
const { systemPrompt, manifest, skills, sessionId, agentDir, gitagentDir, complianceWarnings } = loaded;
|
|
307
|
+
// Show compliance warnings
|
|
308
|
+
if (complianceWarnings.length > 0) {
|
|
309
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
310
|
+
console.log(yellow("Compliance warnings:"));
|
|
311
|
+
console.log(yellow(formatComplianceWarnings(complianceWarnings)));
|
|
312
|
+
}
|
|
313
|
+
// Initialize audit logger
|
|
314
|
+
const auditEnabled = isAuditEnabled(manifest.compliance);
|
|
315
|
+
const auditLogger = new AuditLogger(gitagentDir, sessionId, auditEnabled);
|
|
316
|
+
if (auditEnabled) {
|
|
317
|
+
await auditLogger.logSessionStart();
|
|
318
|
+
}
|
|
319
|
+
// Load hooks config
|
|
320
|
+
const hooksConfig = await loadHooksConfig(agentDir);
|
|
321
|
+
// Run on_session_start hooks
|
|
322
|
+
if (hooksConfig?.hooks.on_session_start) {
|
|
323
|
+
try {
|
|
324
|
+
const result = await runHooks(hooksConfig.hooks.on_session_start, agentDir, {
|
|
325
|
+
event: "on_session_start",
|
|
326
|
+
session_id: sessionId,
|
|
327
|
+
agent: manifest.name,
|
|
328
|
+
});
|
|
329
|
+
if (result.action === "block") {
|
|
330
|
+
console.error(red(`Session blocked by hook: ${result.reason || "no reason given"}`));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
console.error(red(`Hook error: ${err.message}`));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Map provider to expected env var
|
|
339
|
+
const apiKeyEnvVars = {
|
|
340
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
341
|
+
openai: "OPENAI_API_KEY",
|
|
342
|
+
google: "GOOGLE_API_KEY",
|
|
343
|
+
xai: "XAI_API_KEY",
|
|
344
|
+
groq: "GROQ_API_KEY",
|
|
345
|
+
mistral: "MISTRAL_API_KEY",
|
|
346
|
+
};
|
|
347
|
+
const provider = loaded.model.provider;
|
|
348
|
+
const envVar = apiKeyEnvVars[provider];
|
|
349
|
+
if (envVar && !process.env[envVar]) {
|
|
350
|
+
console.error(red(`Error: ${envVar} environment variable is not set.`));
|
|
351
|
+
console.error(dim(`Set it with: export ${envVar}=your-key-here`));
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
// Build tools — built-in + declarative
|
|
355
|
+
let tools = createBuiltinTools({
|
|
356
|
+
dir,
|
|
357
|
+
timeout: manifest.runtime.timeout,
|
|
358
|
+
sandbox: sandboxCtx,
|
|
359
|
+
});
|
|
360
|
+
// Load declarative tools from tools/*.yaml (Phase 2.2)
|
|
361
|
+
const declarativeTools = await loadDeclarativeTools(agentDir);
|
|
362
|
+
tools = [...tools, ...declarativeTools];
|
|
363
|
+
// Wrap with hooks if configured
|
|
364
|
+
if (hooksConfig) {
|
|
365
|
+
tools = tools.map((t) => wrapToolWithHooks(t, hooksConfig, agentDir, sessionId));
|
|
366
|
+
}
|
|
367
|
+
// Build model options from manifest constraints
|
|
368
|
+
const modelOptions = {};
|
|
369
|
+
if (manifest.model.constraints) {
|
|
370
|
+
const c = manifest.model.constraints;
|
|
371
|
+
if (c.temperature !== undefined)
|
|
372
|
+
modelOptions.temperature = c.temperature;
|
|
373
|
+
if (c.max_tokens !== undefined)
|
|
374
|
+
modelOptions.maxTokens = c.max_tokens;
|
|
375
|
+
if (c.top_p !== undefined)
|
|
376
|
+
modelOptions.topP = c.top_p;
|
|
377
|
+
if (c.top_k !== undefined)
|
|
378
|
+
modelOptions.topK = c.top_k;
|
|
379
|
+
if (c.stop_sequences !== undefined)
|
|
380
|
+
modelOptions.stopSequences = c.stop_sequences;
|
|
381
|
+
}
|
|
382
|
+
const agent = new Agent({
|
|
383
|
+
initialState: {
|
|
384
|
+
systemPrompt,
|
|
385
|
+
model: loaded.model,
|
|
386
|
+
tools,
|
|
387
|
+
...modelOptions,
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
agent.subscribe((event) => handleEvent(event, hooksConfig, agentDir, sessionId, auditLogger));
|
|
391
|
+
console.log(bold(`${manifest.name} v${manifest.version}`));
|
|
392
|
+
console.log(dim(`Model: ${loaded.model.provider}:${loaded.model.id}`));
|
|
393
|
+
const allToolNames = tools.map((t) => t.name);
|
|
394
|
+
console.log(dim(`Tools: ${allToolNames.join(", ")}`));
|
|
395
|
+
if (skills.length > 0) {
|
|
396
|
+
console.log(dim(`Skills: ${skills.map((s) => s.name).join(", ")}`));
|
|
397
|
+
}
|
|
398
|
+
if (loaded.workflows.length > 0) {
|
|
399
|
+
console.log(dim(`Workflows: ${loaded.workflows.map((w) => w.name).join(", ")}`));
|
|
400
|
+
}
|
|
401
|
+
if (loaded.subAgents.length > 0) {
|
|
402
|
+
console.log(dim(`Agents: ${loaded.subAgents.map((a) => a.name).join(", ")}`));
|
|
403
|
+
}
|
|
404
|
+
console.log(dim('Type /skills to list skills, /memory to view memory, /quit to exit\n'));
|
|
405
|
+
// Single-shot mode
|
|
406
|
+
if (prompt) {
|
|
407
|
+
try {
|
|
408
|
+
await agent.prompt(prompt);
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
auditLogger?.logError(err.message).catch(() => { });
|
|
412
|
+
// Fire on_error hooks
|
|
413
|
+
if (hooksConfig?.hooks.on_error) {
|
|
414
|
+
runHooks(hooksConfig.hooks.on_error, agentDir, {
|
|
415
|
+
event: "on_error",
|
|
416
|
+
session_id: sessionId,
|
|
417
|
+
error: err.message,
|
|
418
|
+
}).catch(() => { });
|
|
419
|
+
}
|
|
420
|
+
throw err;
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
if (localSession) {
|
|
424
|
+
console.log(dim("Finalizing session..."));
|
|
425
|
+
localSession.finalize();
|
|
426
|
+
}
|
|
427
|
+
if (sandboxCtx) {
|
|
428
|
+
console.log(dim("Stopping sandbox..."));
|
|
429
|
+
await sandboxCtx.gitMachine.stop();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// REPL mode
|
|
435
|
+
const rl = createInterface({
|
|
436
|
+
input: process.stdin,
|
|
437
|
+
output: process.stdout,
|
|
438
|
+
});
|
|
439
|
+
const ask = () => {
|
|
440
|
+
rl.question(green("→ "), async (input) => {
|
|
441
|
+
const trimmed = input.trim();
|
|
442
|
+
if (!trimmed) {
|
|
443
|
+
ask();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (trimmed === "/quit" || trimmed === "/exit") {
|
|
447
|
+
rl.close();
|
|
448
|
+
if (localSession) {
|
|
449
|
+
console.log(dim("Finalizing session..."));
|
|
450
|
+
localSession.finalize();
|
|
451
|
+
}
|
|
452
|
+
await stopSandbox();
|
|
453
|
+
process.exit(0);
|
|
454
|
+
}
|
|
455
|
+
if (trimmed === "/memory") {
|
|
456
|
+
try {
|
|
457
|
+
const mem = await readFile(join(dir, "memory/MEMORY.md"), "utf-8");
|
|
458
|
+
console.log(dim("--- memory ---"));
|
|
459
|
+
console.log(mem.trim() || "(empty)");
|
|
460
|
+
console.log(dim("--- end ---"));
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
console.log(dim("(no memory file)"));
|
|
464
|
+
}
|
|
465
|
+
ask();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (trimmed === "/skills") {
|
|
469
|
+
if (skills.length === 0) {
|
|
470
|
+
console.log(dim("No skills installed."));
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
for (const s of skills) {
|
|
474
|
+
console.log(` ${bold(s.name)} — ${dim(s.description)}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
ask();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// Skill expansion: /skill:name [args]
|
|
481
|
+
let promptText = trimmed;
|
|
482
|
+
if (trimmed.startsWith("/skill:")) {
|
|
483
|
+
const result = await expandSkillCommand(trimmed, skills);
|
|
484
|
+
if (result) {
|
|
485
|
+
console.log(dim(`▶ loading skill: ${result.skillName}`));
|
|
486
|
+
promptText = result.expanded;
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
const requested = trimmed.match(/^\/skill:([a-z0-9-]*)/)?.[1] || "?";
|
|
490
|
+
console.error(red(`Unknown skill: ${requested}`));
|
|
491
|
+
ask();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
await agent.prompt(promptText);
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
console.error(red(`Error: ${err.message}`));
|
|
500
|
+
auditLogger?.logError(err.message).catch(() => { });
|
|
501
|
+
// Fire on_error hooks
|
|
502
|
+
if (hooksConfig?.hooks.on_error) {
|
|
503
|
+
runHooks(hooksConfig.hooks.on_error, agentDir, {
|
|
504
|
+
event: "on_error",
|
|
505
|
+
session_id: sessionId,
|
|
506
|
+
error: err.message,
|
|
507
|
+
}).catch(() => { });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
ask();
|
|
511
|
+
});
|
|
512
|
+
};
|
|
513
|
+
// Sandbox cleanup helper
|
|
514
|
+
const stopSandbox = async () => {
|
|
515
|
+
if (sandboxCtx) {
|
|
516
|
+
console.log(dim("Stopping sandbox..."));
|
|
517
|
+
await sandboxCtx.gitMachine.stop();
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
// Handle Ctrl+C during streaming
|
|
521
|
+
rl.on("SIGINT", () => {
|
|
522
|
+
if (agent.state.isStreaming) {
|
|
523
|
+
agent.abort();
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
console.log("\nBye!");
|
|
527
|
+
rl.close();
|
|
528
|
+
if (localSession) {
|
|
529
|
+
try {
|
|
530
|
+
localSession.finalize();
|
|
531
|
+
}
|
|
532
|
+
catch { /* best-effort */ }
|
|
533
|
+
}
|
|
534
|
+
stopSandbox().finally(() => process.exit(0));
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
ask();
|
|
538
|
+
}
|
|
539
|
+
main().catch((err) => {
|
|
540
|
+
console.error(red(`Fatal: ${err.message}`));
|
|
541
|
+
process.exit(1);
|
|
542
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface KnowledgeEntry {
|
|
2
|
+
path: string;
|
|
3
|
+
tags: string[];
|
|
4
|
+
priority: "high" | "medium" | "low";
|
|
5
|
+
always_load?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface LoadedKnowledge {
|
|
8
|
+
/** Content from always_load docs, ready to inject into system prompt */
|
|
9
|
+
preloaded: Array<{
|
|
10
|
+
path: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}>;
|
|
13
|
+
/** Entries available on demand via read tool */
|
|
14
|
+
available: KnowledgeEntry[];
|
|
15
|
+
}
|
|
16
|
+
export declare function loadKnowledge(agentDir: string): Promise<LoadedKnowledge>;
|
|
17
|
+
export declare function formatKnowledgeForPrompt(knowledge: LoadedKnowledge): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
export async function loadKnowledge(agentDir) {
|
|
5
|
+
const knowledgeDir = join(agentDir, "knowledge");
|
|
6
|
+
const indexPath = join(knowledgeDir, "index.yaml");
|
|
7
|
+
let raw;
|
|
8
|
+
try {
|
|
9
|
+
raw = await readFile(indexPath, "utf-8");
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return { preloaded: [], available: [] };
|
|
13
|
+
}
|
|
14
|
+
const index = yaml.load(raw);
|
|
15
|
+
if (!index?.entries || !Array.isArray(index.entries)) {
|
|
16
|
+
return { preloaded: [], available: [] };
|
|
17
|
+
}
|
|
18
|
+
const preloaded = [];
|
|
19
|
+
const available = [];
|
|
20
|
+
for (const entry of index.entries) {
|
|
21
|
+
if (entry.always_load) {
|
|
22
|
+
try {
|
|
23
|
+
const content = await readFile(join(knowledgeDir, entry.path), "utf-8");
|
|
24
|
+
preloaded.push({ path: entry.path, content: content.trim() });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Skip missing files
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
available.push(entry);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { preloaded, available };
|
|
35
|
+
}
|
|
36
|
+
export function formatKnowledgeForPrompt(knowledge) {
|
|
37
|
+
const parts = [];
|
|
38
|
+
// Inject always_load content directly
|
|
39
|
+
for (const doc of knowledge.preloaded) {
|
|
40
|
+
parts.push(`<knowledge path="${doc.path}">\n${doc.content}\n</knowledge>`);
|
|
41
|
+
}
|
|
42
|
+
// List available docs for on-demand access
|
|
43
|
+
if (knowledge.available.length > 0) {
|
|
44
|
+
const entries = knowledge.available
|
|
45
|
+
.map((e) => {
|
|
46
|
+
const tags = e.tags.length > 0 ? ` tags="${e.tags.join(",")}"` : "";
|
|
47
|
+
return `<doc path="knowledge/${e.path}" priority="${e.priority}"${tags} />`;
|
|
48
|
+
})
|
|
49
|
+
.join("\n");
|
|
50
|
+
parts.push(`<available_knowledge>\n${entries}\n</available_knowledge>\n\nUse the \`read\` tool to load any available knowledge document when needed.`);
|
|
51
|
+
}
|
|
52
|
+
if (parts.length === 0)
|
|
53
|
+
return "";
|
|
54
|
+
return `# Knowledge\n\n${parts.join("\n\n")}`;
|
|
55
|
+
}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { SkillMetadata } from "./skills.js";
|
|
3
|
+
import type { LoadedKnowledge } from "./knowledge.js";
|
|
4
|
+
import type { WorkflowMetadata } from "./workflows.js";
|
|
5
|
+
import type { EnvConfig } from "./config.js";
|
|
6
|
+
import type { SubAgentMetadata } from "./agents.js";
|
|
7
|
+
import type { ExampleEntry } from "./examples.js";
|
|
8
|
+
import type { ComplianceWarning } from "./compliance.js";
|
|
9
|
+
export interface AgentManifest {
|
|
10
|
+
spec_version: string;
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
description: string;
|
|
14
|
+
author?: string;
|
|
15
|
+
license?: string;
|
|
16
|
+
tags?: string[];
|
|
17
|
+
metadata?: Record<string, string | number | boolean>;
|
|
18
|
+
model: {
|
|
19
|
+
preferred: string;
|
|
20
|
+
fallback: string[];
|
|
21
|
+
constraints?: {
|
|
22
|
+
temperature?: number;
|
|
23
|
+
max_tokens?: number;
|
|
24
|
+
top_p?: number;
|
|
25
|
+
top_k?: number;
|
|
26
|
+
stop_sequences?: string[];
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
tools: string[];
|
|
30
|
+
skills?: string[];
|
|
31
|
+
runtime: {
|
|
32
|
+
max_turns: number;
|
|
33
|
+
timeout?: number;
|
|
34
|
+
};
|
|
35
|
+
extends?: string;
|
|
36
|
+
dependencies?: Array<{
|
|
37
|
+
name: string;
|
|
38
|
+
source: string;
|
|
39
|
+
version: string;
|
|
40
|
+
mount: string;
|
|
41
|
+
}>;
|
|
42
|
+
agents?: Record<string, any>;
|
|
43
|
+
delegation?: {
|
|
44
|
+
mode: "auto" | "explicit" | "router";
|
|
45
|
+
router?: string;
|
|
46
|
+
};
|
|
47
|
+
compliance?: Record<string, any>;
|
|
48
|
+
}
|
|
49
|
+
export interface LoadedAgent {
|
|
50
|
+
systemPrompt: string;
|
|
51
|
+
manifest: AgentManifest;
|
|
52
|
+
model: Model<any>;
|
|
53
|
+
skills: SkillMetadata[];
|
|
54
|
+
knowledge: LoadedKnowledge;
|
|
55
|
+
workflows: WorkflowMetadata[];
|
|
56
|
+
subAgents: SubAgentMetadata[];
|
|
57
|
+
examples: ExampleEntry[];
|
|
58
|
+
envConfig: EnvConfig;
|
|
59
|
+
sessionId: string;
|
|
60
|
+
agentDir: string;
|
|
61
|
+
gitagentDir: string;
|
|
62
|
+
complianceWarnings: ComplianceWarning[];
|
|
63
|
+
}
|
|
64
|
+
export declare function loadAgent(agentDir: string, modelFlag?: string, envFlag?: string): Promise<LoadedAgent>;
|