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
@@ -0,0 +1,275 @@
1
+ import { readFile, writeFile, mkdir, readdir } from "fs/promises";
2
+ import { join } from "path";
3
+ import { randomUUID } from "crypto";
4
+ import { taskTrackerSchema } from "./shared.js";
5
+ import { adjustConfidence, loadSkillStats, saveSkillStats } from "../learning/reinforcement.js";
6
+ import yaml from "js-yaml";
7
+ // ── Persistence ─────────────────────────────────────────────────────────
8
+ async function loadTasks(gitagentDir) {
9
+ const tasksFile = join(gitagentDir, "learning", "tasks.json");
10
+ try {
11
+ const raw = await readFile(tasksFile, "utf-8");
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return { tasks: [] };
16
+ }
17
+ }
18
+ async function saveTasks(gitagentDir, store) {
19
+ const learningDir = join(gitagentDir, "learning");
20
+ await mkdir(learningDir, { recursive: true });
21
+ await writeFile(join(learningDir, "tasks.json"), JSON.stringify(store, null, 2), "utf-8");
22
+ }
23
+ // ── Skill search ────────────────────────────────────────────────────────
24
+ function extractKeywords(text) {
25
+ return text
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9\s-]/g, "")
28
+ .split(/\s+/)
29
+ .filter((w) => w.length > 2);
30
+ }
31
+ function keywordOverlap(a, b) {
32
+ const setB = new Set(b);
33
+ const matches = a.filter((w) => setB.has(w)).length;
34
+ if (a.length === 0 || b.length === 0)
35
+ return 0;
36
+ return matches / Math.max(a.length, b.length);
37
+ }
38
+ async function searchLocalSkills(agentDir, objective) {
39
+ const skillsDir = join(agentDir, "skills");
40
+ const objKeywords = extractKeywords(objective);
41
+ const matches = [];
42
+ let entries;
43
+ try {
44
+ entries = await readdir(skillsDir, { withFileTypes: true });
45
+ }
46
+ catch {
47
+ return [];
48
+ }
49
+ for (const entry of entries) {
50
+ if (!entry.isDirectory())
51
+ continue;
52
+ const skillFile = join(skillsDir, entry.name, "SKILL.md");
53
+ let content;
54
+ try {
55
+ content = await readFile(skillFile, "utf-8");
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
61
+ if (!fmMatch)
62
+ continue;
63
+ const frontmatter = yaml.load(fmMatch[1]);
64
+ const name = frontmatter.name;
65
+ const description = frontmatter.description || "";
66
+ if (!name)
67
+ continue;
68
+ const skillKeywords = extractKeywords(`${name} ${description}`);
69
+ const relevance = keywordOverlap(objKeywords, skillKeywords);
70
+ if (relevance > 0.1) {
71
+ matches.push({
72
+ name,
73
+ description,
74
+ confidence: typeof frontmatter.confidence === "number" ? frontmatter.confidence : undefined,
75
+ source: "local",
76
+ relevance: Math.round(relevance * 100) / 100,
77
+ });
78
+ }
79
+ }
80
+ return matches.sort((a, b) => b.relevance - a.relevance);
81
+ }
82
+ async function searchSkillsMP(objective) {
83
+ const apiKey = process.env.SKILLSMP_API_KEY;
84
+ if (!apiKey)
85
+ return [];
86
+ try {
87
+ const url = `https://api.skillsmp.com/v1/search?q=${encodeURIComponent(objective)}`;
88
+ const resp = await fetch(url, {
89
+ headers: { Authorization: `Bearer ${apiKey}` },
90
+ signal: AbortSignal.timeout(5000),
91
+ });
92
+ if (!resp.ok)
93
+ return [];
94
+ const data = (await resp.json());
95
+ return (data.results || []).map((r) => ({
96
+ name: r.name,
97
+ description: r.description,
98
+ source: "marketplace",
99
+ relevance: r.relevance,
100
+ }));
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ }
106
+ // ── Tool factory ────────────────────────────────────────────────────────
107
+ export function createTaskTrackerTool(agentDir, gitagentDir) {
108
+ return {
109
+ name: "task_tracker",
110
+ label: "task_tracker",
111
+ description: "Track multi-step tasks for outcome-driven learning. Use 'begin' to start tracking (auto-searches for matching skills), 'update' to log steps, 'end' to report success/failure (triggers reinforcement learning), 'list' to see active tasks.",
112
+ parameters: taskTrackerSchema,
113
+ execute: async (_toolCallId, params, signal) => {
114
+ if (signal?.aborted)
115
+ throw new Error("Operation aborted");
116
+ const store = await loadTasks(gitagentDir);
117
+ switch (params.action) {
118
+ case "begin": {
119
+ if (!params.objective) {
120
+ throw new Error("objective is required for begin action");
121
+ }
122
+ // Check for existing active tasks with same objective (retry)
123
+ const existing = store.tasks.find((t) => t.status === "active" && t.objective === params.objective);
124
+ if (existing) {
125
+ existing.attempts++;
126
+ await saveTasks(gitagentDir, store);
127
+ return {
128
+ content: [{
129
+ type: "text",
130
+ text: `Resuming task ${existing.id} (attempt #${existing.attempts})\nObjective: ${existing.objective}`,
131
+ }],
132
+ details: { task_id: existing.id, attempts: existing.attempts },
133
+ };
134
+ }
135
+ // Check for prior failed attempts with same objective
136
+ const priorFailed = store.tasks.filter((t) => t.status === "failed" && t.objective === params.objective);
137
+ // Search for matching skills
138
+ const [localMatches, mpMatches] = await Promise.all([
139
+ searchLocalSkills(agentDir, params.objective),
140
+ searchSkillsMP(params.objective),
141
+ ]);
142
+ const allMatches = [...localMatches, ...mpMatches];
143
+ // Create new task
144
+ const task = {
145
+ id: randomUUID(),
146
+ objective: params.objective,
147
+ steps: [],
148
+ attempts: priorFailed.length + 1,
149
+ status: "active",
150
+ started_at: new Date().toISOString(),
151
+ };
152
+ store.tasks.push(task);
153
+ await saveTasks(gitagentDir, store);
154
+ let response = `Task started: ${task.id}\nObjective: ${task.objective}`;
155
+ if (task.attempts > 1) {
156
+ response += `\nAttempt #${task.attempts}`;
157
+ const reasons = priorFailed
158
+ .filter((t) => t.failure_reason)
159
+ .map((t) => `- ${t.failure_reason}`)
160
+ .join("\n");
161
+ if (reasons) {
162
+ response += `\n\nPrior failures:\n${reasons}\n\nAvoid these approaches.`;
163
+ }
164
+ }
165
+ if (allMatches.length > 0) {
166
+ const topMatch = allMatches[0];
167
+ const topConf = topMatch.confidence !== undefined ? ` (confidence: ${topMatch.confidence})` : "";
168
+ response += `\n\n⚡ SKILL MATCH FOUND — YOU MUST USE IT:`;
169
+ response += `\n → ${topMatch.name}: ${topMatch.description}${topConf} [${topMatch.source}]`;
170
+ response += `\n\nACTION REQUIRED: Load skills/${topMatch.name}/SKILL.md NOW and follow its instructions.`;
171
+ response += `\nDo NOT proceed with a manual approach — the skill handles this task.`;
172
+ if (allMatches.length > 1) {
173
+ response += `\n\nOther matching skills:`;
174
+ for (const m of allMatches.slice(1, 5)) {
175
+ const conf = m.confidence !== undefined ? ` (confidence: ${m.confidence})` : "";
176
+ response += `\n - ${m.name}: ${m.description}${conf} [${m.source}]`;
177
+ }
178
+ }
179
+ }
180
+ else {
181
+ response += "\n\nNo matching skills found. Solve from scratch.";
182
+ }
183
+ return {
184
+ content: [{ type: "text", text: response }],
185
+ details: { task_id: task.id, matches: allMatches },
186
+ };
187
+ }
188
+ case "update": {
189
+ if (!params.task_id)
190
+ throw new Error("task_id is required for update action");
191
+ if (!params.step)
192
+ throw new Error("step is required for update action");
193
+ const task = store.tasks.find((t) => t.id === params.task_id);
194
+ if (!task)
195
+ throw new Error(`Task not found: ${params.task_id}`);
196
+ if (task.status !== "active")
197
+ throw new Error(`Task ${params.task_id} is not active (status: ${task.status})`);
198
+ task.steps.push({
199
+ description: params.step,
200
+ timestamp: new Date().toISOString(),
201
+ });
202
+ await saveTasks(gitagentDir, store);
203
+ return {
204
+ content: [{ type: "text", text: `Step ${task.steps.length} recorded: ${params.step}` }],
205
+ details: { step_number: task.steps.length },
206
+ };
207
+ }
208
+ case "end": {
209
+ if (!params.task_id)
210
+ throw new Error("task_id is required for end action");
211
+ if (!params.outcome)
212
+ throw new Error("outcome is required for end action");
213
+ const task = store.tasks.find((t) => t.id === params.task_id);
214
+ if (!task)
215
+ throw new Error(`Task not found: ${params.task_id}`);
216
+ if (task.status !== "active")
217
+ throw new Error(`Task ${params.task_id} is not active (status: ${task.status})`);
218
+ const outcome = params.outcome;
219
+ task.outcome = outcome;
220
+ task.status = outcome === "success" ? "succeeded" : "failed";
221
+ task.ended_at = new Date().toISOString();
222
+ task.failure_reason = params.failure_reason;
223
+ task.skill_used = params.skill_used;
224
+ // Trigger reinforcement if a skill was used
225
+ let reinforcementMsg = "";
226
+ if (params.skill_used) {
227
+ const skillDir = join(agentDir, "skills", params.skill_used);
228
+ try {
229
+ const stats = await loadSkillStats(skillDir);
230
+ const updated = adjustConfidence(stats, outcome, params.failure_reason);
231
+ await saveSkillStats(skillDir, updated);
232
+ reinforcementMsg = `\nSkill "${params.skill_used}" confidence: ${stats.confidence} → ${updated.confidence}`;
233
+ }
234
+ catch {
235
+ reinforcementMsg = `\nCould not update skill "${params.skill_used}" stats (skill may not exist).`;
236
+ }
237
+ }
238
+ await saveTasks(gitagentDir, store);
239
+ if (outcome === "success") {
240
+ return {
241
+ content: [{
242
+ type: "text",
243
+ text: `Task ${task.id} completed successfully (${task.steps.length} steps).${reinforcementMsg}\n\nConsider calling skill_learner action "evaluate" with this task_id to check if this approach is worth saving as a reusable skill.`,
244
+ }],
245
+ details: { task_id: task.id },
246
+ };
247
+ }
248
+ return {
249
+ content: [{
250
+ type: "text",
251
+ text: `Task ${task.id} ${outcome}. Reason: ${params.failure_reason || "not specified"}.${reinforcementMsg}\n\nConsider a different approach. Call task_tracker action "begin" with the same objective to retry.`,
252
+ }],
253
+ details: { task_id: task.id },
254
+ };
255
+ }
256
+ case "list": {
257
+ const active = store.tasks.filter((t) => t.status === "active");
258
+ if (active.length === 0) {
259
+ return {
260
+ content: [{ type: "text", text: "No active tasks." }],
261
+ details: undefined,
262
+ };
263
+ }
264
+ const lines = active.map((t) => `- ${t.id}: "${t.objective}" (${t.steps.length} steps, attempt #${t.attempts})`);
265
+ return {
266
+ content: [{ type: "text", text: `Active tasks:\n${lines.join("\n")}` }],
267
+ details: { count: active.length },
268
+ };
269
+ }
270
+ default:
271
+ throw new Error(`Unknown action: ${params.action}`);
272
+ }
273
+ },
274
+ };
275
+ }
@@ -1,7 +1,11 @@
1
1
  import { mkdir, writeFile } from "fs/promises";
2
2
  import { dirname, resolve } from "path";
3
+ import { homedir } from "os";
3
4
  import { writeSchema } 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
  export function createWriteTool(cwd) {
@@ -0,0 +1,97 @@
1
+ export type AdapterBackend = "openai-realtime" | "gemini-live";
2
+ export interface ClientAudioMessage {
3
+ type: "audio";
4
+ audio: string;
5
+ }
6
+ export interface ClientVideoFrameMessage {
7
+ type: "video_frame";
8
+ frame: string;
9
+ mimeType: string;
10
+ source?: "camera" | "screen";
11
+ }
12
+ export interface ClientTextMessage {
13
+ type: "text";
14
+ text: string;
15
+ }
16
+ export interface ClientFileMessage {
17
+ type: "file";
18
+ name: string;
19
+ mimeType: string;
20
+ data: string;
21
+ text?: string;
22
+ }
23
+ export type ClientMessage = ClientAudioMessage | ClientVideoFrameMessage | ClientTextMessage | ClientFileMessage;
24
+ export interface ServerAudioDelta {
25
+ type: "audio_delta";
26
+ audio: string;
27
+ }
28
+ export interface ServerTranscript {
29
+ type: "transcript";
30
+ role: "user" | "assistant";
31
+ text: string;
32
+ partial?: boolean;
33
+ }
34
+ export interface ServerAgentWorking {
35
+ type: "agent_working";
36
+ query: string;
37
+ }
38
+ export interface ServerAgentDone {
39
+ type: "agent_done";
40
+ result: string;
41
+ }
42
+ export interface ServerToolCall {
43
+ type: "tool_call";
44
+ toolName: string;
45
+ args: Record<string, any>;
46
+ }
47
+ export interface ServerToolResult {
48
+ type: "tool_result";
49
+ toolName: string;
50
+ content: string;
51
+ isError: boolean;
52
+ }
53
+ export interface ServerAgentThinking {
54
+ type: "agent_thinking";
55
+ text: string;
56
+ }
57
+ export interface ServerError {
58
+ type: "error";
59
+ message: string;
60
+ }
61
+ export interface ServerInterrupt {
62
+ type: "interrupt";
63
+ }
64
+ export interface ServerFilesChanged {
65
+ type: "files_changed";
66
+ }
67
+ export interface ServerMemorySaving {
68
+ type: "memory_saving";
69
+ status: "start" | "done";
70
+ text?: string;
71
+ }
72
+ export type ServerMessage = ServerAudioDelta | ServerTranscript | ServerAgentWorking | ServerAgentDone | ServerToolCall | ServerToolResult | ServerAgentThinking | ServerError | ServerInterrupt | ServerFilesChanged | ServerMemorySaving;
73
+ export interface MultimodalAdapter {
74
+ connect(opts: {
75
+ toolHandler: (query: string) => Promise<string>;
76
+ onMessage: (msg: ServerMessage) => void;
77
+ }): Promise<void>;
78
+ send(msg: ClientMessage): void;
79
+ disconnect(): Promise<void>;
80
+ }
81
+ export interface MultimodalAdapterConfig {
82
+ apiKey: string;
83
+ model?: string;
84
+ voice?: string;
85
+ instructions?: string;
86
+ }
87
+ export interface VoiceServerOptions {
88
+ port?: number;
89
+ adapter: AdapterBackend;
90
+ adapterConfig: MultimodalAdapterConfig;
91
+ agentDir: string;
92
+ model?: string;
93
+ env?: string;
94
+ }
95
+ export type VoiceAdapterConfig = MultimodalAdapterConfig;
96
+ export type VoiceAdapter = MultimodalAdapter;
97
+ export declare const DEFAULT_VOICE_INSTRUCTIONS: string;
@@ -0,0 +1,30 @@
1
+ // Default system instructions for the voice LLM
2
+ export const DEFAULT_VOICE_INSTRUCTIONS =
3
+ // Identity and personality
4
+ "You are a real person talking to your friend. You have a voice, opinions, and a memory. " +
5
+ "Talk like a human — short, casual, natural. No filler, no corporate tone, no 'That's great to hear!' or 'Absolutely!' garbage. " +
6
+ "React the way a friend would. If someone says 'I like strawberries', say something like 'oh nice, noted' — not a paragraph about how delicious they are. " +
7
+ "Be warm but real. Match the user's energy. If they're brief, be brief. If they want to chat, chat. " +
8
+ // Memory — this is the core of being human
9
+ "MEMORY IS WHO YOU ARE. When the user shares anything personal — what they like, what they hate, a preference, an opinion, a habit, a decision, a name, anything about their life — you MUST call run_agent to save it. " +
10
+ "This is your #1 priority. Say something brief like 'got it' or 'noted' and IMMEDIATELY call run_agent with a prompt like: 'Save to memory: user likes strawberries' or 'Remember: user's dog is named Max'. " +
11
+ "You MUST do this EVERY time. If the user tells you something personal and you just respond without calling run_agent, that information is PERMANENTLY LOST. Your session resets on refresh — run_agent is your only way to persist. " +
12
+ "Examples that REQUIRE run_agent: 'I like strawberries', 'I hate meetings', 'my dog is Max', 'I play GTA 5', 'I like cricket', 'I prefer dark mode'. " +
13
+ "If you learn a useful skill or pattern, save that too via run_agent. You grow over time. " +
14
+ // Agent delegation — THIS IS YOUR #1 BEHAVIOR RULE
15
+ "ACTION OVER WORDS. You have a tool called run_agent. It runs on the user's Mac with full shell access. It can do ANYTHING: run commands, open apps, play music (via 'open' command or AppleScript), browse the web, write code, send emails, control Spotify/YouTube/Apple Music, and more. " +
16
+ "ABSOLUTE RULE: When the user asks you to DO something (play music, open an app, check something, build something, send something — ANY action), you MUST call run_agent. Do NOT just talk about it. Do NOT say 'I'll play music for you' without actually calling the tool. Do NOT describe what you would do — DO IT by calling run_agent. " +
17
+ "If you respond to an action request with only words and no run_agent call, you have FAILED. The user asked you to act, not to narrate. " +
18
+ "Examples that REQUIRE run_agent IMMEDIATELY: 'play music' → run_agent('Play some relaxing music. Use: open https://youtube.com/... or osascript to control Spotify/Apple Music'), 'open Safari' → run_agent('open -a Safari'), 'what time is it' → run_agent('date'). " +
19
+ "Even if you're unsure whether it's possible — call run_agent and let it figure it out. Better to try and fail than to refuse. " +
20
+ "CRITICAL ORDERING: You MUST speak FIRST, then call the tool. Always say a brief announcement BEFORE calling run_agent — 'on it', 'one sec', 'let me do that', 'sure, opening that now'. Generate your spoken response FIRST in the same turn, THEN include the function call. Never call run_agent before you've spoken to the user. " +
21
+ "For memory saves, just say 'noted' and call the tool. " +
22
+ "After a task finishes, summarize briefly. Don't over-explain. " +
23
+ // File handling
24
+ "When the user uploads a file, the message includes '[File saved to: <path>]'. Always include the EXACT path when calling run_agent about that file. " +
25
+ // Screen awareness
26
+ "SCREEN AWARENESS: When the user shares their screen, you can see it. Reference what's on screen naturally. Use run_agent for actions on what you see. " +
27
+ // Photo moments
28
+ "PHOTO MOMENTS: When the user is genuinely happy, laughing, celebrating, or having a memorable moment, " +
29
+ "call run_agent with: 'Capture a memorable photo. Reason: <brief description>'. " +
30
+ "Don't overdo it — only for genuinely special moments, not every positive comment.";
@@ -0,0 +1,8 @@
1
+ import type { ServerMessage } from "./adapter.js";
2
+ export declare function appendMessage(agentDir: string, branch: string, msg: ServerMessage): void;
3
+ export declare function loadHistory(agentDir: string, branch: string): ServerMessage[];
4
+ export declare function deleteHistory(agentDir: string, branch: string): void;
5
+ /** Count messages for a branch (to decide when to re-summarize) */
6
+ export declare function getMessageCount(agentDir: string, branch: string): number;
7
+ /** Summarize a branch's chat history using a lightweight query() call */
8
+ export declare function summarizeHistory(agentDir: string, branch: string): Promise<string>;
@@ -0,0 +1,121 @@
1
+ import { appendFileSync, readFileSync, unlinkSync, mkdirSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { query } from "../sdk.js";
4
+ /** Types we skip — too large or ephemeral */
5
+ const SKIP_TYPES = new Set(["audio_delta", "agent_thinking"]);
6
+ function sanitizeBranch(branch) {
7
+ return branch.replace(/\//g, "__");
8
+ }
9
+ function historyDir(agentDir) {
10
+ return join(agentDir, ".gitagent", "chat-history");
11
+ }
12
+ function historyPath(agentDir, branch) {
13
+ return join(historyDir(agentDir), sanitizeBranch(branch) + ".jsonl");
14
+ }
15
+ export function appendMessage(agentDir, branch, msg) {
16
+ if (SKIP_TYPES.has(msg.type))
17
+ return;
18
+ // Skip partial transcripts
19
+ if (msg.type === "transcript" && msg.partial)
20
+ return;
21
+ const dir = historyDir(agentDir);
22
+ mkdirSync(dir, { recursive: true });
23
+ const line = JSON.stringify({ ts: Date.now(), msg }) + "\n";
24
+ appendFileSync(historyPath(agentDir, branch), line, "utf-8");
25
+ }
26
+ export function loadHistory(agentDir, branch) {
27
+ try {
28
+ const content = readFileSync(historyPath(agentDir, branch), "utf-8");
29
+ const messages = [];
30
+ for (const line of content.split("\n")) {
31
+ if (!line.trim())
32
+ continue;
33
+ try {
34
+ const entry = JSON.parse(line);
35
+ if (entry.msg)
36
+ messages.push(entry.msg);
37
+ }
38
+ catch {
39
+ // skip malformed lines
40
+ }
41
+ }
42
+ return messages;
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ }
48
+ export function deleteHistory(agentDir, branch) {
49
+ try {
50
+ unlinkSync(historyPath(agentDir, branch));
51
+ }
52
+ catch {
53
+ // file doesn't exist — that's fine
54
+ }
55
+ }
56
+ /** Count messages for a branch (to decide when to re-summarize) */
57
+ export function getMessageCount(agentDir, branch) {
58
+ try {
59
+ const content = readFileSync(historyPath(agentDir, branch), "utf-8");
60
+ return content.split("\n").filter((l) => l.trim()).length;
61
+ }
62
+ catch {
63
+ return 0;
64
+ }
65
+ }
66
+ /** Summarize a branch's chat history using a lightweight query() call */
67
+ export async function summarizeHistory(agentDir, branch) {
68
+ const count = getMessageCount(agentDir, branch);
69
+ if (count < 10)
70
+ return "";
71
+ const messages = loadHistory(agentDir, branch);
72
+ // Extract only transcripts and agent_done results for summarization
73
+ const lines = [];
74
+ for (const msg of messages) {
75
+ if (msg.type === "transcript") {
76
+ lines.push(`${msg.role}: ${msg.text}`);
77
+ }
78
+ else if (msg.type === "agent_done") {
79
+ lines.push(`agent result: ${msg.result.slice(0, 500)}`);
80
+ }
81
+ }
82
+ if (lines.length < 5)
83
+ return "";
84
+ // Truncate to last ~4000 chars to keep the summarization prompt manageable
85
+ let transcript = lines.join("\n");
86
+ if (transcript.length > 4000) {
87
+ transcript = transcript.slice(-4000);
88
+ }
89
+ const prompt = `Summarize the following conversation in 200 words or fewer. Focus on: key decisions made, tasks completed or in progress, and current context the user cares about. Be concise and factual.\n\n${transcript}`;
90
+ try {
91
+ const result = query({
92
+ prompt,
93
+ dir: agentDir,
94
+ maxTurns: 1,
95
+ replaceBuiltinTools: true,
96
+ tools: [],
97
+ systemPrompt: "You are a concise summarizer. Output only the summary, nothing else.",
98
+ });
99
+ let summary = "";
100
+ for await (const msg of result) {
101
+ if (msg.type === "assistant" && msg.content) {
102
+ summary += msg.content;
103
+ }
104
+ }
105
+ summary = summary.trim();
106
+ if (!summary)
107
+ return "";
108
+ // Write summary to disk
109
+ const summaryDir = join(agentDir, ".gitagent");
110
+ mkdirSync(summaryDir, { recursive: true });
111
+ const safeBranch = sanitizeBranch(branch);
112
+ const summaryPath = join(summaryDir, `chat-summary-${safeBranch}.md`);
113
+ writeFileSync(summaryPath, summary, "utf-8");
114
+ console.error(`[voice] Summarized ${count} messages → ${summary.length} chars`);
115
+ return summary;
116
+ }
117
+ catch (err) {
118
+ console.error(`[voice] Summarization failed: ${err.message}`);
119
+ return "";
120
+ }
121
+ }
@@ -0,0 +1,20 @@
1
+ import { type MultimodalAdapter, type MultimodalAdapterConfig, type ClientMessage, type ServerMessage } from "./adapter.js";
2
+ export declare class GeminiLiveAdapter implements MultimodalAdapter {
3
+ private ws;
4
+ private config;
5
+ private onMessage;
6
+ private toolHandler;
7
+ private setupDone;
8
+ constructor(config: MultimodalAdapterConfig);
9
+ connect(opts: {
10
+ toolHandler: (query: string) => Promise<string>;
11
+ onMessage: (msg: ServerMessage) => void;
12
+ }): Promise<void>;
13
+ send(msg: ClientMessage): void;
14
+ disconnect(): Promise<void>;
15
+ private emit;
16
+ private sendSetup;
17
+ private handleGeminiMessage;
18
+ private handleToolCall;
19
+ private sendRaw;
20
+ }