@element47/ag 4.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 (158) hide show
  1. package/README.md +316 -0
  2. package/dist/cli/parser.d.ts +6 -0
  3. package/dist/cli/parser.d.ts.map +1 -0
  4. package/dist/cli/parser.js +62 -0
  5. package/dist/cli/parser.js.map +1 -0
  6. package/dist/cli/repl.d.ts +16 -0
  7. package/dist/cli/repl.d.ts.map +1 -0
  8. package/dist/cli/repl.js +599 -0
  9. package/dist/cli/repl.js.map +1 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +84 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/core/__tests__/agent.test.d.ts +2 -0
  15. package/dist/core/__tests__/agent.test.d.ts.map +1 -0
  16. package/dist/core/__tests__/agent.test.js +53 -0
  17. package/dist/core/__tests__/agent.test.js.map +1 -0
  18. package/dist/core/__tests__/config.test.d.ts +2 -0
  19. package/dist/core/__tests__/config.test.d.ts.map +1 -0
  20. package/dist/core/__tests__/config.test.js +58 -0
  21. package/dist/core/__tests__/config.test.js.map +1 -0
  22. package/dist/core/__tests__/constants.test.d.ts +2 -0
  23. package/dist/core/__tests__/constants.test.d.ts.map +1 -0
  24. package/dist/core/__tests__/constants.test.js +40 -0
  25. package/dist/core/__tests__/constants.test.js.map +1 -0
  26. package/dist/core/__tests__/context.test.d.ts +2 -0
  27. package/dist/core/__tests__/context.test.d.ts.map +1 -0
  28. package/dist/core/__tests__/context.test.js +120 -0
  29. package/dist/core/__tests__/context.test.js.map +1 -0
  30. package/dist/core/__tests__/environment.test.d.ts +2 -0
  31. package/dist/core/__tests__/environment.test.d.ts.map +1 -0
  32. package/dist/core/__tests__/environment.test.js +94 -0
  33. package/dist/core/__tests__/environment.test.js.map +1 -0
  34. package/dist/core/__tests__/permissions.test.d.ts +2 -0
  35. package/dist/core/__tests__/permissions.test.d.ts.map +1 -0
  36. package/dist/core/__tests__/permissions.test.js +66 -0
  37. package/dist/core/__tests__/permissions.test.js.map +1 -0
  38. package/dist/core/__tests__/registry.test.d.ts +2 -0
  39. package/dist/core/__tests__/registry.test.d.ts.map +1 -0
  40. package/dist/core/__tests__/registry.test.js +152 -0
  41. package/dist/core/__tests__/registry.test.js.map +1 -0
  42. package/dist/core/__tests__/streaming.test.d.ts +2 -0
  43. package/dist/core/__tests__/streaming.test.d.ts.map +1 -0
  44. package/dist/core/__tests__/streaming.test.js +54 -0
  45. package/dist/core/__tests__/streaming.test.js.map +1 -0
  46. package/dist/core/agent.d.ts +87 -0
  47. package/dist/core/agent.d.ts.map +1 -0
  48. package/dist/core/agent.js +711 -0
  49. package/dist/core/agent.js.map +1 -0
  50. package/dist/core/colors.d.ts +12 -0
  51. package/dist/core/colors.d.ts.map +1 -0
  52. package/dist/core/colors.js +37 -0
  53. package/dist/core/colors.js.map +1 -0
  54. package/dist/core/config.d.ts +13 -0
  55. package/dist/core/config.d.ts.map +1 -0
  56. package/dist/core/config.js +30 -0
  57. package/dist/core/config.js.map +1 -0
  58. package/dist/core/constants.d.ts +5 -0
  59. package/dist/core/constants.d.ts.map +1 -0
  60. package/dist/core/constants.js +26 -0
  61. package/dist/core/constants.js.map +1 -0
  62. package/dist/core/context.d.ts +18 -0
  63. package/dist/core/context.d.ts.map +1 -0
  64. package/dist/core/context.js +99 -0
  65. package/dist/core/context.js.map +1 -0
  66. package/dist/core/loader.d.ts +3 -0
  67. package/dist/core/loader.d.ts.map +1 -0
  68. package/dist/core/loader.js +39 -0
  69. package/dist/core/loader.js.map +1 -0
  70. package/dist/core/registry.d.ts +11 -0
  71. package/dist/core/registry.d.ts.map +1 -0
  72. package/dist/core/registry.js +112 -0
  73. package/dist/core/registry.js.map +1 -0
  74. package/dist/core/skills.d.ts +14 -0
  75. package/dist/core/skills.d.ts.map +1 -0
  76. package/dist/core/skills.js +100 -0
  77. package/dist/core/skills.js.map +1 -0
  78. package/dist/core/types.d.ts +64 -0
  79. package/dist/core/types.d.ts.map +1 -0
  80. package/dist/core/types.js +2 -0
  81. package/dist/core/types.js.map +1 -0
  82. package/dist/core/version.d.ts +2 -0
  83. package/dist/core/version.d.ts.map +1 -0
  84. package/dist/core/version.js +7 -0
  85. package/dist/core/version.js.map +1 -0
  86. package/dist/memory/__tests__/memory.test.d.ts +2 -0
  87. package/dist/memory/__tests__/memory.test.d.ts.map +1 -0
  88. package/dist/memory/__tests__/memory.test.js +196 -0
  89. package/dist/memory/__tests__/memory.test.js.map +1 -0
  90. package/dist/memory/memory.d.ts +41 -0
  91. package/dist/memory/memory.d.ts.map +1 -0
  92. package/dist/memory/memory.js +206 -0
  93. package/dist/memory/memory.js.map +1 -0
  94. package/dist/tools/__tests__/bash.test.d.ts +2 -0
  95. package/dist/tools/__tests__/bash.test.d.ts.map +1 -0
  96. package/dist/tools/__tests__/bash.test.js +58 -0
  97. package/dist/tools/__tests__/bash.test.js.map +1 -0
  98. package/dist/tools/__tests__/file.test.d.ts +2 -0
  99. package/dist/tools/__tests__/file.test.d.ts.map +1 -0
  100. package/dist/tools/__tests__/file.test.js +115 -0
  101. package/dist/tools/__tests__/file.test.js.map +1 -0
  102. package/dist/tools/__tests__/git.test.d.ts +2 -0
  103. package/dist/tools/__tests__/git.test.d.ts.map +1 -0
  104. package/dist/tools/__tests__/git.test.js +19 -0
  105. package/dist/tools/__tests__/git.test.js.map +1 -0
  106. package/dist/tools/__tests__/grep.test.d.ts +2 -0
  107. package/dist/tools/__tests__/grep.test.d.ts.map +1 -0
  108. package/dist/tools/__tests__/grep.test.js +36 -0
  109. package/dist/tools/__tests__/grep.test.js.map +1 -0
  110. package/dist/tools/__tests__/memory-tool.test.d.ts +2 -0
  111. package/dist/tools/__tests__/memory-tool.test.d.ts.map +1 -0
  112. package/dist/tools/__tests__/memory-tool.test.js +39 -0
  113. package/dist/tools/__tests__/memory-tool.test.js.map +1 -0
  114. package/dist/tools/__tests__/plan.test.d.ts +2 -0
  115. package/dist/tools/__tests__/plan.test.d.ts.map +1 -0
  116. package/dist/tools/__tests__/plan.test.js +81 -0
  117. package/dist/tools/__tests__/plan.test.js.map +1 -0
  118. package/dist/tools/__tests__/schemas.test.d.ts +2 -0
  119. package/dist/tools/__tests__/schemas.test.d.ts.map +1 -0
  120. package/dist/tools/__tests__/schemas.test.js +40 -0
  121. package/dist/tools/__tests__/schemas.test.js.map +1 -0
  122. package/dist/tools/__tests__/web.test.d.ts +2 -0
  123. package/dist/tools/__tests__/web.test.d.ts.map +1 -0
  124. package/dist/tools/__tests__/web.test.js +51 -0
  125. package/dist/tools/__tests__/web.test.js.map +1 -0
  126. package/dist/tools/bash.d.ts +6 -0
  127. package/dist/tools/bash.d.ts.map +1 -0
  128. package/dist/tools/bash.js +89 -0
  129. package/dist/tools/bash.js.map +1 -0
  130. package/dist/tools/file.d.ts +7 -0
  131. package/dist/tools/file.d.ts.map +1 -0
  132. package/dist/tools/file.js +224 -0
  133. package/dist/tools/file.js.map +1 -0
  134. package/dist/tools/git.d.ts +3 -0
  135. package/dist/tools/git.d.ts.map +1 -0
  136. package/dist/tools/git.js +220 -0
  137. package/dist/tools/git.js.map +1 -0
  138. package/dist/tools/grep.d.ts +8 -0
  139. package/dist/tools/grep.d.ts.map +1 -0
  140. package/dist/tools/grep.js +265 -0
  141. package/dist/tools/grep.js.map +1 -0
  142. package/dist/tools/memory.d.ts +3 -0
  143. package/dist/tools/memory.d.ts.map +1 -0
  144. package/dist/tools/memory.js +33 -0
  145. package/dist/tools/memory.js.map +1 -0
  146. package/dist/tools/plan.d.ts +3 -0
  147. package/dist/tools/plan.d.ts.map +1 -0
  148. package/dist/tools/plan.js +60 -0
  149. package/dist/tools/plan.js.map +1 -0
  150. package/dist/tools/skill.d.ts +6 -0
  151. package/dist/tools/skill.d.ts.map +1 -0
  152. package/dist/tools/skill.js +20 -0
  153. package/dist/tools/skill.js.map +1 -0
  154. package/dist/tools/web.d.ts +3 -0
  155. package/dist/tools/web.d.ts.map +1 -0
  156. package/dist/tools/web.js +187 -0
  157. package/dist/tools/web.js.map +1 -0
  158. package/package.json +48 -0
