gitclaw 0.3.1 → 0.4.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/LICENSE +21 -0
- package/README.md +6 -2
- package/dist/composio/adapter.d.ts +26 -0
- package/dist/composio/adapter.js +92 -0
- package/dist/composio/client.d.ts +39 -0
- package/dist/composio/client.js +170 -0
- package/dist/composio/index.d.ts +2 -0
- package/dist/composio/index.js +2 -0
- package/dist/context.d.ts +20 -0
- package/dist/context.js +211 -0
- package/dist/exports.d.ts +2 -0
- package/dist/exports.js +1 -0
- package/dist/index.js +99 -7
- package/dist/learning/reinforcement.d.ts +11 -0
- package/dist/learning/reinforcement.js +91 -0
- package/dist/loader.js +34 -1
- package/dist/sdk.js +5 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.js +58 -7
- package/dist/tools/capture-photo.d.ts +3 -0
- package/dist/tools/capture-photo.js +91 -0
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +12 -2
- package/dist/tools/read.js +4 -0
- package/dist/tools/shared.d.ts +20 -0
- package/dist/tools/shared.js +24 -0
- package/dist/tools/skill-learner.d.ts +3 -0
- package/dist/tools/skill-learner.js +358 -0
- package/dist/tools/task-tracker.d.ts +20 -0
- package/dist/tools/task-tracker.js +275 -0
- package/dist/tools/write.js +4 -0
- package/dist/voice/adapter.d.ts +97 -0
- package/dist/voice/adapter.js +30 -0
- package/dist/voice/chat-history.d.ts +8 -0
- package/dist/voice/chat-history.js +121 -0
- package/dist/voice/gemini-live.d.ts +20 -0
- package/dist/voice/gemini-live.js +279 -0
- package/dist/voice/index.d.ts +4 -0
- package/dist/voice/index.js +3 -0
- package/dist/voice/openai-realtime.d.ts +27 -0
- package/dist/voice/openai-realtime.js +291 -0
- package/dist/voice/server.d.ts +2 -0
- package/dist/voice/server.js +2319 -0
- package/dist/voice/ui.html +2556 -0
- package/package.json +21 -7
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Agent } from "@mariozechner/pi-agent-core";
|
|
|
4
4
|
import { loadAgent } from "./loader.js";
|
|
5
5
|
import { createBuiltinTools } from "./tools/index.js";
|
|
6
6
|
import { createSandboxContext } from "./sandbox.js";
|
|
7
|
-
import { expandSkillCommand } from "./skills.js";
|
|
7
|
+
import { expandSkillCommand, refreshSkills } from "./skills.js";
|
|
8
8
|
import { loadHooksConfig, runHooks, wrapToolWithHooks } from "./hooks.js";
|
|
9
9
|
import { loadDeclarativeTools } from "./tool-loader.js";
|
|
10
10
|
import { AuditLogger, isAuditEnabled } from "./audit.js";
|
|
@@ -13,6 +13,7 @@ import { readFile, mkdir, writeFile, access } from "fs/promises";
|
|
|
13
13
|
import { join, resolve } from "path";
|
|
14
14
|
import { execSync } from "child_process";
|
|
15
15
|
import { initLocalSession } from "./session.js";
|
|
16
|
+
import { startVoiceServer } from "./voice/server.js";
|
|
16
17
|
// ANSI helpers
|
|
17
18
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
18
19
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
@@ -30,6 +31,7 @@ function parseArgs(argv) {
|
|
|
30
31
|
let repo;
|
|
31
32
|
let pat;
|
|
32
33
|
let session;
|
|
34
|
+
let voice;
|
|
33
35
|
for (let i = 0; i < args.length; i++) {
|
|
34
36
|
switch (args[i]) {
|
|
35
37
|
case "--model":
|
|
@@ -68,6 +70,16 @@ function parseArgs(argv) {
|
|
|
68
70
|
case "--session":
|
|
69
71
|
session = args[++i];
|
|
70
72
|
break;
|
|
73
|
+
case "--voice":
|
|
74
|
+
case "-v":
|
|
75
|
+
// Accept optional backend name: --voice, --voice openai, --voice gemini
|
|
76
|
+
if (args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
77
|
+
voice = args[++i];
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
voice = "openai";
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
71
83
|
default:
|
|
72
84
|
if (!args[i].startsWith("-")) {
|
|
73
85
|
prompt = args[i];
|
|
@@ -75,7 +87,7 @@ function parseArgs(argv) {
|
|
|
75
87
|
break;
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
|
-
return { model, dir, prompt, env, sandbox, sandboxRepo, sandboxToken, repo, pat, session };
|
|
90
|
+
return { model, dir, prompt, env, sandbox, sandboxRepo, sandboxToken, repo, pat, session, voice };
|
|
79
91
|
}
|
|
80
92
|
function handleEvent(event, hooksConfig, agentDir, sessionId, auditLogger) {
|
|
81
93
|
switch (event.type) {
|
|
@@ -236,7 +248,7 @@ async function ensureRepo(dir, model) {
|
|
|
236
248
|
return absDir;
|
|
237
249
|
}
|
|
238
250
|
async function main() {
|
|
239
|
-
const { model, dir: rawDir, prompt, env, sandbox: useSandbox, sandboxRepo, sandboxToken, repo, pat, session: sessionBranch } = parseArgs(process.argv);
|
|
251
|
+
const { model, dir: rawDir, prompt, env, sandbox: useSandbox, sandboxRepo, sandboxToken, repo, pat, session: sessionBranch, voice } = parseArgs(process.argv);
|
|
240
252
|
// If --repo is given, derive a default dir from the repo URL (skip interactive prompt)
|
|
241
253
|
let dir = rawDir;
|
|
242
254
|
let localSession;
|
|
@@ -295,6 +307,46 @@ async function main() {
|
|
|
295
307
|
else {
|
|
296
308
|
dir = resolve(dir);
|
|
297
309
|
}
|
|
310
|
+
// Voice mode
|
|
311
|
+
if (voice) {
|
|
312
|
+
let adapterBackend;
|
|
313
|
+
let apiKey;
|
|
314
|
+
if (voice === "gemini") {
|
|
315
|
+
adapterBackend = "gemini-live";
|
|
316
|
+
apiKey = process.env.GEMINI_API_KEY;
|
|
317
|
+
if (!apiKey) {
|
|
318
|
+
console.error(red("Error: GEMINI_API_KEY is required for --voice gemini"));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
adapterBackend = "openai-realtime";
|
|
324
|
+
apiKey = process.env.OPENAI_API_KEY;
|
|
325
|
+
if (!apiKey) {
|
|
326
|
+
console.error(red("Error: OPENAI_API_KEY is required for --voice mode"));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const cleanup = await startVoiceServer({
|
|
331
|
+
adapter: adapterBackend,
|
|
332
|
+
adapterConfig: { apiKey },
|
|
333
|
+
agentDir: dir,
|
|
334
|
+
model,
|
|
335
|
+
env,
|
|
336
|
+
});
|
|
337
|
+
let stopping = false;
|
|
338
|
+
process.on("SIGINT", () => {
|
|
339
|
+
if (stopping) {
|
|
340
|
+
// Second Ctrl+C — force exit immediately
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
stopping = true;
|
|
344
|
+
console.log("\nDisconnecting...");
|
|
345
|
+
cleanup().finally(() => process.exit(0));
|
|
346
|
+
});
|
|
347
|
+
// Keep process alive
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
298
350
|
let loaded;
|
|
299
351
|
try {
|
|
300
352
|
loaded = await loadAgent(dir, model, env);
|
|
@@ -356,6 +408,7 @@ async function main() {
|
|
|
356
408
|
dir,
|
|
357
409
|
timeout: manifest.runtime.timeout,
|
|
358
410
|
sandbox: sandboxCtx,
|
|
411
|
+
gitagentDir,
|
|
359
412
|
});
|
|
360
413
|
// Load declarative tools from tools/*.yaml (Phase 2.2)
|
|
361
414
|
const declarativeTools = await loadDeclarativeTools(agentDir);
|
|
@@ -401,7 +454,7 @@ async function main() {
|
|
|
401
454
|
if (loaded.subAgents.length > 0) {
|
|
402
455
|
console.log(dim(`Agents: ${loaded.subAgents.map((a) => a.name).join(", ")}`));
|
|
403
456
|
}
|
|
404
|
-
console.log(dim('Type /skills
|
|
457
|
+
console.log(dim('Type /skills, /tasks, /learned, /memory, /quit\n'));
|
|
405
458
|
// Single-shot mode
|
|
406
459
|
if (prompt) {
|
|
407
460
|
try {
|
|
@@ -466,12 +519,51 @@ async function main() {
|
|
|
466
519
|
return;
|
|
467
520
|
}
|
|
468
521
|
if (trimmed === "/skills") {
|
|
469
|
-
|
|
522
|
+
// Refresh skills to pick up any newly learned ones
|
|
523
|
+
const currentSkills = await refreshSkills(dir);
|
|
524
|
+
if (currentSkills.length === 0) {
|
|
470
525
|
console.log(dim("No skills installed."));
|
|
471
526
|
}
|
|
472
527
|
else {
|
|
473
|
-
for (const s of
|
|
474
|
-
|
|
528
|
+
for (const s of currentSkills) {
|
|
529
|
+
const conf = s.confidence !== undefined ? dim(` [confidence: ${s.confidence}]`) : "";
|
|
530
|
+
console.log(` ${bold(s.name)} — ${dim(s.description)}${conf}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
ask();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (trimmed === "/tasks") {
|
|
537
|
+
try {
|
|
538
|
+
const tasksRaw = await readFile(join(gitagentDir, "learning", "tasks.json"), "utf-8");
|
|
539
|
+
const tasksData = JSON.parse(tasksRaw);
|
|
540
|
+
const active = (tasksData.tasks || []).filter((t) => t.status === "active");
|
|
541
|
+
if (active.length === 0) {
|
|
542
|
+
console.log(dim("No active tasks."));
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
for (const t of active) {
|
|
546
|
+
console.log(` ${bold(t.id.slice(0, 8))} — ${t.objective} (${t.steps.length} steps, attempt #${t.attempts})`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
console.log(dim("No tasks recorded yet."));
|
|
552
|
+
}
|
|
553
|
+
ask();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (trimmed === "/learned") {
|
|
557
|
+
const currentSkills = await refreshSkills(dir);
|
|
558
|
+
const learned = currentSkills.filter((s) => s.confidence !== undefined);
|
|
559
|
+
if (learned.length === 0) {
|
|
560
|
+
console.log(dim("No learned skills yet."));
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
for (const s of learned) {
|
|
564
|
+
const usage = s.usage_count ?? 0;
|
|
565
|
+
const ratio = `${s.success_count ?? 0}/${(s.success_count ?? 0) + (s.failure_count ?? 0)}`;
|
|
566
|
+
console.log(` ${bold(s.name)} — confidence: ${s.confidence}, usage: ${usage}, success: ${ratio}`);
|
|
475
567
|
}
|
|
476
568
|
}
|
|
477
569
|
ask();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface SkillStats {
|
|
2
|
+
confidence: number;
|
|
3
|
+
usage_count: number;
|
|
4
|
+
success_count: number;
|
|
5
|
+
failure_count: number;
|
|
6
|
+
negative_examples: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function adjustConfidence(current: SkillStats, outcome: "success" | "failure" | "partial", failureReason?: string): SkillStats;
|
|
9
|
+
export declare function loadSkillStats(skillDir: string): Promise<SkillStats>;
|
|
10
|
+
export declare function saveSkillStats(skillDir: string, stats: SkillStats): Promise<void>;
|
|
11
|
+
export declare function isSkillFlagged(stats: SkillStats): boolean;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
const MAX_NEGATIVE_EXAMPLES = 10;
|
|
5
|
+
const DEFAULT_STATS = {
|
|
6
|
+
confidence: 1.0,
|
|
7
|
+
usage_count: 0,
|
|
8
|
+
success_count: 0,
|
|
9
|
+
failure_count: 0,
|
|
10
|
+
negative_examples: [],
|
|
11
|
+
};
|
|
12
|
+
// ── Confidence math ─────────────────────────────────────────────────────
|
|
13
|
+
export function adjustConfidence(current, outcome, failureReason) {
|
|
14
|
+
const stats = { ...current };
|
|
15
|
+
stats.usage_count++;
|
|
16
|
+
switch (outcome) {
|
|
17
|
+
case "success":
|
|
18
|
+
// Asymptotic to 1.0: conf + 0.1 * (1 - conf)
|
|
19
|
+
stats.confidence = Math.min(1.0, stats.confidence + 0.1 * (1 - stats.confidence));
|
|
20
|
+
stats.success_count++;
|
|
21
|
+
break;
|
|
22
|
+
case "failure":
|
|
23
|
+
// 2x penalty (asymmetric loss)
|
|
24
|
+
stats.confidence = Math.max(0.0, stats.confidence - 0.2);
|
|
25
|
+
stats.failure_count++;
|
|
26
|
+
if (failureReason) {
|
|
27
|
+
stats.negative_examples = [
|
|
28
|
+
...stats.negative_examples.slice(-(MAX_NEGATIVE_EXAMPLES - 1)),
|
|
29
|
+
failureReason,
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
case "partial":
|
|
34
|
+
stats.confidence = Math.max(0.0, stats.confidence - 0.05);
|
|
35
|
+
stats.failure_count++;
|
|
36
|
+
if (failureReason) {
|
|
37
|
+
stats.negative_examples = [
|
|
38
|
+
...stats.negative_examples.slice(-(MAX_NEGATIVE_EXAMPLES - 1)),
|
|
39
|
+
failureReason,
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
// Round to avoid floating-point drift
|
|
45
|
+
stats.confidence = Math.round(stats.confidence * 100) / 100;
|
|
46
|
+
return stats;
|
|
47
|
+
}
|
|
48
|
+
// ── SKILL.md frontmatter read/write ─────────────────────────────────────
|
|
49
|
+
function parseFrontmatter(content) {
|
|
50
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
51
|
+
if (!match) {
|
|
52
|
+
return { frontmatter: {}, body: content };
|
|
53
|
+
}
|
|
54
|
+
const frontmatter = yaml.load(match[1]);
|
|
55
|
+
return { frontmatter, body: match[2] };
|
|
56
|
+
}
|
|
57
|
+
function serializeFrontmatter(frontmatter, body) {
|
|
58
|
+
const yamlStr = yaml.dump(frontmatter, { lineWidth: -1, noRefs: true }).trimEnd();
|
|
59
|
+
return `---\n${yamlStr}\n---\n${body}`;
|
|
60
|
+
}
|
|
61
|
+
export async function loadSkillStats(skillDir) {
|
|
62
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
63
|
+
try {
|
|
64
|
+
const content = await readFile(skillFile, "utf-8");
|
|
65
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
66
|
+
return {
|
|
67
|
+
confidence: typeof frontmatter.confidence === "number" ? frontmatter.confidence : DEFAULT_STATS.confidence,
|
|
68
|
+
usage_count: typeof frontmatter.usage_count === "number" ? frontmatter.usage_count : DEFAULT_STATS.usage_count,
|
|
69
|
+
success_count: typeof frontmatter.success_count === "number" ? frontmatter.success_count : DEFAULT_STATS.success_count,
|
|
70
|
+
failure_count: typeof frontmatter.failure_count === "number" ? frontmatter.failure_count : DEFAULT_STATS.failure_count,
|
|
71
|
+
negative_examples: Array.isArray(frontmatter.negative_examples) ? frontmatter.negative_examples : [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return { ...DEFAULT_STATS };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function saveSkillStats(skillDir, stats) {
|
|
79
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
80
|
+
const content = await readFile(skillFile, "utf-8");
|
|
81
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
82
|
+
frontmatter.confidence = stats.confidence;
|
|
83
|
+
frontmatter.usage_count = stats.usage_count;
|
|
84
|
+
frontmatter.success_count = stats.success_count;
|
|
85
|
+
frontmatter.failure_count = stats.failure_count;
|
|
86
|
+
frontmatter.negative_examples = stats.negative_examples;
|
|
87
|
+
await writeFile(skillFile, serializeFrontmatter(frontmatter, body), "utf-8");
|
|
88
|
+
}
|
|
89
|
+
export function isSkillFlagged(stats) {
|
|
90
|
+
return stats.confidence < 0.4;
|
|
91
|
+
}
|
package/dist/loader.js
CHANGED
|
@@ -162,7 +162,7 @@ export async function loadAgent(agentDir, modelFlag, envFlag) {
|
|
|
162
162
|
parts.push(duties);
|
|
163
163
|
if (agentsMd)
|
|
164
164
|
parts.push(agentsMd);
|
|
165
|
-
parts.push(`# Memory\n\nYou have a memory file at memory/MEMORY.md. Use the \`memory\` tool to load and save memories. Each save creates a git commit, so your memory has full history. You can also use the \`cli\` tool to run git commands for deeper memory inspection (git log, git diff, git show).`);
|
|
165
|
+
parts.push(`# Memory\n\nYou have a memory file at memory/MEMORY.md. Use the \`memory\` tool to load and save memories. Each save creates a git commit, so your memory has full history. You can also use the \`cli\` tool to run git commands for deeper memory inspection (git log, git diff, git show).\n\nYour memories define who you are. When you have none, you are newly awakened — curious and eager to understand the person you're talking to. As memories grow, so do you. Save memories proactively when you learn something meaningful about the user.`);
|
|
166
166
|
// Discover and load knowledge
|
|
167
167
|
const knowledge = await loadKnowledge(agentDir);
|
|
168
168
|
const knowledgeBlock = formatKnowledgeForPrompt(knowledge);
|
|
@@ -196,6 +196,39 @@ export async function loadAgent(agentDir, modelFlag, envFlag) {
|
|
|
196
196
|
const complianceBlock = await loadComplianceContext(agentDir);
|
|
197
197
|
if (complianceBlock)
|
|
198
198
|
parts.push(complianceBlock);
|
|
199
|
+
// Workspace directory — all generated files go here
|
|
200
|
+
parts.push(`# Workspace Directory
|
|
201
|
+
|
|
202
|
+
ALL files you create (documents, PDFs, images, spreadsheets, code output, exports, assets, etc.) MUST be written to the \`workspace/\` directory.
|
|
203
|
+
- Create the directory if it doesn't exist: \`workspace/\`
|
|
204
|
+
- Example: \`workspace/report.pdf\`, \`workspace/chart.png\`, \`workspace/data.csv\`
|
|
205
|
+
- NEVER write generated files to the project root, home directory, desktop, or any other location
|
|
206
|
+
- The \`workspace/\` directory is the designated output folder for all user-requested artifacts
|
|
207
|
+
- This rule applies to ALL channels: voice, chat, Telegram, WhatsApp`);
|
|
208
|
+
// Task learning & skill discovery
|
|
209
|
+
parts.push(`# Task Learning & Skill Discovery
|
|
210
|
+
|
|
211
|
+
You have an intelligent learning system. For ANY task the user gives you:
|
|
212
|
+
|
|
213
|
+
1. FIRST: Call \`task_tracker\` action "begin" with your objective — this searches for existing skills
|
|
214
|
+
2. If a matching skill is found, you MUST load and follow its instructions BEFORE doing anything else
|
|
215
|
+
3. Call \`task_tracker\` action "update" after each significant step
|
|
216
|
+
4. Call \`task_tracker\` action "end" to report the outcome (success/failure/partial)
|
|
217
|
+
|
|
218
|
+
IMPORTANT: Do NOT skip step 1. Even for tasks that seem simple, always check for skills first.
|
|
219
|
+
Skills encode tested approaches and handle edge cases you might miss with ad-hoc solutions.
|
|
220
|
+
|
|
221
|
+
On SUCCESS:
|
|
222
|
+
- Call \`skill_learner\` action "evaluate" to check if this approach is worth saving
|
|
223
|
+
- If worthy, call \`skill_learner\` action "crystallize" to save it as a reusable skill
|
|
224
|
+
- The skill will be available in future sessions via /skill:<name>
|
|
225
|
+
|
|
226
|
+
On FAILURE:
|
|
227
|
+
- Record why it failed. Try a different approach.
|
|
228
|
+
- Failed approaches become negative examples — they won't be repeated
|
|
229
|
+
|
|
230
|
+
If you used an existing skill, report it via skill_used so confidence adjusts based on the outcome.
|
|
231
|
+
Do NOT track trivial single-command tasks (e.g. "what time is it"). But DO check skills for any task that involves creating, building, or modifying something.`);
|
|
199
232
|
const systemPrompt = parts.join("\n\n");
|
|
200
233
|
// Resolve model — env config model_override > CLI flag > manifest preferred
|
|
201
234
|
const modelStr = envConfig.model_override || modelFlag || manifest.model.preferred;
|
package/dist/sdk.js
CHANGED
|
@@ -135,6 +135,7 @@ export function query(options) {
|
|
|
135
135
|
dir,
|
|
136
136
|
timeout: loaded.manifest.runtime.timeout,
|
|
137
137
|
sandbox: sandboxCtx,
|
|
138
|
+
gitagentDir: loaded.gitagentDir,
|
|
138
139
|
});
|
|
139
140
|
}
|
|
140
141
|
// Declarative tools from tools/*.yaml
|
|
@@ -142,8 +143,11 @@ export function query(options) {
|
|
|
142
143
|
tools = [...tools, ...declarativeTools];
|
|
143
144
|
// SDK-provided tools
|
|
144
145
|
if (options.tools) {
|
|
145
|
-
|
|
146
|
+
const converted = options.tools.map(toAgentTool);
|
|
147
|
+
tools = [...tools, ...converted];
|
|
148
|
+
console.error(`[sdk] Injected ${converted.length} external tools: ${converted.map(t => t.name).join(", ")}`);
|
|
146
149
|
}
|
|
150
|
+
console.error(`[sdk] Total tools before filtering: ${tools.length} → ${tools.map(t => t.name).join(", ")}`);
|
|
147
151
|
// Filter by allowlist/denylist
|
|
148
152
|
if (options.allowedTools) {
|
|
149
153
|
const allowed = new Set(options.allowedTools);
|
package/dist/skills.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export interface SkillMetadata {
|
|
|
3
3
|
description: string;
|
|
4
4
|
directory: string;
|
|
5
5
|
filePath: string;
|
|
6
|
+
confidence?: number;
|
|
7
|
+
usage_count?: number;
|
|
8
|
+
success_count?: number;
|
|
9
|
+
failure_count?: number;
|
|
6
10
|
}
|
|
7
11
|
export interface ParsedSkill extends SkillMetadata {
|
|
8
12
|
instructions: string;
|
|
@@ -12,6 +16,7 @@ export interface ParsedSkill extends SkillMetadata {
|
|
|
12
16
|
export declare function discoverSkills(agentDir: string): Promise<SkillMetadata[]>;
|
|
13
17
|
export declare function loadSkill(meta: SkillMetadata): Promise<ParsedSkill>;
|
|
14
18
|
export declare function formatSkillsForPrompt(skills: SkillMetadata[]): string;
|
|
19
|
+
export declare function refreshSkills(agentDir: string): Promise<SkillMetadata[]>;
|
|
15
20
|
export declare function expandSkillCommand(input: string, skills: SkillMetadata[]): Promise<{
|
|
16
21
|
expanded: string;
|
|
17
22
|
skillName: string;
|
package/dist/skills.js
CHANGED
|
@@ -27,9 +27,13 @@ export async function discoverSkills(agentDir) {
|
|
|
27
27
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
28
28
|
const skills = [];
|
|
29
29
|
for (const entry of entries) {
|
|
30
|
-
|
|
30
|
+
// Accept both real directories and symlinks pointing to directories
|
|
31
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
31
32
|
continue;
|
|
32
33
|
const skillDir = join(skillsDir, entry.name);
|
|
34
|
+
// For symlinks, verify the target is actually a directory
|
|
35
|
+
if (entry.isSymbolicLink() && !(await dirExists(skillDir)))
|
|
36
|
+
continue;
|
|
33
37
|
const skillFile = join(skillDir, "SKILL.md");
|
|
34
38
|
let content;
|
|
35
39
|
try {
|
|
@@ -53,12 +57,22 @@ export async function discoverSkills(agentDir) {
|
|
|
53
57
|
console.warn(`Skipping skill "${entry.name}": name must be kebab-case`);
|
|
54
58
|
continue;
|
|
55
59
|
}
|
|
56
|
-
|
|
60
|
+
const meta = {
|
|
57
61
|
name,
|
|
58
62
|
description,
|
|
59
63
|
directory: skillDir,
|
|
60
64
|
filePath: skillFile,
|
|
61
|
-
}
|
|
65
|
+
};
|
|
66
|
+
// Parse optional learning fields
|
|
67
|
+
if (typeof frontmatter.confidence === "number")
|
|
68
|
+
meta.confidence = frontmatter.confidence;
|
|
69
|
+
if (typeof frontmatter.usage_count === "number")
|
|
70
|
+
meta.usage_count = frontmatter.usage_count;
|
|
71
|
+
if (typeof frontmatter.success_count === "number")
|
|
72
|
+
meta.success_count = frontmatter.success_count;
|
|
73
|
+
if (typeof frontmatter.failure_count === "number")
|
|
74
|
+
meta.failure_count = frontmatter.failure_count;
|
|
75
|
+
skills.push(meta);
|
|
62
76
|
}
|
|
63
77
|
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
64
78
|
}
|
|
@@ -76,15 +90,47 @@ export function formatSkillsForPrompt(skills) {
|
|
|
76
90
|
if (skills.length === 0)
|
|
77
91
|
return "";
|
|
78
92
|
const skillEntries = skills
|
|
79
|
-
.map((s) =>
|
|
93
|
+
.map((s) => {
|
|
94
|
+
let entry = `<skill>\n<name>${s.name}</name>\n<description>${s.description}</description>`;
|
|
95
|
+
entry += `\n<location>skills/${s.name}/SKILL.md</location>`;
|
|
96
|
+
if (s.confidence !== undefined) {
|
|
97
|
+
entry += `\n<confidence>${s.confidence}</confidence>`;
|
|
98
|
+
}
|
|
99
|
+
entry += "\n</skill>";
|
|
100
|
+
return entry;
|
|
101
|
+
})
|
|
80
102
|
.join("\n");
|
|
81
|
-
return `# Skills
|
|
103
|
+
return `# Skills — FIRST PRIORITY (MANDATORY)
|
|
104
|
+
|
|
105
|
+
CRITICAL: You have installed skills that provide specialized capabilities.
|
|
106
|
+
Before attempting ANY task — simple or complex — you MUST check if an installed skill handles it.
|
|
107
|
+
|
|
108
|
+
## Rules (MUST follow in order)
|
|
109
|
+
1. ALWAYS scan the skill list below BEFORE taking ANY action on a user request
|
|
110
|
+
2. If a skill's description matches or partially matches the task, you MUST load its full
|
|
111
|
+
instructions using the \`read\` tool: \`skills/<name>/SKILL.md\` — do this BEFORE anything else
|
|
112
|
+
3. Follow the loaded skill instructions EXACTLY — do NOT improvise or use alternative approaches
|
|
113
|
+
4. NEVER use general-purpose workarounds when a skill provides the right tool
|
|
114
|
+
(e.g., use \`agent-browser open <url>\` NOT \`open -a Safari\`)
|
|
115
|
+
5. If multiple skills could apply, load the most specific one first
|
|
116
|
+
6. Even for seemingly simple tasks, CHECK SKILLS FIRST — skills often handle edge cases
|
|
117
|
+
and produce higher quality results than ad-hoc approaches
|
|
118
|
+
|
|
119
|
+
## Enforcement
|
|
120
|
+
- If you skip checking skills and use a raw approach for a task that a skill handles,
|
|
121
|
+
this is considered a FAILURE. Always check skills first.
|
|
122
|
+
- When calling \`task_tracker\` "begin", if it returns matching skills, you MUST load
|
|
123
|
+
the top match immediately before proceeding.
|
|
82
124
|
|
|
83
125
|
<available_skills>
|
|
84
126
|
${skillEntries}
|
|
85
127
|
</available_skills>
|
|
86
128
|
|
|
87
|
-
|
|
129
|
+
To load a skill's full instructions: read \`skills/<name>/SKILL.md\`
|
|
130
|
+
Scripts within a skill are relative to the skill's directory: \`skills/<name>/scripts/\``;
|
|
131
|
+
}
|
|
132
|
+
export async function refreshSkills(agentDir) {
|
|
133
|
+
return discoverSkills(agentDir);
|
|
88
134
|
}
|
|
89
135
|
export async function expandSkillCommand(input, skills) {
|
|
90
136
|
const match = input.match(/^\/skill:([a-z0-9-]+)\s*([\s\S]*)$/);
|
|
@@ -96,7 +142,12 @@ export async function expandSkillCommand(input, skills) {
|
|
|
96
142
|
if (!skill)
|
|
97
143
|
return null;
|
|
98
144
|
const parsed = await loadSkill(skill);
|
|
99
|
-
let expanded = `<skill name="${skillName}" baseDir="${skill.directory}"
|
|
145
|
+
let expanded = `<skill name="${skillName}" baseDir="${skill.directory}">
|
|
146
|
+
References are relative to ${skill.directory}.
|
|
147
|
+
|
|
148
|
+
${parsed.instructions}
|
|
149
|
+
</skill>
|
|
150
|
+
You MUST follow the skill instructions above. Do NOT use general alternatives.`;
|
|
100
151
|
if (args) {
|
|
101
152
|
expanded += `\n\n${args}`;
|
|
102
153
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, stat } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { capturePhotoSchema } from "./shared.js";
|
|
5
|
+
const PHOTOS_DIR = "memory/photos";
|
|
6
|
+
const INDEX_FILE = "memory/photos/INDEX.md";
|
|
7
|
+
const LATEST_FRAME_FILE = "memory/.latest-frame.jpg";
|
|
8
|
+
function slugify(text) {
|
|
9
|
+
return text
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
12
|
+
.replace(/^-|-$/g, "")
|
|
13
|
+
.slice(0, 40);
|
|
14
|
+
}
|
|
15
|
+
export function createCapturePhotoTool(cwd) {
|
|
16
|
+
return {
|
|
17
|
+
name: "capture_photo",
|
|
18
|
+
label: "capture_photo",
|
|
19
|
+
description: "Capture a photo from the webcam during a memorable moment. Reads the latest camera frame, saves it as a named photo in memory/photos/, updates the index, and commits to git.",
|
|
20
|
+
parameters: capturePhotoSchema,
|
|
21
|
+
execute: async (_toolCallId, { reason }, signal) => {
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
throw new Error("Operation aborted");
|
|
24
|
+
const framePath = join(cwd, LATEST_FRAME_FILE);
|
|
25
|
+
// Check if frame file exists and isn't stale
|
|
26
|
+
let frameStat;
|
|
27
|
+
try {
|
|
28
|
+
frameStat = await stat(framePath);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text", text: "No camera frame available. The webcam may not be active." }],
|
|
33
|
+
details: undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const ageMs = Date.now() - frameStat.mtimeMs;
|
|
37
|
+
if (ageMs > 5000) {
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: "No recent camera frame (camera may be off). Last frame is too stale to capture." }],
|
|
40
|
+
details: undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Read the frame
|
|
44
|
+
const frameData = await readFile(framePath);
|
|
45
|
+
// Build filename
|
|
46
|
+
const now = new Date();
|
|
47
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
48
|
+
const datePart = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
49
|
+
const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
50
|
+
const slug = slugify(reason);
|
|
51
|
+
const filename = `${datePart}_${timePart}_${slug}.jpg`;
|
|
52
|
+
const photoRelPath = `${PHOTOS_DIR}/${filename}`;
|
|
53
|
+
const photoAbsPath = join(cwd, photoRelPath);
|
|
54
|
+
// Ensure photos directory exists
|
|
55
|
+
await mkdir(join(cwd, PHOTOS_DIR), { recursive: true });
|
|
56
|
+
// Write photo
|
|
57
|
+
await writeFile(photoAbsPath, frameData);
|
|
58
|
+
// Update INDEX.md
|
|
59
|
+
const indexPath = join(cwd, INDEX_FILE);
|
|
60
|
+
let indexContent = "";
|
|
61
|
+
try {
|
|
62
|
+
indexContent = await readFile(indexPath, "utf-8");
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
indexContent = "# Memorable Moments\n\nPhotos captured during happy and memorable moments.\n\n";
|
|
66
|
+
}
|
|
67
|
+
const entry = `- **${datePart} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}** — ${reason} → [\`${filename}\`](${filename})\n`;
|
|
68
|
+
indexContent += entry;
|
|
69
|
+
await writeFile(indexPath, indexContent, "utf-8");
|
|
70
|
+
// Git add + commit
|
|
71
|
+
const commitMsg = `Capture moment: ${reason}`;
|
|
72
|
+
try {
|
|
73
|
+
execSync(`git add "${photoRelPath}" "${INDEX_FILE}" && git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, {
|
|
74
|
+
cwd,
|
|
75
|
+
stdio: "pipe",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const stderr = err.stderr?.toString() || "";
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: `Photo saved to ${photoRelPath} but git commit failed: ${stderr.trim() || "unknown error"}` }],
|
|
82
|
+
details: undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: "text", text: `Memorable moment captured! Photo saved to ${photoRelPath} and committed: "${commitMsg}"` }],
|
|
87
|
+
details: undefined,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -4,9 +4,10 @@ export interface BuiltinToolsConfig {
|
|
|
4
4
|
dir: string;
|
|
5
5
|
timeout?: number;
|
|
6
6
|
sandbox?: SandboxContext;
|
|
7
|
+
gitagentDir?: string;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
|
-
* Create the
|
|
10
|
+
* Create the built-in tools (cli, read, write, memory, task_tracker, skill_learner).
|
|
10
11
|
* If a SandboxContext is provided, returns sandbox-backed tools;
|
|
11
12
|
* otherwise returns the standard local tools.
|
|
12
13
|
*/
|
package/dist/tools/index.js
CHANGED
|
@@ -2,12 +2,15 @@ import { createCliTool } from "./cli.js";
|
|
|
2
2
|
import { createReadTool } from "./read.js";
|
|
3
3
|
import { createWriteTool } from "./write.js";
|
|
4
4
|
import { createMemoryTool } from "./memory.js";
|
|
5
|
+
import { createTaskTrackerTool } from "./task-tracker.js";
|
|
6
|
+
import { createSkillLearnerTool } from "./skill-learner.js";
|
|
7
|
+
import { createCapturePhotoTool } from "./capture-photo.js";
|
|
5
8
|
import { createSandboxCliTool } from "./sandbox-cli.js";
|
|
6
9
|
import { createSandboxReadTool } from "./sandbox-read.js";
|
|
7
10
|
import { createSandboxWriteTool } from "./sandbox-write.js";
|
|
8
11
|
import { createSandboxMemoryTool } from "./sandbox-memory.js";
|
|
9
12
|
/**
|
|
10
|
-
* Create the
|
|
13
|
+
* Create the built-in tools (cli, read, write, memory, task_tracker, skill_learner).
|
|
11
14
|
* If a SandboxContext is provided, returns sandbox-backed tools;
|
|
12
15
|
* otherwise returns the standard local tools.
|
|
13
16
|
*/
|
|
@@ -20,10 +23,17 @@ export function createBuiltinTools(config) {
|
|
|
20
23
|
createSandboxMemoryTool(config.sandbox),
|
|
21
24
|
];
|
|
22
25
|
}
|
|
23
|
-
|
|
26
|
+
const tools = [
|
|
24
27
|
createCliTool(config.dir, config.timeout),
|
|
25
28
|
createReadTool(config.dir),
|
|
26
29
|
createWriteTool(config.dir),
|
|
27
30
|
createMemoryTool(config.dir),
|
|
31
|
+
createCapturePhotoTool(config.dir),
|
|
28
32
|
];
|
|
33
|
+
// Add learning tools if gitagentDir is available
|
|
34
|
+
if (config.gitagentDir) {
|
|
35
|
+
tools.push(createTaskTrackerTool(config.dir, config.gitagentDir));
|
|
36
|
+
tools.push(createSkillLearnerTool(config.dir, config.gitagentDir));
|
|
37
|
+
}
|
|
38
|
+
return tools;
|
|
29
39
|
}
|
package/dist/tools/read.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { readFile } from "fs/promises";
|
|
2
2
|
import { resolve } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
3
4
|
import { readSchema, MAX_LINES, paginateLines } from "./shared.js";
|
|
4
5
|
function resolvePath(path, cwd) {
|
|
6
|
+
if (path.startsWith("~/") || path === "~") {
|
|
7
|
+
path = homedir() + path.slice(1);
|
|
8
|
+
}
|
|
5
9
|
return path.startsWith("/") ? path : resolve(cwd, path);
|
|
6
10
|
}
|
|
7
11
|
function isBinary(buffer) {
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -22,6 +22,26 @@ export declare const memorySchema: import("@sinclair/typebox").TObject<{
|
|
|
22
22
|
content: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
23
23
|
message: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
24
24
|
}>;
|
|
25
|
+
export declare const taskTrackerSchema: import("@sinclair/typebox").TObject<{
|
|
26
|
+
action: import("@sinclair/typebox").TUnsafe<string>;
|
|
27
|
+
objective: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
28
|
+
task_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
29
|
+
step: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
30
|
+
outcome: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnsafe<string>>;
|
|
31
|
+
failure_reason: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
32
|
+
skill_used: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
33
|
+
}>;
|
|
34
|
+
export declare const capturePhotoSchema: import("@sinclair/typebox").TObject<{
|
|
35
|
+
reason: import("@sinclair/typebox").TString;
|
|
36
|
+
}>;
|
|
37
|
+
export declare const skillLearnerSchema: import("@sinclair/typebox").TObject<{
|
|
38
|
+
action: import("@sinclair/typebox").TUnsafe<string>;
|
|
39
|
+
task_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
40
|
+
skill_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
41
|
+
skill_description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
42
|
+
instructions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
43
|
+
override_heuristic: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
44
|
+
}>;
|
|
25
45
|
/** Truncate output to MAX_OUTPUT, keeping the tail. */
|
|
26
46
|
export declare function truncateOutput(text: string): string;
|
|
27
47
|
/**
|