gitclaw 0.3.1 → 0.4.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/dist/composio/adapter.d.ts +26 -0
  4. package/dist/composio/adapter.js +92 -0
  5. package/dist/composio/client.d.ts +39 -0
  6. package/dist/composio/client.js +170 -0
  7. package/dist/composio/index.d.ts +2 -0
  8. package/dist/composio/index.js +2 -0
  9. package/dist/context.d.ts +20 -0
  10. package/dist/context.js +211 -0
  11. package/dist/exports.d.ts +2 -0
  12. package/dist/exports.js +1 -0
  13. package/dist/index.js +99 -7
  14. package/dist/learning/reinforcement.d.ts +11 -0
  15. package/dist/learning/reinforcement.js +91 -0
  16. package/dist/loader.js +34 -1
  17. package/dist/sdk.js +5 -1
  18. package/dist/skills.d.ts +5 -0
  19. package/dist/skills.js +58 -7
  20. package/dist/tools/capture-photo.d.ts +3 -0
  21. package/dist/tools/capture-photo.js +91 -0
  22. package/dist/tools/index.d.ts +2 -1
  23. package/dist/tools/index.js +12 -2
  24. package/dist/tools/read.js +4 -0
  25. package/dist/tools/shared.d.ts +20 -0
  26. package/dist/tools/shared.js +24 -0
  27. package/dist/tools/skill-learner.d.ts +3 -0
  28. package/dist/tools/skill-learner.js +358 -0
  29. package/dist/tools/task-tracker.d.ts +20 -0
  30. package/dist/tools/task-tracker.js +275 -0
  31. package/dist/tools/write.js +4 -0
  32. package/dist/voice/adapter.d.ts +97 -0
  33. package/dist/voice/adapter.js +30 -0
  34. package/dist/voice/chat-history.d.ts +8 -0
  35. package/dist/voice/chat-history.js +121 -0
  36. package/dist/voice/gemini-live.d.ts +20 -0
  37. package/dist/voice/gemini-live.js +279 -0
  38. package/dist/voice/index.d.ts +4 -0
  39. package/dist/voice/index.js +3 -0
  40. package/dist/voice/openai-realtime.d.ts +27 -0
  41. package/dist/voice/openai-realtime.js +291 -0
  42. package/dist/voice/server.d.ts +2 -0
  43. package/dist/voice/server.js +2319 -0
  44. package/dist/voice/ui.html +2556 -0
  45. package/package.json +21 -7
@@ -1,5 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { StringEnum } from "@mariozechner/pi-ai";
3
+ import { homedir } from "os";
3
4
  // ── Constants ───────────────────────────────────────────────────────────
4
5
  export const MAX_OUTPUT = 100_000; // ~100KB max output to send to LLM
5
6
  export const MAX_LINES = 2000;
@@ -26,6 +27,26 @@ export const memorySchema = Type.Object({
26
27
  content: Type.Optional(Type.String({ description: "Memory content to save (required for save)" })),
27
28
  message: Type.Optional(Type.String({ description: "Commit message describing why this memory changed (required for save)" })),
28
29
  });
30
+ export const taskTrackerSchema = Type.Object({
31
+ action: StringEnum(["begin", "update", "end", "list"], { description: "Action to perform" }),
32
+ objective: Type.Optional(Type.String({ description: "Task objective (required for begin)" })),
33
+ task_id: Type.Optional(Type.String({ description: "Task ID (required for update/end)" })),
34
+ step: Type.Optional(Type.String({ description: "Step description (for update)" })),
35
+ outcome: Type.Optional(StringEnum(["success", "failure", "partial"], { description: "Task outcome (for end)" })),
36
+ failure_reason: Type.Optional(Type.String({ description: "Why the task failed (for end+failure)" })),
37
+ skill_used: Type.Optional(Type.String({ description: "Name of skill used, if any (for end)" })),
38
+ });
39
+ export const capturePhotoSchema = Type.Object({
40
+ reason: Type.String({ description: "Why this moment is being captured (e.g. 'user celebrating project launch')" }),
41
+ });
42
+ export const skillLearnerSchema = Type.Object({
43
+ action: StringEnum(["evaluate", "crystallize", "status", "review", "update", "delete"], { description: "Action to perform" }),
44
+ task_id: Type.Optional(Type.String({ description: "Task ID (for evaluate/crystallize)" })),
45
+ skill_name: Type.Optional(Type.String({ description: "Skill name (for crystallize/update/delete)" })),
46
+ skill_description: Type.Optional(Type.String({ description: "Skill description (for crystallize)" })),
47
+ instructions: Type.Optional(Type.String({ description: "New instructions content (for update)" })),
48
+ override_heuristic: Type.Optional(Type.Boolean({ description: "Override skill-worthiness heuristic (for evaluate)" })),
49
+ });
29
50
  // ── Shared helpers ──────────────────────────────────────────────────────