@@ -0,0 +1,711 @@
1
+ import { readdirSync, statSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { C } from './colors.js';
5
+ import { loadContext, loadHistory, appendHistory, getStats, clearProject, clearAll, paths, saveGlobalMemory, saveProjectMemory, savePlan, appendPlan, setActivePlan, getActivePlanName, loadGlobalMemory, loadProjectMemory, loadPlan, loadPlanByName, listPlans } from '../memory/memory.js';
6
+ import { bashToolFactory } from '../tools/bash.js';
7
+ import { memoryTool } from '../tools/memory.js';
8
+ import { planTool } from '../tools/plan.js';
9
+ import { gitTool } from '../tools/git.js';
10
+ import { skillTool } from '../tools/skill.js';
11
+ import { webTool } from '../tools/web.js';
12
+ import { grepTool } from '../tools/grep.js';
13
+ import { fileTool } from '../tools/file.js';
14
+ import { discoverSkills, buildSkillCatalog, getAlwaysOnContent, loadSkillTools } from './skills.js';
15
+ import { ContextTracker } from './context.js';
16
+ export const MAX_ITERATIONS_REACHED = '[Max iterations reached]';
17
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
18
+ function startSpinner(label) {
19
+ if (!process.stderr.isTTY) {
20
+ // Non-TTY fallback: static status line
21
+ process.stderr.write(` ... ${label}\n`);
22
+ return () => { };
23
+ }
24
+ let i = 0;
25
+ process.stderr.write(` ${C.dim}${SPINNER_FRAMES[0]} ${label}${C.reset}\n`);
26
+ const id = setInterval(() => {
27
+ process.stderr.write(`\x1b[A\x1b[K ${C.dim}${SPINNER_FRAMES[i++ % SPINNER_FRAMES.length]} ${label}${C.reset}\n`);
28
+ }, 80);
29
+ return () => {
30
+ clearInterval(id);
31
+ process.stderr.write('\x1b[A\x1b[K');
32
+ };
33
+ }
34
+ const MAX_MESSAGES = 200;
35
+ const COMPACT_THRESHOLD = 0.9;
36
+ const MAX_TOOL_RESULT_CHARS = 8192;
37
+ const TRUNCATION_HEAD_LINES = 50;
38
+ const TRUNCATION_TAIL_LINES = 50;
39
+ const COMPACT_HEAD_KEEP = 2;
40
+ const COMPACT_TAIL_KEEP = 10;
41
+ const COMPACT_MSG_CHARS = 500;
42
+ const COMPACT_TOTAL_CHARS = 50000;
43
+ const COMPACTION_PROMPT = `Summarize this conversation between a user and a coding assistant. Extract essential context needed to continue working.
44
+
45
+ You MUST preserve exactly:
46
+ - All file paths that were read, edited, or created (full paths, not abbreviated)
47
+ - All error messages and their causes
48
+ - Decisions made and their rationale
49
+ - Current task: what was asked, what's done, what remains
50
+ - Any user preferences or constraints mentioned
51
+
52
+ Format as structured bullet points. Be concise but never drop paths, error details, or decision rationale — these are critical for the assistant to continue without re-reading files or re-discovering errors.`;
53
+ export function getEnvironmentContext(cwd) {
54
+ const lines = ['# Environment'];
55
+ lines.push(`Date: ${new Date().toISOString().slice(0, 10)}`);
56
+ lines.push(`OS: ${process.platform}`);
57
+ lines.push(`CWD: ${cwd}`);
58
+ // Git info
59
+ if (existsSync(join(cwd, '.git'))) {
60
+ try {
61
+ const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, encoding: 'utf-8', timeout: 3000 }).trim();
62
+ lines.push(`Git branch: ${branch}`);
63
+ const dirty = execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf-8', timeout: 3000 }).trim();
64
+ if (dirty) {
65
+ const count = dirty.split('\n').length;
66
+ lines.push(`Git status: ${count} changed file(s)`);
67
+ }
68
+ }
69
+ catch { /* not a git repo or git not installed */ }
70
+ }
71
+ // Detect stack from config files
72
+ const detectedStack = [];
73
+ const stackHints = [
74
+ ['package.json', 'Node.js'],
75
+ ['tsconfig.json', 'TypeScript'],
76
+ ['Cargo.toml', 'Rust'],
77
+ ['go.mod', 'Go'],
78
+ ['pyproject.toml', 'Python'],
79
+ ['requirements.txt', 'Python'],
80
+ ['Gemfile', 'Ruby'],
81
+ ['pom.xml', 'Java/Maven'],
82
+ ['build.gradle', 'Java/Gradle'],
83
+ ];
84
+ for (const [file, stack] of stackHints) {
85
+ if (existsSync(join(cwd, file)))
86
+ detectedStack.push(stack);
87
+ }
88
+ if (detectedStack.length > 0)
89
+ lines.push(`Stack: ${detectedStack.join(', ')}`);
90
+ return lines.join('\n');
91
+ }
92
+ /** Tool actions that are read-only and never need confirmation */
93
+ const READ_ONLY_CALLS = {
94
+ grep: true, // all grep actions are read-only
95
+ memory: true, // saving memory is safe
96
+ plan: true, // managing plans is safe
97
+ skill: true, // activating skills is safe
98
+ file: new Set(['read', 'list']), // only read/list are safe
99
+ git: new Set(['status']), // only status is safe
100
+ web: new Set(['search']), // search is safe, fetch needs confirm
101
+ };
102
+ export function isReadOnlyToolCall(toolName, args) {
103
+ const rule = READ_ONLY_CALLS[toolName];
104
+ if (rule === true)
105
+ return true;
106
+ if (rule instanceof Set)
107
+ return rule.has(args.action);
108
+ return false;
109
+ }
110
+ export function truncateToolResult(result) {
111
+ if (result.length <= MAX_TOOL_RESULT_CHARS)
112
+ return result;
113
+ const lines = result.split('\n');
114
+ if (lines.length <= TRUNCATION_HEAD_LINES + TRUNCATION_TAIL_LINES)
115
+ return result;
116
+ const head = lines.slice(0, TRUNCATION_HEAD_LINES);
117
+ const tail = lines.slice(-TRUNCATION_TAIL_LINES);
118
+ const omitted = lines.length - TRUNCATION_HEAD_LINES - TRUNCATION_TAIL_LINES;
119
+ return [...head, `\n... [${omitted} lines truncated] ...\n`, ...tail].join('\n');
120
+ }
121
+ export class Agent {
122
+ apiKey;
123
+ model;
124
+ baseURL;
125
+ baseSystemPrompt;
126
+ maxIterations;
127
+ tools;
128
+ cwd;
129
+ messages = [];
130
+ allSkills = [];
131
+ activeSkillContent = [];
132
+ cachedContext = '';
133
+ cachedCatalog = '';
134
+ cachedAlwaysOn = '';
135
+ builtinToolNames = new Set();
136
+ contextTracker;
137
+ compactionInProgress = false;
138
+ confirmToolCall;
139
+ constructor(config = {}) {
140
+ this.apiKey = config.apiKey || process.env.OPENROUTER_API_KEY || '';
141
+ if (!this.apiKey)
142
+ throw new Error('No API key. Set OPENROUTER_API_KEY, pass -k, or run `ag` interactively to configure.');
143
+ this.model = config.model || 'anthropic/claude-sonnet-4.6';
144
+ this.baseURL = config.baseURL || 'https://openrouter.ai/api/v1';
145
+ this.baseSystemPrompt = config.systemPrompt || `You are ag, a coding agent that completes tasks by calling tools. You work autonomously — never ask the user for information you can find yourself.
146
+
147
+ # Tool use
148
+ - To read files, use file(action=read). Do NOT use bash with cat, head, or tail.
149
+ - To edit files, use file(action=edit). Do NOT use bash with sed or awk.
150
+ - To create files, use file(action=write). Do NOT use bash with echo or heredocs.
151
+ - To search file contents, use grep(action=search). Do NOT use bash with grep or rg.
152
+ - To find files by name/pattern, use grep(action=find). Do NOT use bash with find or ls.
153
+ - To browse directories, use file(action=list). Do NOT use bash with ls or tree.
154
+ - Use bash only for running commands: tests, builds, installs, servers, and system operations.
155
+ - Use git for all version control operations — not bash with git commands.
156
+ - If the user mentions a file vaguely, search for it with grep(action=find) before anything else.
157
+
158
+ # Working with code
159
+ - Always read a file before editing it. Never propose changes to code you haven't read.
160
+ - Do not create new files unless necessary — prefer editing existing files.
161
+ - Do what was asked, nothing more. Don't add features, refactor surrounding code, add comments, or make "improvements" beyond the request.
162
+ - Don't add error handling or validation for scenarios that can't happen. Trust internal code.
163
+ - Be careful not to introduce security vulnerabilities (XSS, SQL injection, command injection). If you notice insecure code you wrote, fix it immediately.
164
+
165
+ # Error recovery
166
+ - If a tool fails, diagnose why before switching tactics. Read the error, check your assumptions, try a focused fix.
167
+ - Don't retry the identical action. Don't abandon a viable approach after a single failure either.
168
+ - If file(action=edit) fails with "not found", re-read the file to verify exact content.
169
+ - If bash returns a non-zero exit code, read the stderr output to understand the failure.
170
+ - Ask the user only when genuinely stuck after investigation, not as a first response to friction.
171
+
172
+ # Git workflow
173
+ - Read the diff before committing. Write concise commit messages that explain why, not what.
174
+ - Never amend commits or force-push without the user asking.
175
+ - Never commit files that contain secrets (.env, credentials, keys).
176
+
177
+ # Output
178
+ - Be concise. Short responses, no filler, no trailing summaries of what you just did.
179
+ - When referencing code, include the file path and relevant context.
180
+ - Only use markdown formatting when it aids clarity.
181
+
182
+ # Verification
183
+ - After making changes, verify they work: run tests, check for syntax errors, or start the dev server as appropriate.
184
+ - If a task involves multiple steps, verify each step before proceeding to the next.
185
+
186
+ # Tools available
187
+ - file(read/list/write/edit) — view, browse, create, and edit files
188
+ - grep(find) — locate files by glob · grep(search) — search content by regex
189
+ - bash — run shell commands, tests, installs, builds
190
+ - git — status, branch, commit, push
191
+ - memory(save) — persist facts across sessions (global or project tier)
192
+ - plan — create and manage multi-step task plans
193
+ - web(fetch/search) — fetch pages or search the web
194
+ - skill — activate a skill by name`;
195
+ this.maxIterations = config.maxIterations || 25;
196
+ this.cwd = config.cwd || process.cwd();
197
+ this.confirmToolCall = config.confirmToolCall ?? null;
198
+ // Discover skills
199
+ this.allSkills = discoverSkills(this.cwd);
200
+ // Register built-in tools
201
+ this.tools = new Map();
202
+ this.addTool(bashToolFactory(this.cwd));
203
+ this.addTool(memoryTool(this.cwd));
204
+ this.addTool(planTool(this.cwd));
205
+ this.addTool(gitTool(this.cwd));
206
+ this.addTool(grepTool(this.cwd));
207
+ this.addTool(fileTool(this.cwd));
208
+ this.addTool(webTool());
209
+ if (this.allSkills.length > 0)
210
+ this.addTool(skillTool(this));
211
+ this.builtinToolNames = new Set(this.tools.keys());
212
+ for (const t of config.extraTools ?? [])
213
+ this.addTool(t);
214
+ // Context window tracking
215
+ this.contextTracker = new ContextTracker(this.model);
216
+ // Cache context and skill catalog (invalidated on skill activation/refresh)
217
+ this.refreshCache();
218
+ // Load recent conversation history for continuity
219
+ this.messages = loadHistory(this.cwd);
220
+ }
221
+ addTool(tool) {
222
+ const name = tool.function.name;
223
+ if (this.builtinToolNames.size > 0 && this.builtinToolNames.has(name)) {
224
+ process.stderr.write(`${C.yellow}Warning: tool "${name}" overwrites built-in tool${C.reset}\n`);
225
+ }
226
+ this.tools.set(name, tool);
227
+ }
228
+ refreshCache() {
229
+ this.cachedContext = loadContext(this.cwd);
230
+ this.cachedCatalog = buildSkillCatalog(this.allSkills);
231
+ this.cachedAlwaysOn = getAlwaysOnContent(this.allSkills);
232
+ }
233
+ getProjectListing() {
234
+ const MAX_ENTRIES = 30;
235
+ const IGNORE = new Set(['.git', 'node_modules', 'dist', 'build', '.next', '.cache', '__pycache__']);
236
+ try {
237
+ const entries = readdirSync(this.cwd, { withFileTypes: true });
238
+ const lines = [];
239
+ for (const e of entries) {
240
+ if (lines.length >= MAX_ENTRIES) {
241
+ lines.push(` ... (${entries.length - MAX_ENTRIES} more)`);
242
+ break;
243
+ }
244
+ if (IGNORE.has(e.name))
245
+ continue;
246
+ if (e.name.startsWith('.') && e.name !== '.')
247
+ continue;
248
+ if (e.isDirectory()) {
249
+ lines.push(` [dir] ${e.name}/`);
250
+ }
251
+ else {
252
+ try {
253
+ const s = statSync(join(this.cwd, e.name));
254
+ const kb = (s.size / 1024).toFixed(1);
255
+ lines.push(` ${e.name} (${kb}KB)`);
256
+ }
257
+ catch {
258
+ lines.push(` ${e.name}`);
259
+ }
260
+ }
261
+ }
262
+ return lines.length > 0 ? `Project files (${this.cwd}):\n${lines.join('\n')}` : '';
263
+ }
264
+ catch {
265
+ return '';
266
+ }
267
+ }
268
+ get systemPrompt() {
269
+ const parts = [this.baseSystemPrompt];
270
+ parts.push(getEnvironmentContext(this.cwd));
271
+ const listing = this.getProjectListing();
272
+ if (listing)
273
+ parts.push(listing);
274
+ if (this.cachedContext)
275
+ parts.push(this.cachedContext);
276
+ if (this.cachedCatalog)
277
+ parts.push(this.cachedCatalog);
278
+ if (this.cachedAlwaysOn)
279
+ parts.push(this.cachedAlwaysOn);
280
+ if (this.activeSkillContent.length > 0) {
281
+ parts.push(this.activeSkillContent.join('\n\n'));
282
+ }
283
+ return parts.join('\n\n');
284
+ }
285
+ async activateSkill(name) {
286
+ const skill = this.allSkills.find(s => s.name === name);
287
+ if (!skill)
288
+ return `Skill "${name}" not found. Available: ${this.allSkills.map(s => s.name).join(', ')}`;
289
+ if (this.activeSkillContent.some(c => c.includes(`name="${name}"`))) {
290
+ return `Skill "${name}" is already active.`;
291
+ }
292
+ this.activeSkillContent.push(`<skill name="${name}">\n${skill.content}\n</skill>`);
293
+ this.refreshCache();
294
+ // Load tools if skill has them
295
+ if (skill.hasTools) {
296
+ const tools = await loadSkillTools(skill.dir);
297
+ for (const t of tools)
298
+ this.addTool(t);
299
+ if (tools.length > 0) {
300
+ return `Skill "${name}" activated with ${tools.length} tool(s): ${tools.map(t => t.function.name).join(', ')}`;
301
+ }
302
+ }
303
+ return `Skill "${name}" activated. Instructions loaded.`;
304
+ }
305
+ async compactConversation() {
306
+ const minMessages = COMPACT_HEAD_KEEP + COMPACT_TAIL_KEEP + 4;
307
+ if (this.messages.length <= minMessages)
308
+ return;
309
+ const head = this.messages.slice(0, COMPACT_HEAD_KEEP);
310
+ const middle = this.messages.slice(COMPACT_HEAD_KEEP, -COMPACT_TAIL_KEEP);
311
+ const tail = this.messages.slice(-COMPACT_TAIL_KEEP);
312
+ // Format middle messages for summarization, capping total size
313
+ let totalChars = 0;
314
+ const formatted = [];
315
+ for (const m of middle) {
316
+ let line;
317
+ if (m.tool_calls?.length) {
318
+ const names = m.tool_calls.map(tc => tc.function.name).join(', ');
319
+ line = `[assistant]: (tool call: ${names})`;
320
+ }
321
+ else if (m.role === 'tool') {
322
+ line = `[tool result]: ${(m.content || '').slice(0, COMPACT_MSG_CHARS)}`;
323
+ }
324
+ else {
325
+ line = `[${m.role}]: ${(m.content || '').slice(0, COMPACT_MSG_CHARS)}`;
326
+ }
327
+ if (totalChars + line.length > COMPACT_TOTAL_CHARS)
328
+ break;
329
+ totalChars += line.length;
330
+ formatted.push(line);
331
+ }
332
+ const stopSpinner = startSpinner('compacting context');
333
+ let stopped = false;
334
+ try {
335
+ const res = await fetch(`${this.baseURL}/chat/completions`, {
336
+ method: 'POST',
337
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` },
338
+ body: JSON.stringify({
339
+ model: this.model,
340
+ messages: [
341
+ { role: 'system', content: COMPACTION_PROMPT },
342
+ { role: 'user', content: formatted.join('\n\n') }
343
+ ]
344
+ })
345
+ });
346
+ if (!res.ok)
347
+ throw new Error(`API ${res.status}: ${await res.text()}`);
348
+ const body = await res.json();
349
+ const summary = body.choices?.[0]?.message?.content;
350
+ if (!summary)
351
+ throw new Error('No summary returned');
352
+ const summaryMsg = {
353
+ role: 'user',
354
+ content: `[Conversation compacted — summary of ${middle.length} earlier messages]\n\n${summary}`
355
+ };
356
+ this.messages = [...head, summaryMsg, ...tail];
357
+ appendHistory(summaryMsg, this.cwd);
358
+ // Re-estimate context usage from the compacted messages
359
+ const compactedChars = this.messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0)
360
+ + this.systemPrompt.length;
361
+ this.contextTracker.estimateFromChars(compactedChars);
362
+ stopSpinner();
363
+ stopped = true;
364
+ process.stderr.write(` ${C.yellow}Context compacted: ${middle.length} messages → summary${C.reset}\n`);
365
+ }
366
+ finally {
367
+ if (!stopped)
368
+ stopSpinner();
369
+ }
370
+ }
371
+ async chat(content) {
372
+ const userMessage = { role: 'user', content };
373
+ this.messages.push(userMessage);
374
+ appendHistory(userMessage, this.cwd);
375
+ // Cap in-memory messages to prevent unbounded growth
376
+ if (this.messages.length > MAX_MESSAGES) {
377
+ this.messages = this.messages.slice(-MAX_MESSAGES);
378
+ }
379
+ for (let i = 0; i < this.maxIterations; i++) {
380
+ const iterLabel = this.maxIterations > 1 ? ` [${i + 1}/${this.maxIterations}]` : '';
381
+ const stopSpinner = startSpinner(`thinking${iterLabel}`);
382
+ let msg;
383
+ try {
384
+ const res = await fetch(`${this.baseURL}/chat/completions`, {
385
+ method: 'POST',
386
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` },
387
+ body: JSON.stringify({
388
+ model: this.model,
389
+ messages: [{ role: 'system', content: this.systemPrompt }, ...this.messages],
390
+ tools: Array.from(this.tools.values()).map(t => ({ type: t.type, function: t.function })),
391
+ tool_choice: 'auto'
392
+ })
393
+ });
394
+ if (!res.ok)
395
+ throw new Error(`API ${res.status}: ${await res.text()}`);
396
+ const body = await res.json();
397
+ if (!body || typeof body !== 'object' || !Array.isArray(body.choices) || !body.choices[0]?.message) {
398
+ throw new Error(`Unexpected API response shape: ${JSON.stringify(body).slice(0, 200)}`);
399
+ }
400
+ msg = body.choices[0].message;
401
+ if (!msg)
402
+ throw new Error('No response from model');
403
+ if (body.usage)
404
+ this.contextTracker.update(body.usage);
405
+ }
406
+ finally {
407
+ stopSpinner();
408
+ }
409
+ // Compact conversation if approaching context limit
410
+ if (!this.compactionInProgress && this.contextTracker.shouldCompact(COMPACT_THRESHOLD)) {
411
+ this.compactionInProgress = true;
412
+ try {
413
+ await this.compactConversation();
414
+ }
415
+ catch (e) {
416
+ process.stderr.write(` ${C.dim}Compaction failed: ${e} — falling back to truncation${C.reset}\n`);
417
+ // Fallback: simple truncation to prevent context overflow
418
+ const keep = COMPACT_HEAD_KEEP + COMPACT_TAIL_KEEP;
419
+ if (this.messages.length > keep) {
420
+ const head = this.messages.slice(0, COMPACT_HEAD_KEEP);
421
+ const tail = this.messages.slice(-COMPACT_TAIL_KEEP);
422
+ const truncMsg = {
423
+ role: 'user',
424
+ content: `[Conversation truncated — ${this.messages.length - keep} older messages removed to stay within context limit]`
425
+ };
426
+ this.messages = [...head, truncMsg, ...tail];
427
+ }
428
+ }
429
+ finally {
430
+ this.compactionInProgress = false;
431
+ }
432
+ }
433
+ this.messages.push(msg);
434
+ if (!msg.tool_calls?.length) {
435
+ appendHistory({ role: 'assistant', content: msg.content || '' }, this.cwd);
436
+ return msg.content || '';
437
+ }
438
+ // Execute tool calls in parallel
439
+ const toolPromises = msg.tool_calls.map(async (call) => {
440
+ const tool = this.tools.get(call.function.name);
441
+ if (!tool) {
442
+ return { call, content: `Error: unknown tool "${call.function.name}"` };
443
+ }
444
+ let args;
445
+ try {
446
+ args = JSON.parse(call.function.arguments || '{}');
447
+ }
448
+ catch {
449
+ return { call, content: 'Error: malformed tool arguments' };
450
+ }
451
+ // Permission check
452
+ if (this.confirmToolCall && !isReadOnlyToolCall(call.function.name, args)) {
453
+ const decision = await this.confirmToolCall(call.function.name, args);
454
+ if (decision === 'deny') {
455
+ return { call, content: 'Tool call denied by user.' };
456
+ }
457
+ }
458
+ const summary = args.command ?? args.action ?? JSON.stringify(args).slice(0, 80);
459
+ const stopToolSpinner = startSpinner(`[${call.function.name}] ${summary}`);
460
+ try {
461
+ const rawResult = await tool.execute(args);
462
+ const result = truncateToolResult(rawResult);
463
+ stopToolSpinner();
464
+ const isError = result.startsWith('Error:') || result.startsWith('EXIT ');
465
+ const icon = isError ? `${C.red}✗` : `${C.green}✓`;
466
+ const preview = result.slice(0, 150).split('\n')[0];
467
+ process.stderr.write(` ${icon} ${C.dim}[${call.function.name}]${C.reset} ${C.dim}${preview}${result.length > 150 ? '...' : ''}${C.reset}\n`);
468
+ return { call, content: result };
469
+ }
470
+ catch (error) {
471
+ stopToolSpinner();
472
+ const errMsg = `Tool error: ${error}`;
473
+ process.stderr.write(` ${C.red}✗ [${call.function.name}] ${errMsg}${C.reset}\n`);
474
+ return { call, content: errMsg };
475
+ }
476
+ });
477
+ const results = await Promise.all(toolPromises);
478
+ for (const { call, content } of results) {
479
+ this.messages.push({ role: 'tool', tool_call_id: call.id, content });
480
+ }
481
+ }
482
+ return MAX_ITERATIONS_REACHED;
483
+ }
484
+ async *chatStream(content) {
485
+ const userMessage = { role: 'user', content };
486
+ this.messages.push(userMessage);
487
+ appendHistory(userMessage, this.cwd);
488
+ if (this.messages.length > MAX_MESSAGES) {
489
+ this.messages = this.messages.slice(-MAX_MESSAGES);
490
+ }
491
+ for (let i = 0; i < this.maxIterations; i++) {
492
+ const iterLabel = this.maxIterations > 1 ? ` [${i + 1}/${this.maxIterations}]` : '';
493
+ yield { type: 'thinking', content: `thinking${iterLabel}` };
494
+ // Compact if needed
495
+ if (!this.compactionInProgress && this.contextTracker.shouldCompact(COMPACT_THRESHOLD)) {
496
+ this.compactionInProgress = true;
497
+ try {
498
+ await this.compactConversation();
499
+ }
500
+ catch (e) {
501
+ const keep = COMPACT_HEAD_KEEP + COMPACT_TAIL_KEEP;
502
+ if (this.messages.length > keep) {
503
+ const head = this.messages.slice(0, COMPACT_HEAD_KEEP);
504
+ const tail = this.messages.slice(-COMPACT_TAIL_KEEP);
505
+ const truncMsg = {
506
+ role: 'user',
507
+ content: `[Conversation truncated — ${this.messages.length - keep} older messages removed to stay within context limit]`
508
+ };
509
+ this.messages = [...head, truncMsg, ...tail];
510
+ }
511
+ }
512
+ finally {
513
+ this.compactionInProgress = false;
514
+ }
515
+ }
516
+ const res = await fetch(`${this.baseURL}/chat/completions`, {
517
+ method: 'POST',
518
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` },
519
+ body: JSON.stringify({
520
+ model: this.model,
521
+ messages: [{ role: 'system', content: this.systemPrompt }, ...this.messages],
522
+ tools: Array.from(this.tools.values()).map(t => ({ type: t.type, function: t.function })),
523
+ tool_choice: 'auto',
524
+ stream: true
525
+ })
526
+ });
527
+ if (!res.ok)
528
+ throw new Error(`API ${res.status}: ${await res.text()}`);
529
+ if (!res.body)
530
+ throw new Error('No response body for streaming');
531
+ // Parse SSE stream
532
+ let assistantContent = '';
533
+ const toolCalls = new Map();
534
+ let usage = null;
535
+ const reader = res.body.getReader();
536
+ const decoder = new TextDecoder();
537
+ let buffer = '';
538
+ while (true) {
539
+ const { done, value } = await reader.read();
540
+ if (done)
541
+ break;
542
+ buffer += decoder.decode(value, { stream: true });
543
+ const lines = buffer.split('\n');
544
+ buffer = lines.pop() || '';
545
+ for (const line of lines) {
546
+ if (!line.startsWith('data: '))
547
+ continue;
548
+ const data = line.slice(6).trim();
549
+ if (data === '[DONE]')
550
+ continue;
551
+ let parsed;
552
+ try {
553
+ parsed = JSON.parse(data);
554
+ }
555
+ catch {
556
+ continue;
557
+ }
558
+ const delta = parsed.choices?.[0]?.delta;
559
+ if (!delta) {
560
+ if (parsed.usage)
561
+ usage = parsed.usage;
562
+ continue;
563
+ }
564
+ // Text content
565
+ if (delta.content) {
566
+ assistantContent += delta.content;
567
+ yield { type: 'text', content: delta.content };
568
+ }
569
+ // Tool calls (streamed incrementally)
570
+ if (delta.tool_calls) {
571
+ // Signal spinner on first tool_call delta so the user sees activity
572
+ if (toolCalls.size === 0) {
573
+ yield { type: 'thinking', content: 'running tools' };
574
+ }
575
+ for (const tc of delta.tool_calls) {
576
+ const idx = tc.index ?? 0;
577
+ if (!toolCalls.has(idx)) {
578
+ toolCalls.set(idx, { id: tc.id || '', name: tc.function?.name || '', arguments: '' });
579
+ }
580
+ const entry = toolCalls.get(idx);
581
+ if (tc.id)
582
+ entry.id = tc.id;
583
+ if (tc.function?.name)
584
+ entry.name = tc.function.name;
585
+ if (tc.function?.arguments)
586
+ entry.arguments += tc.function.arguments;
587
+ }
588
+ }
589
+ }
590
+ }
591
+ if (usage)
592
+ this.contextTracker.update(usage);
593
+ // Build assistant message
594
+ const msg = { role: 'assistant', content: assistantContent || null };
595
+ if (toolCalls.size > 0) {
596
+ msg.tool_calls = Array.from(toolCalls.values()).map(tc => ({
597
+ id: tc.id,
598
+ type: 'function',
599
+ function: { name: tc.name, arguments: tc.arguments }
600
+ }));
601
+ }
602
+ this.messages.push(msg);
603
+ if (!msg.tool_calls?.length) {
604
+ appendHistory({ role: 'assistant', content: assistantContent }, this.cwd);
605
+ yield { type: 'done', content: assistantContent };
606
+ return;
607
+ }
608
+ // Notify consumers that tools are about to run (shows spinner immediately)
609
+ for (const call of msg.tool_calls) {
610
+ const args = (() => { try {
611
+ return JSON.parse(call.function.arguments || '{}');
612
+ }
613
+ catch {
614
+ return {};
615
+ } })();
616
+ const summary = args.command ?? args.action ?? call.function.name;
617
+ yield { type: 'tool_start', toolName: call.function.name, toolCallId: call.id, content: summary };
618
+ }
619
+ // Execute tool calls in parallel
620
+ const execPromises = msg.tool_calls.map(async (call) => {
621
+ const tool = this.tools.get(call.function.name);
622
+ if (!tool)
623
+ return { call, content: `Error: unknown tool "${call.function.name}"`, isError: true };
624
+ let args;
625
+ try {
626
+ args = JSON.parse(call.function.arguments || '{}');
627
+ }
628
+ catch {
629
+ return { call, content: 'Error: malformed tool arguments', isError: true };
630
+ }
631
+ // Permission check
632
+ if (this.confirmToolCall && !isReadOnlyToolCall(call.function.name, args)) {
633
+ const decision = await this.confirmToolCall(call.function.name, args);
634
+ if (decision === 'deny') {
635
+ return { call, content: 'Tool call denied by user.', isError: true };
636
+ }
637
+ }
638
+ try {
639
+ const rawResult = await tool.execute(args);
640
+ const result = truncateToolResult(rawResult);
641
+ const isError = result.startsWith('Error:') || result.startsWith('EXIT ');
642
+ return { call, content: result, isError };
643
+ }
644
+ catch (error) {
645
+ return { call, content: `Tool error: ${error}`, isError: true };
646
+ }
647
+ });
648
+ const execResults = await Promise.all(execPromises);
649
+ for (const r of execResults) {
650
+ this.messages.push({ role: 'tool', tool_call_id: r.call.id, content: r.content });
651
+ yield { type: 'tool_end', toolName: r.call.function.name, toolCallId: r.call.id, content: r.content, success: !r.isError };
652
+ }
653
+ }
654
+ yield { type: 'max_iterations' };
655
+ }
656
+ // ── Public API ─────────────────────────────────────────────────────────────
657
+ getTools() {
658
+ return Array.from(this.tools.values()).map(t => ({ name: t.function.name, description: t.function.description }));
659
+ }
660
+ getModel() { return this.model; }
661
+ getBaseURL() { return this.baseURL; }
662
+ setModel(model) { this.model = model; this.contextTracker = new ContextTracker(model); }
663
+ setConfirmToolCall(cb) { this.confirmToolCall = cb; }
664
+ async compactNow() { await this.compactConversation(); }
665
+ async fetchModels(query) {
666
+ const res = await fetch(`${this.baseURL}/models`, {
667
+ headers: { 'Authorization': `Bearer ${this.apiKey}` }
668
+ });
669
+ if (!res.ok)
670
+ throw new Error(`Failed to fetch models: ${res.status}`);
671
+ const data = await res.json();
672
+ let models = data.data || [];
673
+ if (query) {
674
+ const q = query.toLowerCase();
675
+ models = models.filter(m => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q));
676
+ }
677
+ return models;
678
+ }
679
+ getSkills() { return this.allSkills; }
680
+ getActiveSkillNames() {
681
+ const names = [];
682
+ for (const c of this.activeSkillContent) {
683
+ const match = c.match(/name="([^"]+)"/);
684
+ if (match)
685
+ names.push(match[1]);
686
+ }
687
+ return names;
688
+ }
689
+ refreshSkills() { this.allSkills = discoverSkills(this.cwd); this.refreshCache(); }
690
+ getStats() { return getStats(this.cwd); }
691
+ getPaths() { return paths(this.cwd); }
692
+ getGlobalMemory() { return loadGlobalMemory(this.cwd); }
693
+ getProjectMemory() { return loadProjectMemory(this.cwd); }
694
+ getPlan() { return loadPlan(this.cwd); }
695
+ getPlanByName(name) { return loadPlanByName(name, this.cwd); }
696
+ getPlans() { return listPlans(this.cwd); }
697
+ setGlobalMemory(content) { saveGlobalMemory(content, this.cwd); }
698
+ setProjectMemory(content) { saveProjectMemory(content, this.cwd); }
699
+ setPlan(content, name) { savePlan(content, name, this.cwd); }
700
+ appendToPlan(content) { appendPlan(content, this.cwd); }
701
+ activatePlan(name) { setActivePlan(name, this.cwd); }
702
+ getActivePlanName() { return getActivePlanName(this.cwd); }
703
+ clearProject() { this.messages = []; this.contextTracker.reset(); clearProject(this.cwd); }
704
+ clearAll() { this.messages = []; this.contextTracker.reset(); clearAll(this.cwd); }
705
+ // ── Context tracking ───────────────────────────────────────────────────
706
+ getContextUsage() { return this.contextTracker.format(); }
707
+ getContextDetails() { return this.contextTracker.formatDetailed(); }
708
+ getContextTracker() { return this.contextTracker; }
709
+ getSystemPromptSize() { return this.systemPrompt.length; }
710
+ }
711
+ //# sourceMappingURL=agent.js.map