cliskill 1.0.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.
- package/LICENSE +21 -0
- package/README.md +470 -0
- package/dist/bootstrap/cli.d.ts +21 -0
- package/dist/bootstrap/cli.js +9 -0
- package/dist/bootstrap/cli.js.map +1 -0
- package/dist/chunk-AJENHWD3.js +103 -0
- package/dist/chunk-AJENHWD3.js.map +1 -0
- package/dist/chunk-ULZHJVWD.js +9945 -0
- package/dist/chunk-ULZHJVWD.js.map +1 -0
- package/dist/index.d.ts +2018 -0
- package/dist/index.js +1658 -0
- package/dist/index.js.map +1 -0
- package/dist/paths-OODUHG6V.js +39 -0
- package/dist/paths-OODUHG6V.js.map +1 -0
- package/package.json +86 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1658 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AdapterRegistry,
|
|
3
|
+
AgentTool,
|
|
4
|
+
ContextCompactor,
|
|
5
|
+
CostTracker,
|
|
6
|
+
DEFAULT_MEMORY_DIR,
|
|
7
|
+
EnhancedMemoryStore,
|
|
8
|
+
EnterPlanModeTool,
|
|
9
|
+
ExitPlanModeTool,
|
|
10
|
+
GenericCompatAdapter,
|
|
11
|
+
InkApp,
|
|
12
|
+
LspTool,
|
|
13
|
+
MessageList,
|
|
14
|
+
QueryEngine,
|
|
15
|
+
Spinner,
|
|
16
|
+
StatusBar,
|
|
17
|
+
StreamingToolExecutor,
|
|
18
|
+
ToolRegistry,
|
|
19
|
+
WebSearchTool,
|
|
20
|
+
analyzeTokenBudget,
|
|
21
|
+
createDefaultToolRegistry,
|
|
22
|
+
estimateMessagesTokens,
|
|
23
|
+
estimateTokens,
|
|
24
|
+
formatAnsiMessage,
|
|
25
|
+
renderInkApp,
|
|
26
|
+
runAgentLoop,
|
|
27
|
+
runCli
|
|
28
|
+
} from "./chunk-ULZHJVWD.js";
|
|
29
|
+
import {
|
|
30
|
+
getGlobalSkillsDir,
|
|
31
|
+
getHistoryPath
|
|
32
|
+
} from "./chunk-AJENHWD3.js";
|
|
33
|
+
|
|
34
|
+
// src/memory/store.ts
|
|
35
|
+
import { readFile, writeFile, mkdir, readdir, stat, unlink } from "fs/promises";
|
|
36
|
+
import { join, resolve } from "path";
|
|
37
|
+
import { existsSync } from "fs";
|
|
38
|
+
var DEFAULT_CONFIG = {
|
|
39
|
+
memoryDir: DEFAULT_MEMORY_DIR,
|
|
40
|
+
maxEntrypointSize: 25e3,
|
|
41
|
+
maxTopicFiles: 50
|
|
42
|
+
};
|
|
43
|
+
var MemoryStore = class {
|
|
44
|
+
config;
|
|
45
|
+
rootDir;
|
|
46
|
+
cache = /* @__PURE__ */ new Map();
|
|
47
|
+
loaded = false;
|
|
48
|
+
constructor(projectRoot, config) {
|
|
49
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
50
|
+
this.rootDir = resolve(projectRoot, this.config.memoryDir);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Initialize the memory directory structure.
|
|
54
|
+
*/
|
|
55
|
+
async init() {
|
|
56
|
+
await mkdir(join(this.rootDir, "topics"), { recursive: true });
|
|
57
|
+
const entrypointPath = join(this.rootDir, "MEMORY.md");
|
|
58
|
+
if (!existsSync(entrypointPath)) {
|
|
59
|
+
await writeFile(
|
|
60
|
+
entrypointPath,
|
|
61
|
+
"# Project Memory\n\nThis file contains persistent memory for the AI assistant.\n",
|
|
62
|
+
"utf-8"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
await this.load();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load all memories from disk into cache.
|
|
69
|
+
*/
|
|
70
|
+
async load() {
|
|
71
|
+
this.cache.clear();
|
|
72
|
+
const topicsDir = join(this.rootDir, "topics");
|
|
73
|
+
if (existsSync(topicsDir)) {
|
|
74
|
+
const files = await readdir(topicsDir);
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
if (file.endsWith(".md")) {
|
|
77
|
+
const topic = file.replace(".md", "");
|
|
78
|
+
const filePath = join(topicsDir, file);
|
|
79
|
+
const content = await readFile(filePath, "utf-8");
|
|
80
|
+
const fileStat = await stat(filePath);
|
|
81
|
+
this.cache.set(topic, {
|
|
82
|
+
id: topic,
|
|
83
|
+
topic,
|
|
84
|
+
content,
|
|
85
|
+
createdAt: fileStat.birthtimeMs,
|
|
86
|
+
lastAccessedAt: fileStat.mtimeMs
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
this.loaded = true;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get a memory by topic.
|
|
95
|
+
*/
|
|
96
|
+
get(topic) {
|
|
97
|
+
return this.cache.get(topic);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get all memories.
|
|
101
|
+
*/
|
|
102
|
+
getAll() {
|
|
103
|
+
return Array.from(this.cache.values());
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Save a memory entry to disk.
|
|
107
|
+
*/
|
|
108
|
+
async set(topic, content) {
|
|
109
|
+
const filePath = join(this.rootDir, "topics", `${topic}.md`);
|
|
110
|
+
await mkdir(join(this.rootDir, "topics"), { recursive: true });
|
|
111
|
+
await writeFile(filePath, content, "utf-8");
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const existing = this.cache.get(topic);
|
|
114
|
+
this.cache.set(topic, {
|
|
115
|
+
id: topic,
|
|
116
|
+
topic,
|
|
117
|
+
content,
|
|
118
|
+
createdAt: existing?.createdAt ?? now,
|
|
119
|
+
lastAccessedAt: now
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Delete a memory by topic.
|
|
124
|
+
*/
|
|
125
|
+
async delete(topic) {
|
|
126
|
+
const filePath = join(this.rootDir, "topics", `${topic}.md`);
|
|
127
|
+
if (existsSync(filePath)) {
|
|
128
|
+
await unlink(filePath);
|
|
129
|
+
this.cache.delete(topic);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Search memories by keyword.
|
|
136
|
+
*/
|
|
137
|
+
search(query) {
|
|
138
|
+
const lowerQuery = query.toLowerCase();
|
|
139
|
+
return this.getAll().filter(
|
|
140
|
+
(entry) => entry.content.toLowerCase().includes(lowerQuery) || entry.topic.toLowerCase().includes(lowerQuery)
|
|
141
|
+
).sort((a, b) => b.lastAccessedAt - a.lastAccessedAt);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get the entrypoint (MEMORY.md) content.
|
|
145
|
+
*/
|
|
146
|
+
async getEntrypoint() {
|
|
147
|
+
const entrypointPath = join(this.rootDir, "MEMORY.md");
|
|
148
|
+
if (existsSync(entrypointPath)) {
|
|
149
|
+
return readFile(entrypointPath, "utf-8");
|
|
150
|
+
}
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Update the entrypoint (MEMORY.md) content.
|
|
155
|
+
*/
|
|
156
|
+
async setEntrypoint(content) {
|
|
157
|
+
if (content.length > this.config.maxEntrypointSize) {
|
|
158
|
+
content = content.slice(0, this.config.maxEntrypointSize) + "\n\n... (truncated)";
|
|
159
|
+
}
|
|
160
|
+
const entrypointPath = join(this.rootDir, "MEMORY.md");
|
|
161
|
+
await writeFile(entrypointPath, content, "utf-8");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Build a context string from relevant memories for inclusion in system prompt.
|
|
165
|
+
*/
|
|
166
|
+
async buildContext(query, maxTokens = 2e3) {
|
|
167
|
+
let memories;
|
|
168
|
+
if (query) {
|
|
169
|
+
memories = this.search(query);
|
|
170
|
+
} else {
|
|
171
|
+
memories = this.getAll().sort((a, b) => b.lastAccessedAt - a.lastAccessedAt);
|
|
172
|
+
}
|
|
173
|
+
const entrypoint = await this.getEntrypoint();
|
|
174
|
+
let context = "";
|
|
175
|
+
if (entrypoint) {
|
|
176
|
+
context += `## Project Memory
|
|
177
|
+
${entrypoint}
|
|
178
|
+
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
for (const memory of memories) {
|
|
182
|
+
const addition = `### ${memory.topic}
|
|
183
|
+
${memory.content.slice(0, 500)}
|
|
184
|
+
|
|
185
|
+
`;
|
|
186
|
+
if ((context.length + addition.length) / 4 > maxTokens) break;
|
|
187
|
+
context += addition;
|
|
188
|
+
}
|
|
189
|
+
return context.trim();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/extensions/skill-loader.ts
|
|
194
|
+
import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
|
|
195
|
+
import { join as join2 } from "path";
|
|
196
|
+
import { existsSync as existsSync2 } from "fs";
|
|
197
|
+
async function loadSkillsFromDir(dir) {
|
|
198
|
+
if (!existsSync2(dir)) return [];
|
|
199
|
+
const skills = [];
|
|
200
|
+
const files = await readdir2(dir);
|
|
201
|
+
for (const file of files) {
|
|
202
|
+
if (!file.endsWith(".md")) continue;
|
|
203
|
+
const filePath = join2(dir, file);
|
|
204
|
+
const content = await readFile2(filePath, "utf-8");
|
|
205
|
+
const skill = parseSkillMarkdown(content, filePath);
|
|
206
|
+
if (skill) {
|
|
207
|
+
skills.push(skill);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return skills;
|
|
211
|
+
}
|
|
212
|
+
function parseSkillMarkdown(content, sourcePath) {
|
|
213
|
+
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
214
|
+
if (!frontmatterMatch) {
|
|
215
|
+
const name = sourcePath ? sourcePath.split(/[\\/]/).pop()?.replace(".md", "") ?? "unnamed" : "unnamed";
|
|
216
|
+
return {
|
|
217
|
+
name,
|
|
218
|
+
description: `Skill: ${name}`,
|
|
219
|
+
prompt: content.trim(),
|
|
220
|
+
builtin: false,
|
|
221
|
+
sourcePath
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const [, frontmatter, body] = frontmatterMatch;
|
|
225
|
+
const metadata = parseSimpleYaml(frontmatter);
|
|
226
|
+
if (!metadata.name) {
|
|
227
|
+
metadata.name = sourcePath ? sourcePath.split(/[\\/]/).pop()?.replace(".md", "") ?? "unnamed" : "unnamed";
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
name: metadata.name,
|
|
231
|
+
description: metadata.description ?? `Skill: ${metadata.name}`,
|
|
232
|
+
aliases: metadata.aliases ? String(metadata.aliases).replace(/^\[(.+)\]$/, "$1").split(",").map((s) => s.trim()) : void 0,
|
|
233
|
+
prompt: body.trim(),
|
|
234
|
+
allowedTools: metadata.allowedTools ? String(metadata.allowedTools).replace(/^\[(.+)\]$/, "$1").split(",").map((s) => s.trim()) : void 0,
|
|
235
|
+
builtin: false,
|
|
236
|
+
sourcePath
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function parseSimpleYaml(yaml) {
|
|
240
|
+
const result = {};
|
|
241
|
+
for (const line of yaml.split("\n")) {
|
|
242
|
+
const trimmed = line.trim();
|
|
243
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
244
|
+
const colonIndex = trimmed.indexOf(":");
|
|
245
|
+
if (colonIndex === -1) continue;
|
|
246
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
247
|
+
const value = trimmed.slice(colonIndex + 1).trim();
|
|
248
|
+
result[key] = value;
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/extensions/skill-registry.ts
|
|
254
|
+
import { join as join3 } from "path";
|
|
255
|
+
var SkillRegistry = class {
|
|
256
|
+
skills = /* @__PURE__ */ new Map();
|
|
257
|
+
aliasIndex = /* @__PURE__ */ new Map();
|
|
258
|
+
/** Register a skill */
|
|
259
|
+
register(skill) {
|
|
260
|
+
this.skills.set(skill.name, skill);
|
|
261
|
+
if (skill.aliases) {
|
|
262
|
+
for (const alias of skill.aliases) {
|
|
263
|
+
this.aliasIndex.set(alias.toLowerCase(), skill.name);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Get a skill by name or alias */
|
|
268
|
+
get(nameOrAlias) {
|
|
269
|
+
const aliasTarget = this.aliasIndex.get(nameOrAlias.toLowerCase());
|
|
270
|
+
return this.skills.get(aliasTarget ?? nameOrAlias);
|
|
271
|
+
}
|
|
272
|
+
/** Get all registered skills */
|
|
273
|
+
getAll() {
|
|
274
|
+
return Array.from(this.skills.values());
|
|
275
|
+
}
|
|
276
|
+
/** Check if a skill exists */
|
|
277
|
+
has(name) {
|
|
278
|
+
return this.skills.has(name) || this.aliasIndex.has(name.toLowerCase());
|
|
279
|
+
}
|
|
280
|
+
/** Unregister a skill */
|
|
281
|
+
unregister(name) {
|
|
282
|
+
const skill = this.skills.get(name);
|
|
283
|
+
if (skill?.aliases) {
|
|
284
|
+
for (const alias of skill.aliases) {
|
|
285
|
+
this.aliasIndex.delete(alias.toLowerCase());
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.skills.delete(name);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Load skills from a directory.
|
|
292
|
+
* Returns the number of skills loaded.
|
|
293
|
+
*/
|
|
294
|
+
async loadFromDir(dir) {
|
|
295
|
+
const skills = await loadSkillsFromDir(dir);
|
|
296
|
+
for (const skill of skills) {
|
|
297
|
+
this.register(skill);
|
|
298
|
+
}
|
|
299
|
+
return skills.length;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Load skills from standard locations:
|
|
303
|
+
* 1. ~/.cliskill/skills/ (global user skills)
|
|
304
|
+
* 2. .cliskill/skills/ (project skills)
|
|
305
|
+
*/
|
|
306
|
+
async loadStandardSkills(projectRoot) {
|
|
307
|
+
let count = 0;
|
|
308
|
+
const globalDir = getGlobalSkillsDir();
|
|
309
|
+
count += await this.loadFromDir(globalDir);
|
|
310
|
+
const projectDir = join3(projectRoot, ".cliskill", "skills");
|
|
311
|
+
count += await this.loadFromDir(projectDir);
|
|
312
|
+
return count;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Get skill prompt for injection into system prompt.
|
|
316
|
+
*/
|
|
317
|
+
getSkillPrompt(nameOrAlias) {
|
|
318
|
+
const skill = this.get(nameOrAlias);
|
|
319
|
+
return skill?.prompt;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// src/safety/permissions.ts
|
|
324
|
+
var PermissionManager = class {
|
|
325
|
+
mode;
|
|
326
|
+
rules = [];
|
|
327
|
+
sessionDecisions = /* @__PURE__ */ new Map();
|
|
328
|
+
constructor(mode, rules) {
|
|
329
|
+
this.mode = mode;
|
|
330
|
+
this.rules = rules ?? [];
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Check if a tool operation is allowed.
|
|
334
|
+
*/
|
|
335
|
+
check(toolName, riskLevel, content) {
|
|
336
|
+
if (this.mode === "auto-accept") {
|
|
337
|
+
return { allowed: true, reason: "auto-accept mode" };
|
|
338
|
+
}
|
|
339
|
+
if (this.mode === "plan") {
|
|
340
|
+
if (riskLevel === "readonly") {
|
|
341
|
+
return { allowed: true, reason: "readonly in plan mode" };
|
|
342
|
+
}
|
|
343
|
+
return { allowed: false, reason: `${riskLevel} operations not allowed in plan mode` };
|
|
344
|
+
}
|
|
345
|
+
for (const rule of this.rules) {
|
|
346
|
+
if (this.matchTool(toolName, rule.tool)) {
|
|
347
|
+
if (!rule.contentPattern || content && this.matchContent(content, rule.contentPattern)) {
|
|
348
|
+
return {
|
|
349
|
+
allowed: rule.allow,
|
|
350
|
+
reason: rule.allow ? `Allowed by rule: ${rule.tool}` : `Denied by rule: ${rule.tool}`
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const sessionKey = `${toolName}:${content ?? ""}`;
|
|
356
|
+
const sessionDecision = this.sessionDecisions.get(sessionKey);
|
|
357
|
+
if (sessionDecision !== void 0) {
|
|
358
|
+
return { allowed: sessionDecision, reason: "session decision" };
|
|
359
|
+
}
|
|
360
|
+
return { allowed: false, reason: "No rule matched \u2014 requires user approval" };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Record a user decision for the session.
|
|
364
|
+
*/
|
|
365
|
+
recordDecision(toolName, content, allowed) {
|
|
366
|
+
const key = `${toolName}:${content}`;
|
|
367
|
+
this.sessionDecisions.set(key, allowed);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Add a permission rule.
|
|
371
|
+
*/
|
|
372
|
+
addRule(rule) {
|
|
373
|
+
this.rules.push(rule);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Set the permission mode.
|
|
377
|
+
*/
|
|
378
|
+
setMode(mode) {
|
|
379
|
+
this.mode = mode;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Get the current permission mode.
|
|
383
|
+
*/
|
|
384
|
+
getMode() {
|
|
385
|
+
return this.mode;
|
|
386
|
+
}
|
|
387
|
+
matchTool(toolName, pattern) {
|
|
388
|
+
if (pattern === "*") return true;
|
|
389
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
390
|
+
return regex.test(toolName);
|
|
391
|
+
}
|
|
392
|
+
matchContent(content, pattern) {
|
|
393
|
+
if (pattern.endsWith("*")) {
|
|
394
|
+
return content.startsWith(pattern.slice(0, -1).trim());
|
|
395
|
+
}
|
|
396
|
+
return content.includes(pattern);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// src/safety/command-inspector.ts
|
|
401
|
+
var SAFE_COMMANDS = /* @__PURE__ */ new Set([
|
|
402
|
+
"ls",
|
|
403
|
+
"dir",
|
|
404
|
+
"cat",
|
|
405
|
+
"type",
|
|
406
|
+
"head",
|
|
407
|
+
"tail",
|
|
408
|
+
"less",
|
|
409
|
+
"more",
|
|
410
|
+
"pwd",
|
|
411
|
+
"echo",
|
|
412
|
+
"whoami",
|
|
413
|
+
"hostname",
|
|
414
|
+
"date",
|
|
415
|
+
"uname",
|
|
416
|
+
"git status",
|
|
417
|
+
"git log",
|
|
418
|
+
"git diff",
|
|
419
|
+
"git branch",
|
|
420
|
+
"git show",
|
|
421
|
+
"git remote",
|
|
422
|
+
"node --version",
|
|
423
|
+
"npm --version",
|
|
424
|
+
"npx --version",
|
|
425
|
+
"which",
|
|
426
|
+
"where",
|
|
427
|
+
"env",
|
|
428
|
+
"printenv",
|
|
429
|
+
"grep",
|
|
430
|
+
"find",
|
|
431
|
+
"wc",
|
|
432
|
+
"sort",
|
|
433
|
+
"uniq",
|
|
434
|
+
"diff",
|
|
435
|
+
"ps",
|
|
436
|
+
"top",
|
|
437
|
+
"df",
|
|
438
|
+
"du",
|
|
439
|
+
"free",
|
|
440
|
+
"curl --head",
|
|
441
|
+
"curl -I"
|
|
442
|
+
]);
|
|
443
|
+
var DANGEROUS_PATTERNS = [
|
|
444
|
+
/\brm\s+(-rf?|-fr?)\s+\//,
|
|
445
|
+
/\brm\s+(-rf?|-fr?)\s+\*/,
|
|
446
|
+
/\bdd\s+if=/,
|
|
447
|
+
/\bmkfs\./,
|
|
448
|
+
/\b(format\s+[A-Z]:)/i,
|
|
449
|
+
/>\s*\/dev\//,
|
|
450
|
+
/\bshutdown\b/,
|
|
451
|
+
/\breboot\b/,
|
|
452
|
+
/\binit\s+[06]\b/,
|
|
453
|
+
/\bsudo\s+rm\b/,
|
|
454
|
+
/\bchmod\s+(-R\s+)?000\b/,
|
|
455
|
+
/\bchown\s+(-R\s+)?root\b/
|
|
456
|
+
];
|
|
457
|
+
var FILE_MODIFYING_COMMANDS = /* @__PURE__ */ new Set([
|
|
458
|
+
"touch",
|
|
459
|
+
"mkdir",
|
|
460
|
+
"cp",
|
|
461
|
+
"mv",
|
|
462
|
+
"rm",
|
|
463
|
+
"rmdir",
|
|
464
|
+
"chmod",
|
|
465
|
+
"chown",
|
|
466
|
+
"ln",
|
|
467
|
+
"npm install",
|
|
468
|
+
"npm uninstall",
|
|
469
|
+
"npm update",
|
|
470
|
+
"pip install",
|
|
471
|
+
"pip uninstall",
|
|
472
|
+
"git add",
|
|
473
|
+
"git commit",
|
|
474
|
+
"git push",
|
|
475
|
+
"git merge",
|
|
476
|
+
"git rebase",
|
|
477
|
+
"git reset",
|
|
478
|
+
"git checkout",
|
|
479
|
+
"docker",
|
|
480
|
+
"kubectl"
|
|
481
|
+
]);
|
|
482
|
+
var NETWORK_COMMANDS = /* @__PURE__ */ new Set([
|
|
483
|
+
"curl",
|
|
484
|
+
"wget",
|
|
485
|
+
"ssh",
|
|
486
|
+
"scp",
|
|
487
|
+
"rsync",
|
|
488
|
+
"nc",
|
|
489
|
+
"netcat",
|
|
490
|
+
"ping",
|
|
491
|
+
"traceroute",
|
|
492
|
+
"dig",
|
|
493
|
+
"nslookup",
|
|
494
|
+
"npm publish",
|
|
495
|
+
"git push",
|
|
496
|
+
"git fetch",
|
|
497
|
+
"git pull"
|
|
498
|
+
]);
|
|
499
|
+
function inspectCommand(command) {
|
|
500
|
+
const trimmed = command.trim();
|
|
501
|
+
const baseCommand = trimmed.split(/\s+/)[0] ?? "";
|
|
502
|
+
const fullCommand = trimmed.toLowerCase();
|
|
503
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
504
|
+
if (pattern.test(fullCommand)) {
|
|
505
|
+
return {
|
|
506
|
+
risk: "dangerous",
|
|
507
|
+
reason: "Potentially destructive command detected",
|
|
508
|
+
baseCommand,
|
|
509
|
+
modifiesFiles: true,
|
|
510
|
+
accessesNetwork: false
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const pipeParts = trimmed.split(/\|/).map((p) => p.trim());
|
|
515
|
+
let maxRisk = "safe";
|
|
516
|
+
let modifiesFiles = false;
|
|
517
|
+
let accessesNetwork = false;
|
|
518
|
+
for (const part of pipeParts) {
|
|
519
|
+
const analysis = analyzeSingleCommand(part);
|
|
520
|
+
if (analysis.risk === "dangerous") return analysis;
|
|
521
|
+
if (analysis.risk === "moderate") maxRisk = "moderate";
|
|
522
|
+
if (analysis.modifiesFiles) modifiesFiles = true;
|
|
523
|
+
if (analysis.accessesNetwork) accessesNetwork = true;
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
risk: maxRisk,
|
|
527
|
+
reason: maxRisk === "safe" ? "Readonly or low-risk command" : "Command may modify files or access network",
|
|
528
|
+
baseCommand,
|
|
529
|
+
modifiesFiles,
|
|
530
|
+
accessesNetwork
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function analyzeSingleCommand(command) {
|
|
534
|
+
const trimmed = command.trim();
|
|
535
|
+
const parts = trimmed.split(/\s+/);
|
|
536
|
+
const base = parts[0] ?? "";
|
|
537
|
+
const fullCommand = trimmed.toLowerCase();
|
|
538
|
+
const firstTwoWords = parts.slice(0, 2).join(" ").toLowerCase();
|
|
539
|
+
if (SAFE_COMMANDS.has(fullCommand) || SAFE_COMMANDS.has(firstTwoWords)) {
|
|
540
|
+
return {
|
|
541
|
+
risk: "safe",
|
|
542
|
+
reason: "Known safe command",
|
|
543
|
+
baseCommand: base,
|
|
544
|
+
modifiesFiles: false,
|
|
545
|
+
accessesNetwork: false
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
const modifiesFiles = matchesAny(fullCommand, firstTwoWords, FILE_MODIFYING_COMMANDS);
|
|
549
|
+
const accessesNetwork = matchesAny(fullCommand, firstTwoWords, NETWORK_COMMANDS);
|
|
550
|
+
let risk = "safe";
|
|
551
|
+
if (modifiesFiles || accessesNetwork) risk = "moderate";
|
|
552
|
+
return {
|
|
553
|
+
risk,
|
|
554
|
+
reason: risk === "safe" ? "Low-risk command" : modifiesFiles ? "Modifies filesystem" : "Accesses network",
|
|
555
|
+
baseCommand: base,
|
|
556
|
+
modifiesFiles,
|
|
557
|
+
accessesNetwork
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function matchesAny(fullCommand, firstTwoWords, set) {
|
|
561
|
+
return set.has(fullCommand) || set.has(firstTwoWords) || Array.from(set).some((cmd) => fullCommand.startsWith(cmd));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/services/history.ts
|
|
565
|
+
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
566
|
+
import { existsSync as existsSync3 } from "fs";
|
|
567
|
+
import { dirname } from "path";
|
|
568
|
+
var DEFAULT_CONFIG2 = {
|
|
569
|
+
maxEntries: 1e3,
|
|
570
|
+
filePath: getHistoryPath()
|
|
571
|
+
};
|
|
572
|
+
var HistoryManager = class {
|
|
573
|
+
entries = [];
|
|
574
|
+
config;
|
|
575
|
+
cursorIndex = -1;
|
|
576
|
+
constructor(config) {
|
|
577
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
578
|
+
}
|
|
579
|
+
addEntry(content, type = "user") {
|
|
580
|
+
const trimmed = content.trim();
|
|
581
|
+
if (!trimmed) return;
|
|
582
|
+
const lastEntry = this.entries[this.entries.length - 1];
|
|
583
|
+
if (lastEntry && lastEntry.content === trimmed) return;
|
|
584
|
+
this.entries.push({
|
|
585
|
+
content: trimmed,
|
|
586
|
+
timestamp: Date.now(),
|
|
587
|
+
type
|
|
588
|
+
});
|
|
589
|
+
if (this.entries.length > this.config.maxEntries) {
|
|
590
|
+
this.entries = this.entries.slice(-this.config.maxEntries);
|
|
591
|
+
}
|
|
592
|
+
this.cursorIndex = this.entries.length;
|
|
593
|
+
}
|
|
594
|
+
getEntries() {
|
|
595
|
+
return [...this.entries];
|
|
596
|
+
}
|
|
597
|
+
search(query) {
|
|
598
|
+
const lowerQuery = query.toLowerCase();
|
|
599
|
+
return this.entries.filter((e) => e.content.toLowerCase().includes(lowerQuery));
|
|
600
|
+
}
|
|
601
|
+
getRecent(count) {
|
|
602
|
+
return this.entries.slice(-count);
|
|
603
|
+
}
|
|
604
|
+
clear() {
|
|
605
|
+
this.entries = [];
|
|
606
|
+
this.cursorIndex = -1;
|
|
607
|
+
}
|
|
608
|
+
navigateUp() {
|
|
609
|
+
if (this.entries.length === 0) return null;
|
|
610
|
+
if (this.cursorIndex > 0) {
|
|
611
|
+
this.cursorIndex--;
|
|
612
|
+
} else if (this.cursorIndex === -1 || this.cursorIndex === this.entries.length) {
|
|
613
|
+
this.cursorIndex = this.entries.length - 1;
|
|
614
|
+
}
|
|
615
|
+
return this.entries[this.cursorIndex]?.content ?? null;
|
|
616
|
+
}
|
|
617
|
+
navigateDown() {
|
|
618
|
+
if (this.cursorIndex < this.entries.length - 1) {
|
|
619
|
+
this.cursorIndex++;
|
|
620
|
+
return this.entries[this.cursorIndex]?.content ?? null;
|
|
621
|
+
}
|
|
622
|
+
this.cursorIndex = this.entries.length;
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
async save() {
|
|
626
|
+
const filePath = this.resolvePath();
|
|
627
|
+
const dir = dirname(filePath);
|
|
628
|
+
if (!existsSync3(dir)) {
|
|
629
|
+
await mkdir2(dir, { recursive: true });
|
|
630
|
+
}
|
|
631
|
+
const lines = this.entries.map((e) => JSON.stringify(e)).join("\n");
|
|
632
|
+
await writeFile2(filePath, lines, "utf-8");
|
|
633
|
+
}
|
|
634
|
+
async load() {
|
|
635
|
+
const filePath = this.resolvePath();
|
|
636
|
+
if (!existsSync3(filePath)) {
|
|
637
|
+
this.entries = [];
|
|
638
|
+
this.cursorIndex = -1;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const content = await readFile3(filePath, "utf-8");
|
|
642
|
+
const lines = content.split("\n").filter(Boolean);
|
|
643
|
+
const loaded = [];
|
|
644
|
+
for (const line of lines) {
|
|
645
|
+
try {
|
|
646
|
+
const entry = JSON.parse(line);
|
|
647
|
+
if (entry.content && typeof entry.content === "string" && entry.timestamp) {
|
|
648
|
+
loaded.push({
|
|
649
|
+
content: entry.content,
|
|
650
|
+
timestamp: entry.timestamp,
|
|
651
|
+
type: entry.type ?? "user"
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
} catch {
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
this.entries = loaded.slice(-this.config.maxEntries);
|
|
658
|
+
this.cursorIndex = this.entries.length;
|
|
659
|
+
}
|
|
660
|
+
resolvePath() {
|
|
661
|
+
if (this.config.filePath.startsWith("~")) {
|
|
662
|
+
return getHistoryPath();
|
|
663
|
+
}
|
|
664
|
+
return this.config.filePath;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
// src/hooks/registry.ts
|
|
669
|
+
var HookRegistry = class {
|
|
670
|
+
hooks = /* @__PURE__ */ new Map();
|
|
671
|
+
register(hook) {
|
|
672
|
+
this.hooks.set(hook.id, hook);
|
|
673
|
+
}
|
|
674
|
+
unregister(id) {
|
|
675
|
+
this.hooks.delete(id);
|
|
676
|
+
}
|
|
677
|
+
getHooksForEvent(event) {
|
|
678
|
+
return Array.from(this.hooks.values()).filter(
|
|
679
|
+
(h) => h.event === event && h.enabled
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
getAll() {
|
|
683
|
+
return Array.from(this.hooks.values());
|
|
684
|
+
}
|
|
685
|
+
loadFromConfig(config) {
|
|
686
|
+
if (!config || typeof config !== "object") return;
|
|
687
|
+
const cfg = config;
|
|
688
|
+
const hooksArr = cfg.hooks;
|
|
689
|
+
if (!Array.isArray(hooksArr)) return;
|
|
690
|
+
for (const raw of hooksArr) {
|
|
691
|
+
if (!raw || typeof raw !== "object") continue;
|
|
692
|
+
const def = raw;
|
|
693
|
+
const hook = {
|
|
694
|
+
id: String(def.id ?? `hook_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`),
|
|
695
|
+
event: String(def.event ?? "pre_tool_use"),
|
|
696
|
+
type: def.type === "function" ? "function" : "command",
|
|
697
|
+
command: typeof def.command === "string" ? def.command : void 0,
|
|
698
|
+
handler: typeof def.handler === "string" ? def.handler : void 0,
|
|
699
|
+
condition: def.condition && typeof def.condition === "object" ? def.condition : void 0,
|
|
700
|
+
timeout: typeof def.timeout === "number" ? def.timeout : 3e4,
|
|
701
|
+
enabled: def.enabled !== false
|
|
702
|
+
};
|
|
703
|
+
this.register(hook);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
clear() {
|
|
707
|
+
this.hooks.clear();
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// src/hooks/executor.ts
|
|
712
|
+
import { exec } from "child_process";
|
|
713
|
+
var HookExecutor = class {
|
|
714
|
+
registry;
|
|
715
|
+
constructor(registry) {
|
|
716
|
+
this.registry = registry;
|
|
717
|
+
}
|
|
718
|
+
async executeHooks(event, context) {
|
|
719
|
+
const hooks = this.registry.getHooksForEvent(event);
|
|
720
|
+
const matched = hooks.filter((h) => this.matchesCondition(h, context));
|
|
721
|
+
const results = [];
|
|
722
|
+
for (const hook of matched) {
|
|
723
|
+
const result = await this.executeOne(hook, context);
|
|
724
|
+
results.push(result);
|
|
725
|
+
}
|
|
726
|
+
return results;
|
|
727
|
+
}
|
|
728
|
+
matchesCondition(hook, context) {
|
|
729
|
+
if (!hook.condition) return true;
|
|
730
|
+
if (hook.condition.tool_name && context.toolName) {
|
|
731
|
+
if (hook.condition.tool_name !== context.toolName) return false;
|
|
732
|
+
}
|
|
733
|
+
if (hook.condition.pattern) {
|
|
734
|
+
try {
|
|
735
|
+
const regex = new RegExp(hook.condition.pattern);
|
|
736
|
+
const text = context.toolOutput ?? String(context.toolInput ?? "");
|
|
737
|
+
if (!regex.test(text)) return false;
|
|
738
|
+
} catch {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
executeOne(hook, context) {
|
|
745
|
+
const start = Date.now();
|
|
746
|
+
if (hook.type === "function") {
|
|
747
|
+
return Promise.resolve({
|
|
748
|
+
hookId: hook.id,
|
|
749
|
+
success: true,
|
|
750
|
+
output: `Function handler "${hook.handler}" executed (stub)`,
|
|
751
|
+
duration: Date.now() - start
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return this.executeCommand(hook, context, start);
|
|
755
|
+
}
|
|
756
|
+
executeCommand(hook, context, start) {
|
|
757
|
+
const command = hook.command ?? "";
|
|
758
|
+
const env = {
|
|
759
|
+
...process.env,
|
|
760
|
+
CLISKILL_EVENT: context.event,
|
|
761
|
+
CLISKILL_TOOL_NAME: context.toolName ?? "",
|
|
762
|
+
CLISKILL_WORKING_DIR: context.workingDirectory
|
|
763
|
+
};
|
|
764
|
+
if (context.error) {
|
|
765
|
+
env.CLISKILL_ERROR_MESSAGE = context.error.message;
|
|
766
|
+
}
|
|
767
|
+
return new Promise((resolve2) => {
|
|
768
|
+
exec(
|
|
769
|
+
command,
|
|
770
|
+
{
|
|
771
|
+
timeout: hook.timeout,
|
|
772
|
+
cwd: context.workingDirectory,
|
|
773
|
+
env
|
|
774
|
+
},
|
|
775
|
+
(error, stdout, stderr) => {
|
|
776
|
+
const duration = Date.now() - start;
|
|
777
|
+
if (error) {
|
|
778
|
+
resolve2({
|
|
779
|
+
hookId: hook.id,
|
|
780
|
+
success: false,
|
|
781
|
+
output: stdout?.trim() ?? "",
|
|
782
|
+
error: stderr?.trim() || error.message,
|
|
783
|
+
duration
|
|
784
|
+
});
|
|
785
|
+
} else {
|
|
786
|
+
resolve2({
|
|
787
|
+
hookId: hook.id,
|
|
788
|
+
success: true,
|
|
789
|
+
output: stdout?.trim() ?? "",
|
|
790
|
+
duration
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
);
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// src/utils/bash-parser.ts
|
|
800
|
+
var SAFE_COMMANDS2 = /* @__PURE__ */ new Set([
|
|
801
|
+
"ls",
|
|
802
|
+
"cat",
|
|
803
|
+
"pwd",
|
|
804
|
+
"echo",
|
|
805
|
+
"head",
|
|
806
|
+
"tail",
|
|
807
|
+
"grep",
|
|
808
|
+
"find",
|
|
809
|
+
"wc",
|
|
810
|
+
"sort",
|
|
811
|
+
"uniq",
|
|
812
|
+
"date",
|
|
813
|
+
"whoami",
|
|
814
|
+
"hostname",
|
|
815
|
+
"uname",
|
|
816
|
+
"which",
|
|
817
|
+
"type",
|
|
818
|
+
"file",
|
|
819
|
+
"stat",
|
|
820
|
+
"df",
|
|
821
|
+
"du",
|
|
822
|
+
"ps",
|
|
823
|
+
"top",
|
|
824
|
+
"env",
|
|
825
|
+
"printenv",
|
|
826
|
+
"id",
|
|
827
|
+
"dirname",
|
|
828
|
+
"basename",
|
|
829
|
+
"realpath"
|
|
830
|
+
]);
|
|
831
|
+
var MODERATE_COMMANDS = /* @__PURE__ */ new Set([
|
|
832
|
+
"curl",
|
|
833
|
+
"wget",
|
|
834
|
+
"npm",
|
|
835
|
+
"pip",
|
|
836
|
+
"git",
|
|
837
|
+
"make",
|
|
838
|
+
"docker",
|
|
839
|
+
"node",
|
|
840
|
+
"python",
|
|
841
|
+
"python3",
|
|
842
|
+
"java",
|
|
843
|
+
"go",
|
|
844
|
+
"cargo",
|
|
845
|
+
"apt",
|
|
846
|
+
"yum",
|
|
847
|
+
"brew",
|
|
848
|
+
"npx",
|
|
849
|
+
"yarn",
|
|
850
|
+
"pnpm"
|
|
851
|
+
]);
|
|
852
|
+
var DANGEROUS_COMMANDS = /* @__PURE__ */ new Set([
|
|
853
|
+
"rm",
|
|
854
|
+
"mv",
|
|
855
|
+
"chmod",
|
|
856
|
+
"chown",
|
|
857
|
+
"sudo",
|
|
858
|
+
"su",
|
|
859
|
+
"dd",
|
|
860
|
+
"mkfs",
|
|
861
|
+
"format",
|
|
862
|
+
"fdisk",
|
|
863
|
+
"mkfs.ext4",
|
|
864
|
+
"mkfs.ntfs",
|
|
865
|
+
"shutdown",
|
|
866
|
+
"reboot",
|
|
867
|
+
"halt",
|
|
868
|
+
"poweroff",
|
|
869
|
+
"kill",
|
|
870
|
+
"killall"
|
|
871
|
+
]);
|
|
872
|
+
var REDIRECTION_RE = /\s(&?>{1,2}|2>>?|<{1,2})\s*(\S+)/g;
|
|
873
|
+
var ENV_VAR_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(\S*)\s+/;
|
|
874
|
+
var SUBSHELL_RE = /\$\(([^)]+)\)/;
|
|
875
|
+
var OPERATOR_SPLIT_RE = /\s*(\|\||&&|;|\|)\s*/;
|
|
876
|
+
function parseCommand(input) {
|
|
877
|
+
const trimmed = input.trim();
|
|
878
|
+
let remaining = trimmed;
|
|
879
|
+
const envVars = {};
|
|
880
|
+
let envMatch;
|
|
881
|
+
while ((envMatch = ENV_VAR_RE.exec(remaining)) !== null) {
|
|
882
|
+
envVars[envMatch[1]] = envMatch[2];
|
|
883
|
+
remaining = remaining.slice(envMatch[0].length);
|
|
884
|
+
}
|
|
885
|
+
const isBackground = /(?:^|\s)&\s*$/.test(remaining);
|
|
886
|
+
if (isBackground) {
|
|
887
|
+
remaining = remaining.replace(/&\s*$/, "").trim();
|
|
888
|
+
}
|
|
889
|
+
const redirections = [];
|
|
890
|
+
let redirMatch;
|
|
891
|
+
while ((redirMatch = REDIRECTION_RE.exec(remaining)) !== null) {
|
|
892
|
+
const kind = redirMatch[1];
|
|
893
|
+
const target = redirMatch[2];
|
|
894
|
+
if (kind === "|") {
|
|
895
|
+
redirections.push({ type: "|", target });
|
|
896
|
+
} else if (kind === ">>") {
|
|
897
|
+
redirections.push({ type: ">>", target });
|
|
898
|
+
} else if (kind === "2>>") {
|
|
899
|
+
redirections.push({ type: "2>>", target });
|
|
900
|
+
} else if (kind === "2>") {
|
|
901
|
+
redirections.push({ type: "2>", target });
|
|
902
|
+
} else if (kind === "&>") {
|
|
903
|
+
redirections.push({ type: "&>", target });
|
|
904
|
+
} else if (kind === "<" || kind === "<<") {
|
|
905
|
+
redirections.push({ type: "<", target });
|
|
906
|
+
} else {
|
|
907
|
+
redirections.push({ type: ">", target });
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
remaining = remaining.replace(REDIRECTION_RE, " ").trim();
|
|
911
|
+
const isSubshell = SUBSHELL_RE.test(remaining);
|
|
912
|
+
const subCommands = [];
|
|
913
|
+
let subMatch;
|
|
914
|
+
const subshellGlobal = /\$\(([^)]+)\)/g;
|
|
915
|
+
while ((subMatch = subshellGlobal.exec(remaining)) !== null) {
|
|
916
|
+
subCommands.push(parseCommand(subMatch[1]));
|
|
917
|
+
}
|
|
918
|
+
const parts = remaining.split(OPERATOR_SPLIT_RE).filter(Boolean);
|
|
919
|
+
const operators = [];
|
|
920
|
+
const commandParts = [];
|
|
921
|
+
for (const part of parts) {
|
|
922
|
+
if (["||", "&&", ";", "|"].includes(part)) {
|
|
923
|
+
operators.push(part);
|
|
924
|
+
} else {
|
|
925
|
+
commandParts.push(part);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
let command = "";
|
|
929
|
+
let args = [];
|
|
930
|
+
if (commandParts.length > 0) {
|
|
931
|
+
const first = commandParts[0];
|
|
932
|
+
const tokens = tokenize(first);
|
|
933
|
+
command = tokens[0] ?? "";
|
|
934
|
+
args = tokens.slice(1);
|
|
935
|
+
}
|
|
936
|
+
const parsedSubCommands = commandParts.slice(1).map((cp) => {
|
|
937
|
+
const tokens = tokenize(cp);
|
|
938
|
+
return {
|
|
939
|
+
command: tokens[0] ?? "",
|
|
940
|
+
args: tokens.slice(1),
|
|
941
|
+
operators: [],
|
|
942
|
+
subCommands: [],
|
|
943
|
+
redirections: [],
|
|
944
|
+
envVars: {},
|
|
945
|
+
isBackground: false,
|
|
946
|
+
isSubshell: false
|
|
947
|
+
};
|
|
948
|
+
});
|
|
949
|
+
return {
|
|
950
|
+
command,
|
|
951
|
+
args,
|
|
952
|
+
operators,
|
|
953
|
+
subCommands: [...subCommands, ...parsedSubCommands],
|
|
954
|
+
redirections,
|
|
955
|
+
envVars,
|
|
956
|
+
isBackground,
|
|
957
|
+
isSubshell
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function analyzeCommand(input) {
|
|
961
|
+
const parsed = parseCommand(input);
|
|
962
|
+
const allCommands = collectCommands(parsed);
|
|
963
|
+
const isPipe = parsed.operators.includes("|");
|
|
964
|
+
const isChained = parsed.operators.some((op) => op === "&&" || op === "||" || op === ";");
|
|
965
|
+
const hasRedirection = parsed.redirections.length > 0;
|
|
966
|
+
const hasSubshell = parsed.isSubshell || parsed.subCommands.length > 0;
|
|
967
|
+
const envVarCount = Object.keys(parsed.envVars).length;
|
|
968
|
+
const estimatedRisk = determineRisk(allCommands, isPipe, isChained, hasRedirection, hasSubshell);
|
|
969
|
+
return {
|
|
970
|
+
commands: allCommands,
|
|
971
|
+
isPipe,
|
|
972
|
+
isChained,
|
|
973
|
+
hasRedirection,
|
|
974
|
+
hasSubshell,
|
|
975
|
+
isBackground: parsed.isBackground,
|
|
976
|
+
envVarCount,
|
|
977
|
+
estimatedRisk
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function collectCommands(parsed) {
|
|
981
|
+
const commands = [];
|
|
982
|
+
if (parsed.command) {
|
|
983
|
+
commands.push(parsed.command);
|
|
984
|
+
}
|
|
985
|
+
for (const sub of parsed.subCommands) {
|
|
986
|
+
commands.push(...collectCommands(sub));
|
|
987
|
+
}
|
|
988
|
+
return commands;
|
|
989
|
+
}
|
|
990
|
+
function determineRisk(commands, isPipe, isChained, hasRedirection, hasSubshell) {
|
|
991
|
+
let hasDangerous = false;
|
|
992
|
+
let hasModerate = false;
|
|
993
|
+
for (const cmd of commands) {
|
|
994
|
+
const base = cmd.split("/").pop() ?? cmd;
|
|
995
|
+
if (DANGEROUS_COMMANDS.has(base)) hasDangerous = true;
|
|
996
|
+
else if (MODERATE_COMMANDS.has(base)) hasModerate = true;
|
|
997
|
+
}
|
|
998
|
+
if (hasDangerous) return "dangerous";
|
|
999
|
+
if (hasModerate) return "moderate";
|
|
1000
|
+
const allSafe = commands.every((cmd) => {
|
|
1001
|
+
const base = cmd.split("/").pop() ?? cmd;
|
|
1002
|
+
return SAFE_COMMANDS2.has(base);
|
|
1003
|
+
});
|
|
1004
|
+
if (allSafe && commands.length > 0) return "safe";
|
|
1005
|
+
if (isPipe || isChained || hasRedirection || hasSubshell) {
|
|
1006
|
+
return "moderate";
|
|
1007
|
+
}
|
|
1008
|
+
return "moderate";
|
|
1009
|
+
}
|
|
1010
|
+
function tokenize(segment) {
|
|
1011
|
+
const tokens = [];
|
|
1012
|
+
let current = "";
|
|
1013
|
+
let inSingle = false;
|
|
1014
|
+
let inDouble = false;
|
|
1015
|
+
for (let i = 0; i < segment.length; i++) {
|
|
1016
|
+
const ch = segment[i];
|
|
1017
|
+
if (ch === "'" && !inDouble) {
|
|
1018
|
+
inSingle = !inSingle;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (ch === '"' && !inSingle) {
|
|
1022
|
+
inDouble = !inDouble;
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (ch === "\\" && !inSingle && i + 1 < segment.length) {
|
|
1026
|
+
current += segment[i + 1];
|
|
1027
|
+
i++;
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (/\s/.test(ch) && !inSingle && !inDouble) {
|
|
1031
|
+
if (current) {
|
|
1032
|
+
tokens.push(current);
|
|
1033
|
+
current = "";
|
|
1034
|
+
}
|
|
1035
|
+
} else {
|
|
1036
|
+
current += ch;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (current) tokens.push(current);
|
|
1040
|
+
return tokens;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/mcp/client.ts
|
|
1044
|
+
import { spawn } from "child_process";
|
|
1045
|
+
import { createInterface } from "readline";
|
|
1046
|
+
var REQUEST_TIMEOUT = 3e4;
|
|
1047
|
+
var MCPClient = class {
|
|
1048
|
+
config;
|
|
1049
|
+
process = null;
|
|
1050
|
+
rl = null;
|
|
1051
|
+
connected = false;
|
|
1052
|
+
nextId = 1;
|
|
1053
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1054
|
+
buffer = "";
|
|
1055
|
+
constructor(config) {
|
|
1056
|
+
this.config = config;
|
|
1057
|
+
}
|
|
1058
|
+
async connect() {
|
|
1059
|
+
if (this.connected) return;
|
|
1060
|
+
const env = {
|
|
1061
|
+
...process.env,
|
|
1062
|
+
...this.config.env
|
|
1063
|
+
};
|
|
1064
|
+
this.process = spawn(this.config.command, this.config.args, {
|
|
1065
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1066
|
+
env
|
|
1067
|
+
});
|
|
1068
|
+
this.process.on("error", (err) => {
|
|
1069
|
+
this.cleanup();
|
|
1070
|
+
throw err;
|
|
1071
|
+
});
|
|
1072
|
+
this.process.on("exit", () => {
|
|
1073
|
+
this.cleanup();
|
|
1074
|
+
});
|
|
1075
|
+
if (!this.process.stdout) {
|
|
1076
|
+
throw new Error("MCP server stdout is not available");
|
|
1077
|
+
}
|
|
1078
|
+
this.rl = createInterface({ input: this.process.stdout });
|
|
1079
|
+
this.rl.on("line", (line) => {
|
|
1080
|
+
this.handleLine(line);
|
|
1081
|
+
});
|
|
1082
|
+
if (this.process.stderr) {
|
|
1083
|
+
this.process.stderr.on("data", () => {
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
await this.sendRequest("initialize", {
|
|
1087
|
+
protocolVersion: "2024-11-05",
|
|
1088
|
+
capabilities: {},
|
|
1089
|
+
clientInfo: { name: "cliskill", version: "1.0.0" }
|
|
1090
|
+
});
|
|
1091
|
+
this.sendNotification("notifications/initialized", {});
|
|
1092
|
+
this.connected = true;
|
|
1093
|
+
}
|
|
1094
|
+
async disconnect() {
|
|
1095
|
+
if (!this.process) return;
|
|
1096
|
+
for (const [, pending] of this.pendingRequests) {
|
|
1097
|
+
clearTimeout(pending.timer);
|
|
1098
|
+
pending.reject(new Error("Connection closed"));
|
|
1099
|
+
}
|
|
1100
|
+
this.pendingRequests.clear();
|
|
1101
|
+
this.process.kill("SIGTERM");
|
|
1102
|
+
this.cleanup();
|
|
1103
|
+
}
|
|
1104
|
+
async listTools() {
|
|
1105
|
+
const response = await this.sendRequest("tools/list", {});
|
|
1106
|
+
const result = response.result;
|
|
1107
|
+
return result?.tools ?? [];
|
|
1108
|
+
}
|
|
1109
|
+
async callTool(name, args) {
|
|
1110
|
+
const response = await this.sendRequest("tools/call", { name, arguments: args });
|
|
1111
|
+
return response.result;
|
|
1112
|
+
}
|
|
1113
|
+
async listResources() {
|
|
1114
|
+
const response = await this.sendRequest("resources/list", {});
|
|
1115
|
+
const result = response.result;
|
|
1116
|
+
return result?.resources ?? [];
|
|
1117
|
+
}
|
|
1118
|
+
async readResource(uri) {
|
|
1119
|
+
const response = await this.sendRequest("resources/read", { uri });
|
|
1120
|
+
return response.result;
|
|
1121
|
+
}
|
|
1122
|
+
async listPrompts() {
|
|
1123
|
+
const response = await this.sendRequest("prompts/list", {});
|
|
1124
|
+
const result = response.result;
|
|
1125
|
+
return result?.prompts ?? [];
|
|
1126
|
+
}
|
|
1127
|
+
isConnected() {
|
|
1128
|
+
return this.connected;
|
|
1129
|
+
}
|
|
1130
|
+
sendRequest(method, params) {
|
|
1131
|
+
return new Promise((resolve2, reject) => {
|
|
1132
|
+
if (!this.process?.stdin) {
|
|
1133
|
+
reject(new Error("MCP server not connected"));
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const id = this.nextId++;
|
|
1137
|
+
const request = { jsonrpc: "2.0", id, method, params };
|
|
1138
|
+
const timer = setTimeout(() => {
|
|
1139
|
+
this.pendingRequests.delete(id);
|
|
1140
|
+
reject(new Error(`Request timeout: ${method} (id=${id})`));
|
|
1141
|
+
}, REQUEST_TIMEOUT);
|
|
1142
|
+
this.pendingRequests.set(id, { resolve: resolve2, reject, timer });
|
|
1143
|
+
const data = JSON.stringify(request) + "\n";
|
|
1144
|
+
this.process.stdin.write(data, (err) => {
|
|
1145
|
+
if (err) {
|
|
1146
|
+
clearTimeout(timer);
|
|
1147
|
+
this.pendingRequests.delete(id);
|
|
1148
|
+
reject(err);
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
sendNotification(method, params) {
|
|
1154
|
+
if (!this.process?.stdin) return;
|
|
1155
|
+
const notification = { jsonrpc: "2.0", method, params };
|
|
1156
|
+
const data = JSON.stringify(notification) + "\n";
|
|
1157
|
+
this.process.stdin.write(data);
|
|
1158
|
+
}
|
|
1159
|
+
handleLine(line) {
|
|
1160
|
+
this.buffer += line;
|
|
1161
|
+
let response;
|
|
1162
|
+
try {
|
|
1163
|
+
response = JSON.parse(this.buffer);
|
|
1164
|
+
} catch {
|
|
1165
|
+
return;
|
|
1166
|
+
} finally {
|
|
1167
|
+
this.buffer = "";
|
|
1168
|
+
}
|
|
1169
|
+
if (response.id !== void 0 && response.id !== null) {
|
|
1170
|
+
const pending = this.pendingRequests.get(response.id);
|
|
1171
|
+
if (pending) {
|
|
1172
|
+
clearTimeout(pending.timer);
|
|
1173
|
+
this.pendingRequests.delete(response.id);
|
|
1174
|
+
pending.resolve(response);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
cleanup() {
|
|
1179
|
+
this.connected = false;
|
|
1180
|
+
this.rl?.close();
|
|
1181
|
+
this.rl = null;
|
|
1182
|
+
this.process = null;
|
|
1183
|
+
this.buffer = "";
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
// src/mcp/manager.ts
|
|
1188
|
+
var MCPConnectionManager = class {
|
|
1189
|
+
clients = /* @__PURE__ */ new Map();
|
|
1190
|
+
async addServer(config) {
|
|
1191
|
+
const client = new MCPClient(config);
|
|
1192
|
+
await client.connect();
|
|
1193
|
+
this.clients.set(config.name, client);
|
|
1194
|
+
}
|
|
1195
|
+
async removeServer(name) {
|
|
1196
|
+
const client = this.clients.get(name);
|
|
1197
|
+
if (client) {
|
|
1198
|
+
await client.disconnect();
|
|
1199
|
+
this.clients.delete(name);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
getConnectedServers() {
|
|
1203
|
+
return Array.from(this.clients.entries()).filter(([, client]) => client.isConnected()).map(([name]) => name);
|
|
1204
|
+
}
|
|
1205
|
+
async getAllTools() {
|
|
1206
|
+
const allTools = [];
|
|
1207
|
+
for (const [, client] of this.clients) {
|
|
1208
|
+
if (!client.isConnected()) continue;
|
|
1209
|
+
try {
|
|
1210
|
+
const tools = await client.listTools();
|
|
1211
|
+
allTools.push(...tools);
|
|
1212
|
+
} catch {
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return allTools;
|
|
1216
|
+
}
|
|
1217
|
+
async callTool(serverName, toolName, args) {
|
|
1218
|
+
const client = this.clients.get(serverName);
|
|
1219
|
+
if (!client) {
|
|
1220
|
+
throw new Error(`MCP server "${serverName}" not found`);
|
|
1221
|
+
}
|
|
1222
|
+
if (!client.isConnected()) {
|
|
1223
|
+
throw new Error(`MCP server "${serverName}" is not connected`);
|
|
1224
|
+
}
|
|
1225
|
+
return client.callTool(toolName, args);
|
|
1226
|
+
}
|
|
1227
|
+
async getToolsForServer(serverName) {
|
|
1228
|
+
const client = this.clients.get(serverName);
|
|
1229
|
+
if (!client || !client.isConnected()) {
|
|
1230
|
+
return [];
|
|
1231
|
+
}
|
|
1232
|
+
return client.listTools();
|
|
1233
|
+
}
|
|
1234
|
+
async disconnectAll() {
|
|
1235
|
+
for (const [, client] of this.clients) {
|
|
1236
|
+
try {
|
|
1237
|
+
await client.disconnect();
|
|
1238
|
+
} catch {
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
this.clients.clear();
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
// src/tasks/manager.ts
|
|
1246
|
+
var TaskManager = class {
|
|
1247
|
+
tasks = /* @__PURE__ */ new Map();
|
|
1248
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1249
|
+
idCounter = 0;
|
|
1250
|
+
createTask(definition) {
|
|
1251
|
+
const id = `task_${++this.idCounter}_${Date.now()}`;
|
|
1252
|
+
const task = {
|
|
1253
|
+
...definition,
|
|
1254
|
+
id,
|
|
1255
|
+
status: "pending",
|
|
1256
|
+
createdAt: Date.now(),
|
|
1257
|
+
progress: 0
|
|
1258
|
+
};
|
|
1259
|
+
this.tasks.set(id, task);
|
|
1260
|
+
return id;
|
|
1261
|
+
}
|
|
1262
|
+
cancelTask(id) {
|
|
1263
|
+
const task = this.tasks.get(id);
|
|
1264
|
+
if (!task) return;
|
|
1265
|
+
if (task.status !== "running" && task.status !== "pending") return;
|
|
1266
|
+
task.status = "cancelled";
|
|
1267
|
+
task.completedAt = Date.now();
|
|
1268
|
+
this.emit("onStatusChange", id, "cancelled");
|
|
1269
|
+
}
|
|
1270
|
+
getTask(id) {
|
|
1271
|
+
return this.tasks.get(id);
|
|
1272
|
+
}
|
|
1273
|
+
listTasks(filter) {
|
|
1274
|
+
const all = Array.from(this.tasks.values());
|
|
1275
|
+
if (!filter?.status) return all;
|
|
1276
|
+
return all.filter((t) => t.status === filter.status);
|
|
1277
|
+
}
|
|
1278
|
+
getActiveCount() {
|
|
1279
|
+
let count = 0;
|
|
1280
|
+
for (const task of this.tasks.values()) {
|
|
1281
|
+
if (task.status === "running" || task.status === "pending") count++;
|
|
1282
|
+
}
|
|
1283
|
+
return count;
|
|
1284
|
+
}
|
|
1285
|
+
removeTask(id) {
|
|
1286
|
+
const task = this.tasks.get(id);
|
|
1287
|
+
if (!task) return;
|
|
1288
|
+
if (task.status === "running") {
|
|
1289
|
+
throw new Error(`Cannot remove running task "${id}". Cancel it first.`);
|
|
1290
|
+
}
|
|
1291
|
+
this.tasks.delete(id);
|
|
1292
|
+
}
|
|
1293
|
+
clearCompleted() {
|
|
1294
|
+
for (const [id, task] of this.tasks) {
|
|
1295
|
+
if (task.status === "completed" || task.status === "failed" || task.status === "cancelled") {
|
|
1296
|
+
this.tasks.delete(id);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
on(event, handler) {
|
|
1301
|
+
if (!this.listeners.has(event)) {
|
|
1302
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
1303
|
+
}
|
|
1304
|
+
this.listeners.get(event).add(handler);
|
|
1305
|
+
}
|
|
1306
|
+
off(event, handler) {
|
|
1307
|
+
this.listeners.get(event)?.delete(handler);
|
|
1308
|
+
}
|
|
1309
|
+
/** @internal Update task status */
|
|
1310
|
+
updateStatus(id, status) {
|
|
1311
|
+
const task = this.tasks.get(id);
|
|
1312
|
+
if (!task) return;
|
|
1313
|
+
task.status = status;
|
|
1314
|
+
if (status === "running") task.startedAt = Date.now();
|
|
1315
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
1316
|
+
task.completedAt = Date.now();
|
|
1317
|
+
}
|
|
1318
|
+
this.emit("onStatusChange", id, status);
|
|
1319
|
+
}
|
|
1320
|
+
/** @internal Update task progress */
|
|
1321
|
+
updateProgress(id, progress, message) {
|
|
1322
|
+
const task = this.tasks.get(id);
|
|
1323
|
+
if (!task) return;
|
|
1324
|
+
task.progress = Math.min(100, Math.max(0, progress));
|
|
1325
|
+
this.emit("onProgress", id, task.progress, message);
|
|
1326
|
+
}
|
|
1327
|
+
/** @internal Set task result */
|
|
1328
|
+
setResult(id, result) {
|
|
1329
|
+
const task = this.tasks.get(id);
|
|
1330
|
+
if (!task) return;
|
|
1331
|
+
task.result = result;
|
|
1332
|
+
task.status = "completed";
|
|
1333
|
+
task.completedAt = Date.now();
|
|
1334
|
+
task.progress = 100;
|
|
1335
|
+
this.emit("onComplete", id, result);
|
|
1336
|
+
this.emit("onStatusChange", id, "completed");
|
|
1337
|
+
}
|
|
1338
|
+
/** @internal Set task error */
|
|
1339
|
+
setError(id, error) {
|
|
1340
|
+
const task = this.tasks.get(id);
|
|
1341
|
+
if (!task) return;
|
|
1342
|
+
task.error = error;
|
|
1343
|
+
task.status = "failed";
|
|
1344
|
+
task.completedAt = Date.now();
|
|
1345
|
+
this.emit("onError", id, error);
|
|
1346
|
+
this.emit("onStatusChange", id, "failed");
|
|
1347
|
+
}
|
|
1348
|
+
emit(event, ...args) {
|
|
1349
|
+
this.listeners.get(event)?.forEach((fn) => fn(...args));
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
// src/tasks/executor.ts
|
|
1354
|
+
import { exec as exec2 } from "child_process";
|
|
1355
|
+
async function executeAgentTask(taskId, manager, adapter, tools, prompt, signal) {
|
|
1356
|
+
manager.updateStatus(taskId, "running");
|
|
1357
|
+
const engine = new QueryEngine(
|
|
1358
|
+
{ maxTurns: 20, systemPrompt: "You are a background agent. Complete the task concisely." },
|
|
1359
|
+
adapter,
|
|
1360
|
+
tools
|
|
1361
|
+
);
|
|
1362
|
+
let finalText = "";
|
|
1363
|
+
let turnsUsed = 0;
|
|
1364
|
+
try {
|
|
1365
|
+
for await (const event of engine.run(prompt)) {
|
|
1366
|
+
if (signal?.aborted) {
|
|
1367
|
+
manager.cancelTask(taskId);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
switch (event.type) {
|
|
1371
|
+
case "text_delta":
|
|
1372
|
+
finalText += event.text;
|
|
1373
|
+
break;
|
|
1374
|
+
case "turn_end":
|
|
1375
|
+
turnsUsed = event.turn;
|
|
1376
|
+
manager.updateProgress(taskId, Math.min(90, turnsUsed * 10));
|
|
1377
|
+
break;
|
|
1378
|
+
case "complete":
|
|
1379
|
+
finalText = finalText || event.result.messages.filter((m) => m.role === "assistant").flatMap((m) => m.content).filter((b) => b.type === "text").map((b) => b.text).join("\n") || "Task completed";
|
|
1380
|
+
break;
|
|
1381
|
+
case "error":
|
|
1382
|
+
manager.setError(taskId, event.error.message);
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
manager.setResult(taskId, finalText || "Task completed (no output)");
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
manager.setError(taskId, err.message);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
async function executeShellTask(taskId, manager, command, cwd, signal) {
|
|
1392
|
+
manager.updateStatus(taskId, "running");
|
|
1393
|
+
return new Promise((resolve2) => {
|
|
1394
|
+
const child = exec2(command, { cwd, signal }, (error, stdout, stderr) => {
|
|
1395
|
+
if (signal?.aborted) {
|
|
1396
|
+
manager.cancelTask(taskId);
|
|
1397
|
+
resolve2();
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
if (error) {
|
|
1401
|
+
manager.setError(taskId, stderr || error.message);
|
|
1402
|
+
resolve2();
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
manager.setResult(taskId, stdout || "(no output)");
|
|
1406
|
+
resolve2();
|
|
1407
|
+
});
|
|
1408
|
+
if (signal) {
|
|
1409
|
+
signal.addEventListener("abort", () => {
|
|
1410
|
+
child.kill("SIGTERM");
|
|
1411
|
+
}, { once: true });
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/swarm/coordinator.ts
|
|
1417
|
+
var MAX_PARALLEL_WORKERS = 3;
|
|
1418
|
+
var SUBTASK_REGEX = /(?:^|\n)\s*(?:\d+[\.\)])\s*(.+?)(?=\n\s*(?:\d+[\.\)])|$)/gs;
|
|
1419
|
+
var SwarmCoordinator = class {
|
|
1420
|
+
constructor(adapter, tools) {
|
|
1421
|
+
this.adapter = adapter;
|
|
1422
|
+
this.tools = tools;
|
|
1423
|
+
}
|
|
1424
|
+
adapter;
|
|
1425
|
+
tools;
|
|
1426
|
+
agents = /* @__PURE__ */ new Map();
|
|
1427
|
+
mailbox = /* @__PURE__ */ new Map();
|
|
1428
|
+
abortController = new AbortController();
|
|
1429
|
+
addAgent(config) {
|
|
1430
|
+
this.agents.set(config.id, config);
|
|
1431
|
+
this.mailbox.set(config.id, []);
|
|
1432
|
+
}
|
|
1433
|
+
removeAgent(id) {
|
|
1434
|
+
this.agents.delete(id);
|
|
1435
|
+
this.mailbox.delete(id);
|
|
1436
|
+
}
|
|
1437
|
+
getAgents() {
|
|
1438
|
+
return Array.from(this.agents.values());
|
|
1439
|
+
}
|
|
1440
|
+
async sendMessage(message) {
|
|
1441
|
+
if (message.to === "*") {
|
|
1442
|
+
for (const [agentId] of this.agents) {
|
|
1443
|
+
this.mailbox.get(agentId)?.push({ ...message, to: agentId });
|
|
1444
|
+
}
|
|
1445
|
+
} else {
|
|
1446
|
+
this.mailbox.get(message.to)?.push(message);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
async executeSwarm(task) {
|
|
1450
|
+
this.abortController = new AbortController();
|
|
1451
|
+
const startTime = performance.now();
|
|
1452
|
+
const coordinator = this.findAgentByRole("coordinator");
|
|
1453
|
+
const workers = this.findAgentsByRole("worker");
|
|
1454
|
+
const reviewers = this.findAgentsByRole("reviewer");
|
|
1455
|
+
const subtasks = coordinator ? await this.decomposeTask(coordinator, task) : this.fallbackDecompose(task, workers.length);
|
|
1456
|
+
const assignments = this.assignSubtasks(subtasks, workers);
|
|
1457
|
+
const workerResults = await this.executeParallel(assignments);
|
|
1458
|
+
if (reviewers.length > 0) {
|
|
1459
|
+
await this.reviewResults(reviewers[0], workerResults);
|
|
1460
|
+
}
|
|
1461
|
+
const results = workerResults.map((wr) => ({
|
|
1462
|
+
agentId: wr.agentId,
|
|
1463
|
+
agentName: wr.agentName,
|
|
1464
|
+
task: wr.task,
|
|
1465
|
+
result: wr.result,
|
|
1466
|
+
success: wr.success,
|
|
1467
|
+
duration: performance.now() - startTime,
|
|
1468
|
+
turns: wr.turns
|
|
1469
|
+
}));
|
|
1470
|
+
return results;
|
|
1471
|
+
}
|
|
1472
|
+
shutdown() {
|
|
1473
|
+
this.abortController.abort();
|
|
1474
|
+
for (const [agentId] of this.agents) {
|
|
1475
|
+
this.mailbox.get(agentId)?.push({
|
|
1476
|
+
from: "system",
|
|
1477
|
+
to: agentId,
|
|
1478
|
+
type: "shutdown",
|
|
1479
|
+
content: "Shutdown requested",
|
|
1480
|
+
timestamp: Date.now()
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
findAgentByRole(role) {
|
|
1485
|
+
for (const agent of this.agents.values()) {
|
|
1486
|
+
if (agent.role === role) return agent;
|
|
1487
|
+
}
|
|
1488
|
+
return void 0;
|
|
1489
|
+
}
|
|
1490
|
+
findAgentsByRole(role) {
|
|
1491
|
+
return Array.from(this.agents.values()).filter((a) => a.role === role);
|
|
1492
|
+
}
|
|
1493
|
+
async decomposeTask(coordinator, task) {
|
|
1494
|
+
const engine = this.createEngine(coordinator);
|
|
1495
|
+
const prompt = `Decompose the following task into subtasks. Return ONLY a numbered list, one subtask per line. No explanations.
|
|
1496
|
+
|
|
1497
|
+
Task: ${task}
|
|
1498
|
+
|
|
1499
|
+
Subtasks:`;
|
|
1500
|
+
let decomposition = "";
|
|
1501
|
+
try {
|
|
1502
|
+
for await (const event of engine.run(prompt)) {
|
|
1503
|
+
if (event.type === "text_delta") decomposition += event.text;
|
|
1504
|
+
if (event.type === "error") break;
|
|
1505
|
+
}
|
|
1506
|
+
} catch {
|
|
1507
|
+
return this.fallbackDecompose(task, this.findAgentsByRole("worker").length);
|
|
1508
|
+
}
|
|
1509
|
+
const matches = [...decomposition.matchAll(SUBTASK_REGEX)];
|
|
1510
|
+
if (matches.length === 0) {
|
|
1511
|
+
return this.fallbackDecompose(task, this.findAgentsByRole("worker").length);
|
|
1512
|
+
}
|
|
1513
|
+
return matches.map((m, i) => ({
|
|
1514
|
+
id: `subtask_${i + 1}`,
|
|
1515
|
+
description: m[1].trim(),
|
|
1516
|
+
status: "pending"
|
|
1517
|
+
}));
|
|
1518
|
+
}
|
|
1519
|
+
fallbackDecompose(task, workerCount) {
|
|
1520
|
+
const count = Math.max(1, Math.min(workerCount, MAX_PARALLEL_WORKERS));
|
|
1521
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
1522
|
+
id: `subtask_${i + 1}`,
|
|
1523
|
+
description: count === 1 ? task : `Part ${i + 1} of: ${task}`,
|
|
1524
|
+
status: "pending"
|
|
1525
|
+
}));
|
|
1526
|
+
}
|
|
1527
|
+
assignSubtasks(subtasks, workers) {
|
|
1528
|
+
const assignments = [];
|
|
1529
|
+
if (workers.length === 0) return assignments;
|
|
1530
|
+
subtasks.forEach((task, i) => {
|
|
1531
|
+
const worker = workers[i % workers.length];
|
|
1532
|
+
task.assignedTo = worker.id;
|
|
1533
|
+
assignments.push({ task, worker });
|
|
1534
|
+
});
|
|
1535
|
+
return assignments;
|
|
1536
|
+
}
|
|
1537
|
+
async executeParallel(assignments) {
|
|
1538
|
+
const chunks = [];
|
|
1539
|
+
for (let i = 0; i < assignments.length; i += MAX_PARALLEL_WORKERS) {
|
|
1540
|
+
chunks.push(assignments.slice(i, i + MAX_PARALLEL_WORKERS));
|
|
1541
|
+
}
|
|
1542
|
+
const allResults = [];
|
|
1543
|
+
for (const chunk of chunks) {
|
|
1544
|
+
if (this.abortController.signal.aborted) break;
|
|
1545
|
+
const settled = await Promise.allSettled(
|
|
1546
|
+
chunk.map(async ({ task, worker }) => {
|
|
1547
|
+
task.status = "running";
|
|
1548
|
+
const engine = this.createEngine(worker);
|
|
1549
|
+
let output = "";
|
|
1550
|
+
let turns = 0;
|
|
1551
|
+
try {
|
|
1552
|
+
for await (const event of engine.run(task.description)) {
|
|
1553
|
+
if (this.abortController.signal.aborted) break;
|
|
1554
|
+
if (event.type === "text_delta") output += event.text;
|
|
1555
|
+
if (event.type === "turn_end") turns = event.turn;
|
|
1556
|
+
if (event.type === "complete") {
|
|
1557
|
+
turns = event.result.turns;
|
|
1558
|
+
output = output || event.result.messages.filter((m) => m.role === "assistant").flatMap((m) => m.content).filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
task.status = "completed";
|
|
1562
|
+
task.result = output;
|
|
1563
|
+
return { agentId: worker.id, agentName: worker.name, task: task.description, result: output, success: true, turns };
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
task.status = "failed";
|
|
1566
|
+
return { agentId: worker.id, agentName: worker.name, task: task.description, result: err.message, success: false, turns };
|
|
1567
|
+
}
|
|
1568
|
+
})
|
|
1569
|
+
);
|
|
1570
|
+
for (const r of settled) {
|
|
1571
|
+
if (r.status === "fulfilled") allResults.push(r.value);
|
|
1572
|
+
else allResults.push({ agentId: "unknown", agentName: "unknown", task: "", result: r.reason?.message ?? "Unknown error", success: false, turns: 0 });
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return allResults;
|
|
1576
|
+
}
|
|
1577
|
+
async reviewResults(reviewer, results) {
|
|
1578
|
+
const engine = this.createEngine(reviewer);
|
|
1579
|
+
const summary = results.map((r, i) => `Worker ${i + 1} (${r.agentName}): ${r.success ? "SUCCESS" : "FAILED"}
|
|
1580
|
+
Task: ${r.task}
|
|
1581
|
+
Result: ${r.result.slice(0, 500)}`).join("\n\n");
|
|
1582
|
+
const prompt = `Review the following worker results and provide a brief assessment:
|
|
1583
|
+
|
|
1584
|
+
${summary}
|
|
1585
|
+
|
|
1586
|
+
Provide a concise review:`;
|
|
1587
|
+
try {
|
|
1588
|
+
for await (const event of engine.run(prompt)) {
|
|
1589
|
+
if (event.type === "error") break;
|
|
1590
|
+
}
|
|
1591
|
+
} catch {
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
createEngine(agent) {
|
|
1595
|
+
const filteredTools = new ToolRegistry();
|
|
1596
|
+
if (agent.tools) {
|
|
1597
|
+
for (const toolName of agent.tools) {
|
|
1598
|
+
const tool = this.tools.get(toolName);
|
|
1599
|
+
if (tool) filteredTools.register(tool);
|
|
1600
|
+
}
|
|
1601
|
+
} else {
|
|
1602
|
+
for (const tool of this.tools.getAll()) {
|
|
1603
|
+
if (tool.name !== "agent") filteredTools.register(tool);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return new QueryEngine(
|
|
1607
|
+
{
|
|
1608
|
+
maxTurns: agent.maxTurns ?? 10,
|
|
1609
|
+
systemPrompt: agent.systemPrompt ?? `You are agent "${agent.name}" with role "${agent.role}". Complete assigned tasks concisely.`
|
|
1610
|
+
},
|
|
1611
|
+
this.adapter,
|
|
1612
|
+
filteredTools
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
export {
|
|
1617
|
+
AdapterRegistry,
|
|
1618
|
+
AgentTool,
|
|
1619
|
+
ContextCompactor,
|
|
1620
|
+
CostTracker,
|
|
1621
|
+
EnhancedMemoryStore,
|
|
1622
|
+
EnterPlanModeTool,
|
|
1623
|
+
ExitPlanModeTool,
|
|
1624
|
+
GenericCompatAdapter,
|
|
1625
|
+
HistoryManager,
|
|
1626
|
+
HookExecutor,
|
|
1627
|
+
HookRegistry,
|
|
1628
|
+
InkApp,
|
|
1629
|
+
LspTool,
|
|
1630
|
+
MCPClient,
|
|
1631
|
+
MCPConnectionManager,
|
|
1632
|
+
MemoryStore,
|
|
1633
|
+
MessageList,
|
|
1634
|
+
PermissionManager,
|
|
1635
|
+
QueryEngine,
|
|
1636
|
+
SkillRegistry,
|
|
1637
|
+
Spinner,
|
|
1638
|
+
StatusBar,
|
|
1639
|
+
StreamingToolExecutor,
|
|
1640
|
+
SwarmCoordinator,
|
|
1641
|
+
TaskManager,
|
|
1642
|
+
ToolRegistry,
|
|
1643
|
+
WebSearchTool,
|
|
1644
|
+
analyzeCommand,
|
|
1645
|
+
analyzeTokenBudget,
|
|
1646
|
+
createDefaultToolRegistry,
|
|
1647
|
+
estimateMessagesTokens,
|
|
1648
|
+
estimateTokens,
|
|
1649
|
+
executeAgentTask,
|
|
1650
|
+
executeShellTask,
|
|
1651
|
+
formatAnsiMessage,
|
|
1652
|
+
inspectCommand,
|
|
1653
|
+
parseCommand,
|
|
1654
|
+
renderInkApp,
|
|
1655
|
+
runAgentLoop,
|
|
1656
|
+
runCli
|
|
1657
|
+
};
|
|
1658
|
+
//# sourceMappingURL=index.js.map
|