30
51
  /** Truncate output to MAX_OUTPUT, keeping the tail. */
31
52
  export function truncateOutput(text) {
@@ -63,6 +84,9 @@ export function paginateLines(text, offset, limit) {
63
84
  }
64
85
  /** Resolve a path relative to a sandbox repo root. */
65
86
  export function resolveSandboxPath(path, repoRoot) {
87
+ if (path.startsWith("~/") || path === "~") {
88
+ path = homedir() + path.slice(1);
89
+ }
66
90
  if (path.startsWith("/"))
67
91
  return path;
68
92
  return repoRoot.endsWith("/") ? repoRoot + path : repoRoot + "/" + path;
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { skillLearnerSchema } from "./shared.js";
3
+ export declare function createSkillLearnerTool(agentDir: string, gitagentDir: string): AgentTool<typeof skillLearnerSchema>;
@@ -0,0 +1,358 @@
1
+ import { readFile, writeFile, mkdir, readdir, rm } from "fs/promises";
2
+ import { join } from "path";
3
+ import { execSync } from "child_process";
4
+ import { skillLearnerSchema } from "./shared.js";
5
+ import { loadSkillStats, isSkillFlagged } from "../learning/reinforcement.js";
6
+ import yaml from "js-yaml";
7
+ async function loadTasks(gitagentDir) {
8
+ const tasksFile = join(gitagentDir, "learning", "tasks.json");
9
+ try {
10
+ const raw = await readFile(tasksFile, "utf-8");
11
+ return JSON.parse(raw);
12
+ }
13
+ catch {
14
+ return { tasks: [] };
15
+ }
16
+ }
17
+ function extractKeywords(text) {
18
+ return text
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9\s-]/g, "")
21
+ .split(/\s+/)
22
+ .filter((w) => w.length > 2);
23
+ }
24
+ function jaccardSimilarity(a, b) {
25
+ const setA = new Set(a);
26
+ const setB = new Set(b);
27
+ const intersection = [...setA].filter((x) => setB.has(x)).length;
28
+ const union = new Set([...setA, ...setB]).size;
29
+ return union === 0 ? 0 : intersection / union;
30
+ }
31
+ // Checks if a step looks project-specific (absolute paths, UUIDs, etc.)
32
+ function isProjectSpecific(step) {
33
+ const patterns = [
34
+ /\/[a-zA-Z][\w/.-]{5,}/, // absolute-ish paths
35
+ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i, // UUID
36
+ /[A-Z][a-z]+(?:[A-Z][a-z]+){2,}/, // PascalCase with 3+ parts (likely project-specific class)
37
+ ];
38
+ return patterns.some((p) => p.test(step));
39
+ }
40
+ async function getExistingSkillDescriptions(agentDir) {
41
+ const skillsDir = join(agentDir, "skills");
42
+ const result = [];
43
+ let entries;
44
+ try {
45
+ entries = await readdir(skillsDir, { withFileTypes: true });
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ for (const entry of entries) {
51
+ if (!entry.isDirectory())
52
+ continue;
53
+ const skillFile = join(skillsDir, entry.name, "SKILL.md");
54
+ try {
55
+ const content = await readFile(skillFile, "utf-8");
56
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
57
+ if (!fmMatch)
58
+ continue;
59
+ const fm = yaml.load(fmMatch[1]);
60
+ if (fm.description) {
61
+ result.push({
62
+ name: fm.name,
63
+ keywords: extractKeywords(fm.description),
64
+ });
65
+ }
66
+ }
67
+ catch {
68
+ continue;
69
+ }
70
+ }
71
+ return result;
72
+ }
73
+ function gitCommit(agentDir, files, message) {
74
+ try {
75
+ for (const f of files) {
76
+ execSync(`git add "${f}"`, { cwd: agentDir, stdio: "pipe" });
77
+ }
78
+ execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
79
+ cwd: agentDir,
80
+ stdio: "pipe",
81
+ });
82
+ }
83
+ catch {
84
+ // Not fatal — file was still written
85
+ }
86
+ }
87
+ // ── Tool factory ────────────────────────────────────────────────────────
88
+ export function createSkillLearnerTool(agentDir, gitagentDir) {
89
+ return {
90
+ name: "skill_learner",
91
+ label: "skill_learner",
92
+ description: "Learn from successful tasks. Use 'evaluate' to check if a completed task is worth saving as a skill, 'crystallize' to save it, 'status' to list all skills with confidence scores, 'review' to see flagged low-confidence skills, 'update' to modify a skill, 'delete' to remove one.",
93
+ parameters: skillLearnerSchema,
94
+ execute: async (_toolCallId, params, signal) => {
95
+ if (signal?.aborted)
96
+ throw new Error("Operation aborted");
97
+ switch (params.action) {
98
+ case "evaluate": {
99
+ if (!params.task_id)
100
+ throw new Error("task_id is required for evaluate action");
101
+ const store = await loadTasks(gitagentDir);
102
+ const task = store.tasks.find((t) => t.id === params.task_id);
103
+ if (!task)
104
+ throw new Error(`Task not found: ${params.task_id}`);
105
+ if (task.status !== "succeeded") {
106
+ return {
107
+ content: [{ type: "text", text: `Task ${params.task_id} did not succeed (status: ${task.status}). Only successful tasks can become skills.` }],
108
+ details: undefined,
109
+ };
110
+ }
111
+ // Skill-worthiness heuristic
112
+ const checks = {
113
+ multi_step: task.steps.length >= 3,
114
+ non_trivial: task.steps.length >= 2,
115
+ novel: true,
116
+ generalizable: true,
117
+ };
118
+ // Check novelty: no existing skill with >0.5 Jaccard similarity
119
+ const taskKeywords = extractKeywords(task.objective);
120
+ const existingSkills = await getExistingSkillDescriptions(agentDir);
121
+ for (const skill of existingSkills) {
122
+ if (jaccardSimilarity(taskKeywords, skill.keywords) > 0.5) {
123
+ checks.novel = false;
124
+ break;
125
+ }
126
+ }
127
+ // Check generalizability: <30% of steps are project-specific
128
+ const specificSteps = task.steps.filter((s) => isProjectSpecific(s.description)).length;
129
+ checks.generalizable = specificSteps / Math.max(task.steps.length, 1) < 0.3;
130
+ const passCount = Object.values(checks).filter(Boolean).length;
131
+ const worthy = params.override_heuristic || passCount >= 3 || (checks.multi_step && checks.novel);
132
+ const reasons = [
133
+ `Multi-step (${task.steps.length} steps): ${checks.multi_step ? "PASS" : "FAIL"}`,
134
+ `Non-trivial: ${checks.non_trivial ? "PASS" : "FAIL"}`,
135
+ `Novel: ${checks.novel ? "PASS" : "FAIL"}`,
136
+ `Generalizable: ${checks.generalizable ? "PASS" : "FAIL"}`,
137
+ ];
138
+ if (worthy) {
139
+ return {
140
+ content: [{
141
+ type: "text",
142
+ text: `Task IS worthy of becoming a skill.\n\nChecks:\n${reasons.join("\n")}\n\nCall skill_learner action "crystallize" with this task_id, a skill_name (kebab-case), and a skill_description.`,
143
+ }],
144
+ details: { worthy: true, checks },
145
+ };
146
+ }
147
+ return {
148
+ content: [{
149
+ type: "text",
150
+ text: `Task is NOT worthy of becoming a skill (${passCount}/4 checks passed).\n\nChecks:\n${reasons.join("\n")}`,
151
+ }],
152
+ details: { worthy: false, checks },
153
+ };
154
+ }
155
+ case "crystallize": {
156
+ if (!params.task_id)
157
+ throw new Error("task_id is required for crystallize action");
158
+ if (!params.skill_name)
159
+ throw new Error("skill_name is required for crystallize action");
160
+ if (!params.skill_description)
161
+ throw new Error("skill_description is required for crystallize action");
162
+ // Validate kebab-case
163
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(params.skill_name)) {
164
+ throw new Error("skill_name must be kebab-case (e.g., deploy-staging)");
165
+ }
166
+ const store = await loadTasks(gitagentDir);
167
+ const task = store.tasks.find((t) => t.id === params.task_id);
168
+ if (!task)
169
+ throw new Error(`Task not found: ${params.task_id}`);
170
+ // SUCCESS GATE
171
+ if (task.outcome !== "success") {
172
+ throw new Error(`Cannot crystallize failed task. Only successful tasks can become skills.`);
173
+ }
174
+ // Build SKILL.md
175
+ const frontmatter = {
176
+ name: params.skill_name,
177
+ description: params.skill_description,
178
+ learned_from: `task:${task.id}`,
179
+ learned_at: new Date().toISOString(),
180
+ confidence: 1.0,
181
+ usage_count: 0,
182
+ success_count: 0,
183
+ failure_count: 0,
184
+ negative_examples: [],
185
+ };
186
+ const stepsSection = task.steps
187
+ .map((s, i) => `${i + 1}. ${s.description}`)
188
+ .join("\n");
189
+ // Collect negative examples from prior failed attempts with same objective
190
+ const priorFailed = store.tasks.filter((t) => t.status === "failed" && t.objective === task.objective);
191
+ let whatDidNotWork = "";
192
+ if (priorFailed.length > 0) {
193
+ const failureReasons = priorFailed
194
+ .filter((t) => t.failure_reason)
195
+ .map((t) => `- ${t.failure_reason}`)
196
+ .join("\n");
197
+ if (failureReasons) {
198
+ whatDidNotWork = failureReasons;
199
+ }
200
+ }
201
+ let body = `\n## Steps\n${stepsSection}\n\n## What Worked\nThis approach succeeded on attempt #${task.attempts}.\n`;
202
+ if (whatDidNotWork) {
203
+ body += `\n## What Did NOT Work\n${whatDidNotWork}\n`;
204
+ }
205
+ const content = `---\n${yaml.dump(frontmatter, { lineWidth: -1, noRefs: true }).trimEnd()}\n---\n${body}`;
206
+ // Write skill
207
+ const skillDir = join(agentDir, "skills", params.skill_name);
208
+ await mkdir(skillDir, { recursive: true });
209
+ const skillFile = join(skillDir, "SKILL.md");
210
+ await writeFile(skillFile, content, "utf-8");
211
+ // Git commit
212
+ gitCommit(agentDir, [`skills/${params.skill_name}/SKILL.md`], `Learn skill: ${params.skill_name}`);
213
+ return {
214
+ content: [{
215
+ type: "text",
216
+ text: `Skill "${params.skill_name}" crystallized and committed.\nPath: skills/${params.skill_name}/SKILL.md\nConfidence: 1.0\n\nThe skill is now available via /skill:${params.skill_name}`,
217
+ }],
218
+ details: { skill_name: params.skill_name, path: skillFile },
219
+ };
220
+ }
221
+ case "status": {
222
+ const skillsDir = join(agentDir, "skills");
223
+ let entries;
224
+ try {
225
+ entries = await readdir(skillsDir, { withFileTypes: true });
226
+ }
227
+ catch {
228
+ return {
229
+ content: [{ type: "text", text: "No skills directory found." }],
230
+ details: undefined,
231
+ };
232
+ }
233
+ const skills = [];
234
+ for (const entry of entries) {
235
+ if (!entry.isDirectory())
236
+ continue;
237
+ const dir = join(skillsDir, entry.name);
238
+ const stats = await loadSkillStats(dir);
239
+ // Only include learned skills (those with stats fields)
240
+ skills.push({
241
+ name: entry.name,
242
+ confidence: stats.confidence,
243
+ usage: stats.usage_count,
244
+ ratio: `${stats.success_count}/${stats.success_count + stats.failure_count}`,
245
+ });
246
+ }
247
+ if (skills.length === 0) {
248
+ return {
249
+ content: [{ type: "text", text: "No skills found." }],
250
+ details: undefined,
251
+ };
252
+ }
253
+ const lines = skills.map((s) => ` ${s.name}: confidence=${s.confidence}, usage=${s.usage}, success_ratio=${s.ratio}`);
254
+ return {
255
+ content: [{ type: "text", text: `Skills:\n${lines.join("\n")}` }],
256
+ details: { skills },
257
+ };
258
+ }
259
+ case "review": {
260
+ const skillsDir = join(agentDir, "skills");
261
+ let entries;
262
+ try {
263
+ entries = await readdir(skillsDir, { withFileTypes: true });
264
+ }
265
+ catch {
266
+ return {
267
+ content: [{ type: "text", text: "No skills directory found." }],
268
+ details: undefined,
269
+ };
270
+ }
271
+ const flagged = [];
272
+ for (const entry of entries) {
273
+ if (!entry.isDirectory())
274
+ continue;
275
+ const dir = join(skillsDir, entry.name);
276
+ const stats = await loadSkillStats(dir);
277
+ if (isSkillFlagged(stats)) {
278
+ flagged.push({
279
+ name: entry.name,
280
+ confidence: stats.confidence,
281
+ negatives: stats.negative_examples,
282
+ });
283
+ }
284
+ }
285
+ if (flagged.length === 0) {
286
+ return {
287
+ content: [{ type: "text", text: "No flagged skills (all confidence >= 0.4)." }],
288
+ details: undefined,
289
+ };
290
+ }
291
+ const lines = flagged.map((s) => {
292
+ let line = ` ${s.name}: confidence=${s.confidence}`;
293
+ if (s.negatives.length > 0) {
294
+ line += `\n Failures: ${s.negatives.join("; ")}`;
295
+ }
296
+ return line;
297
+ });
298
+ return {
299
+ content: [{ type: "text", text: `Flagged skills (confidence < 0.4):\n${lines.join("\n")}\n\nConsider updating or deleting these skills.` }],
300
+ details: { flagged },
301
+ };
302
+ }
303
+ case "update": {
304
+ if (!params.skill_name)
305
+ throw new Error("skill_name is required for update action");
306
+ if (!params.instructions)
307
+ throw new Error("instructions is required for update action");
308
+ const skillFile = join(agentDir, "skills", params.skill_name, "SKILL.md");
309
+ let content;
310
+ try {
311
+ content = await readFile(skillFile, "utf-8");
312
+ }
313
+ catch {
314
+ throw new Error(`Skill not found: ${params.skill_name}`);
315
+ }
316
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
317
+ if (!fmMatch)
318
+ throw new Error("Invalid SKILL.md format");
319
+ const frontmatter = yaml.load(fmMatch[1]);
320
+ const yamlStr = yaml.dump(frontmatter, { lineWidth: -1, noRefs: true }).trimEnd();
321
+ const updated = `---\n${yamlStr}\n---\n${params.instructions}\n`;
322
+ await writeFile(skillFile, updated, "utf-8");
323
+ gitCommit(agentDir, [`skills/${params.skill_name}/SKILL.md`], `Update skill: ${params.skill_name}`);
324
+ return {
325
+ content: [{ type: "text", text: `Skill "${params.skill_name}" updated and committed.` }],
326
+ details: undefined,
327
+ };
328
+ }
329
+ case "delete": {
330
+ if (!params.skill_name)
331
+ throw new Error("skill_name is required for delete action");
332
+ const skillDir = join(agentDir, "skills", params.skill_name);
333
+ try {
334
+ await rm(skillDir, { recursive: true });
335
+ }
336
+ catch {
337
+ throw new Error(`Skill not found: ${params.skill_name}`);
338
+ }
339
+ try {
340
+ execSync(`git add -A && git commit -m "Delete skill: ${params.skill_name.replace(/"/g, '\\"')}"`, {
341
+ cwd: agentDir,
342
+ stdio: "pipe",
343
+ });
344
+ }
345
+ catch {
346
+ // Not fatal
347
+ }
348
+ return {
349
+ content: [{ type: "text", text: `Skill "${params.skill_name}" deleted.` }],
350
+ details: undefined,
351
+ };
352
+ }
353
+ default:
354
+ throw new Error(`Unknown action: ${params.action}`);
355
+ }
356
+ },
357
+ };
358
+ }
@@ -0,0 +1,20 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { taskTrackerSchema } from "./shared.js";
3
+ interface TaskStep {
4
+ description: string;
5
+ timestamp: string;
6
+ }
7
+ export interface TaskRecord {
8
+ id: string;
9
+ objective: string;
10
+ steps: TaskStep[];
11
+ attempts: number;
12
+ status: "active" | "succeeded" | "failed";
13
+ outcome?: "success" | "failure" | "partial";
14
+ failure_reason?: string;
15
+ skill_used?: string;
16
+ started_at: string;
17
+ ended_at?: string;
18
+ }
19
+ export declare function createTaskTrackerTool(agentDir: string, gitagentDir: string): AgentTool<typeof taskTrackerSchema>;
20
+ export {};