gitclaw 0.3.0 → 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 +54 -28
  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
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
- if (!entry.isDirectory())
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
- skills.push({
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) => `<skill>\n<name>${s.name}</name>\n<description>${s.description}</description>\n</skill>`)
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
- When a task matches a skill, use the \`read\` tool to load \`skills/<name>/SKILL.md\` for full instructions. Scripts within a skill are relative to the skill's directory (e.g., \`skills/<name>/scripts/\`). Use the \`cli\` tool to execute them.`;
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}">\n${parsed.instructions}\n</skill>`;
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,3 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { capturePhotoSchema } from "./shared.js";
3
+ export declare function createCapturePhotoTool(cwd: string): AgentTool<typeof capturePhotoSchema>;
@@ -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
+ }
@@ -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 four built-in tools (cli, read, write, memory).
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
  */
@@ -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 four built-in tools (cli, read, write, memory).
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
- return [
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
  }
@@ -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) {
@@ -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
  /**
@@ -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>;