@germanescobar/anita 0.3.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/README.md +353 -0
- package/dist/agent/agents.d.ts +16 -0
- package/dist/agent/agents.js +115 -0
- package/dist/agent/context-budget.d.ts +7 -0
- package/dist/agent/context-budget.js +17 -0
- package/dist/agent/context-builder.d.ts +34 -0
- package/dist/agent/context-builder.js +175 -0
- package/dist/agent/executor.d.ts +13 -0
- package/dist/agent/executor.js +65 -0
- package/dist/agent/loop.d.ts +54 -0
- package/dist/agent/loop.js +548 -0
- package/dist/agent/policies.d.ts +25 -0
- package/dist/agent/policies.js +177 -0
- package/dist/agent/session.d.ts +12 -0
- package/dist/agent/session.js +42 -0
- package/dist/attachments.d.ts +3 -0
- package/dist/attachments.js +73 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +327 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/models/anthropic.d.ts +15 -0
- package/dist/models/anthropic.js +195 -0
- package/dist/models/openai-responses.d.ts +62 -0
- package/dist/models/openai-responses.js +377 -0
- package/dist/models/openai.d.ts +32 -0
- package/dist/models/openai.js +330 -0
- package/dist/models/provider.d.ts +33 -0
- package/dist/models/provider.js +1 -0
- package/dist/models/resolve.d.ts +48 -0
- package/dist/models/resolve.js +211 -0
- package/dist/security/sensitive-content.d.ts +6 -0
- package/dist/security/sensitive-content.js +59 -0
- package/dist/skills/skills.d.ts +62 -0
- package/dist/skills/skills.js +371 -0
- package/dist/storage/event-store.d.ts +7 -0
- package/dist/storage/event-store.js +36 -0
- package/dist/storage/session-store.d.ts +11 -0
- package/dist/storage/session-store.js +64 -0
- package/dist/tools/delete-file.d.ts +2 -0
- package/dist/tools/delete-file.js +25 -0
- package/dist/tools/edit-file.d.ts +2 -0
- package/dist/tools/edit-file.js +50 -0
- package/dist/tools/read-file.d.ts +2 -0
- package/dist/tools/read-file.js +122 -0
- package/dist/tools/registry.d.ts +9 -0
- package/dist/tools/registry.js +122 -0
- package/dist/tools/run-command.d.ts +2 -0
- package/dist/tools/run-command.js +103 -0
- package/dist/tools/write-file.d.ts +2 -0
- package/dist/tools/write-file.js +29 -0
- package/dist/types/agent.d.ts +44 -0
- package/dist/types/agent.js +1 -0
- package/dist/types/conversation.d.ts +43 -0
- package/dist/types/conversation.js +201 -0
- package/dist/types/events.d.ts +8 -0
- package/dist/types/events.js +1 -0
- package/dist/types/messages.d.ts +39 -0
- package/dist/types/messages.js +1 -0
- package/dist/types/output.d.ts +19 -0
- package/dist/types/output.js +1 -0
- package/dist/types/stream.d.ts +55 -0
- package/dist/types/stream.js +1 -0
- package/dist/types/tools.d.ts +28 -0
- package/dist/types/tools.js +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface SkillFrontmatter {
|
|
2
|
+
name?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
license?: string;
|
|
5
|
+
compatibility?: string;
|
|
6
|
+
metadata?: Record<string, string>;
|
|
7
|
+
"allowed-tools"?: string;
|
|
8
|
+
"disable-model-invocation"?: boolean;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface Skill {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
filePath: string;
|
|
15
|
+
baseDir: string;
|
|
16
|
+
disableModelInvocation: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface SkillDiagnostic {
|
|
19
|
+
type: "warning" | "error" | "collision";
|
|
20
|
+
message: string;
|
|
21
|
+
path: string;
|
|
22
|
+
collision?: {
|
|
23
|
+
name: string;
|
|
24
|
+
winnerPath: string;
|
|
25
|
+
loserPath: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface LoadSkillsResult {
|
|
29
|
+
skills: Skill[];
|
|
30
|
+
diagnostics: SkillDiagnostic[];
|
|
31
|
+
}
|
|
32
|
+
export interface LoadSkillsOptions {
|
|
33
|
+
/** Working directory for project-local skills. Default: process.cwd() */
|
|
34
|
+
cwd?: string;
|
|
35
|
+
/** Explicit skill paths (files or directories). Additive even with noSkills. */
|
|
36
|
+
skillPaths?: string[];
|
|
37
|
+
/** Include default discovery directories. Default: true */
|
|
38
|
+
includeDefaults?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Load skills from a directory.
|
|
42
|
+
*
|
|
43
|
+
* Discovery rules (following the Agent Skills spec):
|
|
44
|
+
* - If a directory contains SKILL.md, treat it as a skill root (no further recursion)
|
|
45
|
+
* - Otherwise, recurse into subdirectories to find SKILL.md files
|
|
46
|
+
* - In agent-specific directories (~/.anita/skills/, .anita/skills/), root .md files
|
|
47
|
+
* are also discovered as individual skills
|
|
48
|
+
* - Skip dot-dirs, node_modules
|
|
49
|
+
*/
|
|
50
|
+
export declare function loadSkillsFromDir(dir: string, options?: {
|
|
51
|
+
includeRootFiles?: boolean;
|
|
52
|
+
}): LoadSkillsResult;
|
|
53
|
+
export declare function loadSkills(options?: LoadSkillsOptions): LoadSkillsResult;
|
|
54
|
+
/**
|
|
55
|
+
* Format skills for inclusion in a system prompt.
|
|
56
|
+
* Uses XML format per the Agent Skills standard.
|
|
57
|
+
* See: https://agentskills.io/integrate-skills
|
|
58
|
+
*
|
|
59
|
+
* Skills with disableModelInvocation=true are excluded from the prompt
|
|
60
|
+
* (they can only be invoked explicitly via --skill or future command support).
|
|
61
|
+
*/
|
|
62
|
+
export declare function formatSkillsForPrompt(skills: Skill[]): string;
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
5
|
+
const MAX_NAME_LENGTH = 64;
|
|
6
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
7
|
+
// ── Frontmatter parsing ────────────────────────────────────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Extract YAML frontmatter from a markdown file.
|
|
10
|
+
* Returns `{ frontmatter, body }` where frontmatter is parsed key-value pairs
|
|
11
|
+
* and body is the markdown content after the closing `---`.
|
|
12
|
+
*/
|
|
13
|
+
function parseFrontmatter(content) {
|
|
14
|
+
const trimmed = content.replace(/^\uFEFF/, ""); // strip BOM
|
|
15
|
+
if (!trimmed.startsWith("---")) {
|
|
16
|
+
return { frontmatter: {}, body: content };
|
|
17
|
+
}
|
|
18
|
+
const closingIndex = trimmed.indexOf("---", 3);
|
|
19
|
+
if (closingIndex === -1) {
|
|
20
|
+
return { frontmatter: {}, body: content };
|
|
21
|
+
}
|
|
22
|
+
const yamlBlock = trimmed.slice(3, closingIndex).trim();
|
|
23
|
+
const body = trimmed.slice(closingIndex + 3).trim();
|
|
24
|
+
const frontmatter = {};
|
|
25
|
+
for (const line of yamlBlock.split("\n")) {
|
|
26
|
+
const colonIndex = line.indexOf(":");
|
|
27
|
+
if (colonIndex === -1)
|
|
28
|
+
continue;
|
|
29
|
+
const key = line.slice(0, colonIndex).trim();
|
|
30
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
31
|
+
// Handle quoted strings
|
|
32
|
+
if ((typeof value === "string" && value.startsWith('"') && value.endsWith('"')) ||
|
|
33
|
+
(typeof value === "string" && value.startsWith("'") && value.endsWith("'"))) {
|
|
34
|
+
value = value.slice(1, -1);
|
|
35
|
+
}
|
|
36
|
+
// Handle boolean
|
|
37
|
+
if (value === "true")
|
|
38
|
+
value = true;
|
|
39
|
+
if (value === "false")
|
|
40
|
+
value = false;
|
|
41
|
+
if (key === "metadata") {
|
|
42
|
+
// Skip nested metadata parsing for now; store as empty
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
frontmatter[key] = value;
|
|
46
|
+
}
|
|
47
|
+
return { frontmatter, body };
|
|
48
|
+
}
|
|
49
|
+
// ── Validation ─────────────────────────────────────────────────────────────
|
|
50
|
+
function validateName(name, parentDirName) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
if (name !== parentDirName) {
|
|
53
|
+
errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
|
|
54
|
+
}
|
|
55
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
56
|
+
errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
|
|
57
|
+
}
|
|
58
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
59
|
+
errors.push("name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)");
|
|
60
|
+
}
|
|
61
|
+
if (name.startsWith("-") || name.endsWith("-")) {
|
|
62
|
+
errors.push("name must not start or end with a hyphen");
|
|
63
|
+
}
|
|
64
|
+
if (name.includes("--")) {
|
|
65
|
+
errors.push("name must not contain consecutive hyphens");
|
|
66
|
+
}
|
|
67
|
+
return errors;
|
|
68
|
+
}
|
|
69
|
+
function validateDescription(description) {
|
|
70
|
+
const errors = [];
|
|
71
|
+
if (!description || description.trim() === "") {
|
|
72
|
+
errors.push("description is required");
|
|
73
|
+
}
|
|
74
|
+
else if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
75
|
+
errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
|
|
76
|
+
}
|
|
77
|
+
return errors;
|
|
78
|
+
}
|
|
79
|
+
// ── XML escaping ───────────────────────────────────────────────────────────
|
|
80
|
+
function escapeXml(str) {
|
|
81
|
+
return str
|
|
82
|
+
.replace(/&/g, "&")
|
|
83
|
+
.replace(/</g, "<")
|
|
84
|
+
.replace(/>/g, ">")
|
|
85
|
+
.replace(/"/g, """)
|
|
86
|
+
.replace(/'/g, "'");
|
|
87
|
+
}
|
|
88
|
+
// ── Single file loading ────────────────────────────────────────────────────
|
|
89
|
+
function loadSkillFromFile(filePath) {
|
|
90
|
+
const diagnostics = [];
|
|
91
|
+
try {
|
|
92
|
+
const rawContent = fs.readFileSync(filePath, "utf-8");
|
|
93
|
+
const { frontmatter } = parseFrontmatter(rawContent);
|
|
94
|
+
const skillDir = path.dirname(filePath);
|
|
95
|
+
const parentDirName = path.basename(skillDir);
|
|
96
|
+
// Validate description
|
|
97
|
+
const descErrors = validateDescription(frontmatter.description);
|
|
98
|
+
for (const error of descErrors) {
|
|
99
|
+
diagnostics.push({ type: "error", message: error, path: filePath });
|
|
100
|
+
}
|
|
101
|
+
// Use name from frontmatter, or fall back to parent directory name
|
|
102
|
+
const name = frontmatter.name || parentDirName;
|
|
103
|
+
// Validate name
|
|
104
|
+
const nameErrors = validateName(name, parentDirName);
|
|
105
|
+
for (const error of nameErrors) {
|
|
106
|
+
diagnostics.push({ type: "warning", message: error, path: filePath });
|
|
107
|
+
}
|
|
108
|
+
// Missing description → skip skill entirely
|
|
109
|
+
if (!frontmatter.description || frontmatter.description.trim() === "") {
|
|
110
|
+
return { skill: null, diagnostics };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
skill: {
|
|
114
|
+
name,
|
|
115
|
+
description: frontmatter.description,
|
|
116
|
+
filePath,
|
|
117
|
+
baseDir: skillDir,
|
|
118
|
+
disableModelInvocation: frontmatter["disable-model-invocation"] === true,
|
|
119
|
+
},
|
|
120
|
+
diagnostics,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
const message = err instanceof Error ? err.message : "failed to parse skill file";
|
|
125
|
+
diagnostics.push({ type: "error", message, path: filePath });
|
|
126
|
+
return { skill: null, diagnostics };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ── Directory scanning ────────────────────────────────────────────────────
|
|
130
|
+
/**
|
|
131
|
+
* Load skills from a directory.
|
|
132
|
+
*
|
|
133
|
+
* Discovery rules (following the Agent Skills spec):
|
|
134
|
+
* - If a directory contains SKILL.md, treat it as a skill root (no further recursion)
|
|
135
|
+
* - Otherwise, recurse into subdirectories to find SKILL.md files
|
|
136
|
+
* - In agent-specific directories (~/.anita/skills/, .anita/skills/), root .md files
|
|
137
|
+
* are also discovered as individual skills
|
|
138
|
+
* - Skip dot-dirs, node_modules
|
|
139
|
+
*/
|
|
140
|
+
export function loadSkillsFromDir(dir, options = {}) {
|
|
141
|
+
return loadSkillsFromDirInternal(dir, options.includeRootFiles ?? false);
|
|
142
|
+
}
|
|
143
|
+
function loadSkillsFromDirInternal(dir, includeRootFiles) {
|
|
144
|
+
const skills = [];
|
|
145
|
+
const diagnostics = [];
|
|
146
|
+
if (!fs.existsSync(dir)) {
|
|
147
|
+
return { skills, diagnostics };
|
|
148
|
+
}
|
|
149
|
+
let entries;
|
|
150
|
+
try {
|
|
151
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return { skills, diagnostics };
|
|
155
|
+
}
|
|
156
|
+
// Check for SKILL.md at root → this directory IS a skill
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
if (entry.name !== "SKILL.md")
|
|
159
|
+
continue;
|
|
160
|
+
const fullPath = path.join(dir, entry.name);
|
|
161
|
+
let isFile = entry.isFile();
|
|
162
|
+
if (entry.isSymbolicLink()) {
|
|
163
|
+
try {
|
|
164
|
+
isFile = fs.statSync(fullPath).isFile();
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!isFile)
|
|
171
|
+
continue;
|
|
172
|
+
const result = loadSkillFromFile(fullPath);
|
|
173
|
+
if (result.skill)
|
|
174
|
+
skills.push(result.skill);
|
|
175
|
+
diagnostics.push(...result.diagnostics);
|
|
176
|
+
// This directory is a skill root; do not recurse further
|
|
177
|
+
return { skills, diagnostics };
|
|
178
|
+
}
|
|
179
|
+
// No SKILL.md at root → scan subdirectories and optionally root .md files
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
if (entry.name.startsWith("."))
|
|
182
|
+
continue;
|
|
183
|
+
if (entry.name === "node_modules")
|
|
184
|
+
continue;
|
|
185
|
+
const fullPath = path.join(dir, entry.name);
|
|
186
|
+
let isDirectory = entry.isDirectory();
|
|
187
|
+
let isFile = entry.isFile();
|
|
188
|
+
if (entry.isSymbolicLink()) {
|
|
189
|
+
try {
|
|
190
|
+
const stat = fs.statSync(fullPath);
|
|
191
|
+
isDirectory = stat.isDirectory();
|
|
192
|
+
isFile = stat.isFile();
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (isDirectory) {
|
|
199
|
+
const subResult = loadSkillsFromDirInternal(fullPath, false);
|
|
200
|
+
skills.push(...subResult.skills);
|
|
201
|
+
diagnostics.push(...subResult.diagnostics);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (isFile && includeRootFiles && entry.name.endsWith(".md")) {
|
|
205
|
+
const result = loadSkillFromFile(fullPath);
|
|
206
|
+
if (result.skill)
|
|
207
|
+
skills.push(result.skill);
|
|
208
|
+
diagnostics.push(...result.diagnostics);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { skills, diagnostics };
|
|
212
|
+
}
|
|
213
|
+
// ── Full skills loading ────────────────────────────────────────────────────
|
|
214
|
+
/**
|
|
215
|
+
* Load skills from all configured locations.
|
|
216
|
+
*
|
|
217
|
+
* Discovery order (later entries override earlier on name collision):
|
|
218
|
+
* 1. User-level: ~/.anita/skills/ and ~/.agents/skills/
|
|
219
|
+
* 2. Project-level: .anita/skills/ and .agents/skills/ (in cwd)
|
|
220
|
+
* 3. Explicit --skill paths (additive even with --no-skills)
|
|
221
|
+
*
|
|
222
|
+
* The agent-specific directory falls back to the legacy `.ada` location when
|
|
223
|
+
* `.anita` does not exist, so existing skill setups keep working.
|
|
224
|
+
*/
|
|
225
|
+
/*
|
|
226
|
+
* Resolve the agent-specific skills directory under `base`, preferring the
|
|
227
|
+
* canonical `.anita/skills` and falling back to the legacy `.ada/skills` when
|
|
228
|
+
* the canonical directory does not exist.
|
|
229
|
+
*/
|
|
230
|
+
function resolveAgentSkillsDir(base) {
|
|
231
|
+
const primary = path.join(base, ".anita", "skills");
|
|
232
|
+
if (fs.existsSync(primary))
|
|
233
|
+
return primary;
|
|
234
|
+
const legacy = path.join(base, ".ada", "skills");
|
|
235
|
+
if (fs.existsSync(legacy))
|
|
236
|
+
return legacy;
|
|
237
|
+
return primary;
|
|
238
|
+
}
|
|
239
|
+
export function loadSkills(options = {}) {
|
|
240
|
+
const { cwd = process.cwd(), skillPaths = [], includeDefaults = true } = options;
|
|
241
|
+
const skillMap = new Map();
|
|
242
|
+
const realPathSet = new Set();
|
|
243
|
+
const allDiagnostics = [];
|
|
244
|
+
function addSkills(result) {
|
|
245
|
+
allDiagnostics.push(...result.diagnostics);
|
|
246
|
+
for (const skill of result.skills) {
|
|
247
|
+
// Resolve symlinks to detect duplicate files
|
|
248
|
+
let realPath;
|
|
249
|
+
try {
|
|
250
|
+
realPath = fs.realpathSync(skill.filePath);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
realPath = skill.filePath;
|
|
254
|
+
}
|
|
255
|
+
// Skip if already loaded via symlink
|
|
256
|
+
if (realPathSet.has(realPath))
|
|
257
|
+
continue;
|
|
258
|
+
const existing = skillMap.get(skill.name);
|
|
259
|
+
if (existing) {
|
|
260
|
+
allDiagnostics.push({
|
|
261
|
+
type: "collision",
|
|
262
|
+
message: `name "${skill.name}" collision`,
|
|
263
|
+
path: skill.filePath,
|
|
264
|
+
collision: {
|
|
265
|
+
name: skill.name,
|
|
266
|
+
winnerPath: existing.filePath,
|
|
267
|
+
loserPath: skill.filePath,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
skillMap.set(skill.name, skill);
|
|
273
|
+
realPathSet.add(realPath);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (includeDefaults) {
|
|
278
|
+
const homeDir = os.homedir();
|
|
279
|
+
// User-level (agent-specific directory: includeRootFiles for standalone .md skills)
|
|
280
|
+
addSkills(loadSkillsFromDirInternal(resolveAgentSkillsDir(homeDir), true));
|
|
281
|
+
// User-level (cross-client .agents directory: no root .md files)
|
|
282
|
+
addSkills(loadSkillsFromDirInternal(path.join(homeDir, ".agents", "skills"), false));
|
|
283
|
+
// Project-level
|
|
284
|
+
addSkills(loadSkillsFromDirInternal(resolveAgentSkillsDir(cwd), true));
|
|
285
|
+
addSkills(loadSkillsFromDirInternal(path.join(cwd, ".agents", "skills"), false));
|
|
286
|
+
}
|
|
287
|
+
// Explicit skill paths (always additive)
|
|
288
|
+
for (const rawPath of skillPaths) {
|
|
289
|
+
const resolvedPath = resolveSkillPath(rawPath, cwd);
|
|
290
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
291
|
+
allDiagnostics.push({
|
|
292
|
+
type: "warning",
|
|
293
|
+
message: "skill path does not exist",
|
|
294
|
+
path: resolvedPath,
|
|
295
|
+
});
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const stat = fs.statSync(resolvedPath);
|
|
300
|
+
if (stat.isDirectory()) {
|
|
301
|
+
addSkills(loadSkillsFromDirInternal(resolvedPath, true));
|
|
302
|
+
}
|
|
303
|
+
else if (stat.isFile() && resolvedPath.endsWith(".md")) {
|
|
304
|
+
const result = loadSkillFromFile(resolvedPath);
|
|
305
|
+
if (result.skill) {
|
|
306
|
+
addSkills({ skills: [result.skill], diagnostics: result.diagnostics });
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
allDiagnostics.push(...result.diagnostics);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
allDiagnostics.push({
|
|
314
|
+
type: "warning",
|
|
315
|
+
message: "skill path is not a markdown file or directory",
|
|
316
|
+
path: resolvedPath,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
const message = err instanceof Error ? err.message : "failed to read skill path";
|
|
322
|
+
allDiagnostics.push({ type: "warning", message, path: resolvedPath });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
skills: Array.from(skillMap.values()),
|
|
327
|
+
diagnostics: allDiagnostics,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
// ── Path resolution ────────────────────────────────────────────────────────
|
|
331
|
+
function resolveSkillPath(rawPath, cwd) {
|
|
332
|
+
const trimmed = rawPath.trim();
|
|
333
|
+
if (trimmed === "~")
|
|
334
|
+
return os.homedir();
|
|
335
|
+
if (trimmed.startsWith("~/"))
|
|
336
|
+
return path.join(os.homedir(), trimmed.slice(2));
|
|
337
|
+
if (trimmed.startsWith("~"))
|
|
338
|
+
return path.join(os.homedir(), trimmed.slice(1));
|
|
339
|
+
return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
|
340
|
+
}
|
|
341
|
+
// ── Prompt formatting ─────────────────────────────────────────────────────
|
|
342
|
+
/**
|
|
343
|
+
* Format skills for inclusion in a system prompt.
|
|
344
|
+
* Uses XML format per the Agent Skills standard.
|
|
345
|
+
* See: https://agentskills.io/integrate-skills
|
|
346
|
+
*
|
|
347
|
+
* Skills with disableModelInvocation=true are excluded from the prompt
|
|
348
|
+
* (they can only be invoked explicitly via --skill or future command support).
|
|
349
|
+
*/
|
|
350
|
+
export function formatSkillsForPrompt(skills) {
|
|
351
|
+
const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
|
|
352
|
+
if (visibleSkills.length === 0)
|
|
353
|
+
return "";
|
|
354
|
+
const lines = [
|
|
355
|
+
"",
|
|
356
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
357
|
+
"Use the read_file tool to load a skill's file when the task matches its description.",
|
|
358
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent directory of SKILL.md) and use that absolute path in tool commands.",
|
|
359
|
+
"",
|
|
360
|
+
"<available_skills>",
|
|
361
|
+
];
|
|
362
|
+
for (const skill of visibleSkills) {
|
|
363
|
+
lines.push(" <skill>");
|
|
364
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
365
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
366
|
+
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
|
367
|
+
lines.push(" </skill>");
|
|
368
|
+
}
|
|
369
|
+
lines.push("</available_skills>");
|
|
370
|
+
return lines.join("\n");
|
|
371
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AgentEvent, EventType } from "../types/events.js";
|
|
2
|
+
export declare class EventStore {
|
|
3
|
+
private baseDir;
|
|
4
|
+
constructor(baseDir: string);
|
|
5
|
+
append(sessionId: string, type: EventType, data: Record<string, unknown>): Promise<AgentEvent>;
|
|
6
|
+
getEvents(sessionId: string): Promise<AgentEvent[]>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { v4 as uuidv4 } from "uuid";
|
|
4
|
+
export class EventStore {
|
|
5
|
+
baseDir;
|
|
6
|
+
constructor(baseDir) {
|
|
7
|
+
this.baseDir = baseDir;
|
|
8
|
+
}
|
|
9
|
+
async append(sessionId, type, data) {
|
|
10
|
+
const event = {
|
|
11
|
+
id: uuidv4(),
|
|
12
|
+
sessionId,
|
|
13
|
+
timestamp: new Date().toISOString(),
|
|
14
|
+
type,
|
|
15
|
+
data,
|
|
16
|
+
};
|
|
17
|
+
await fs.mkdir(this.baseDir, { recursive: true });
|
|
18
|
+
const filePath = path.join(this.baseDir, `${sessionId}.jsonl`);
|
|
19
|
+
await fs.appendFile(filePath, JSON.stringify(event) + "\n");
|
|
20
|
+
return event;
|
|
21
|
+
}
|
|
22
|
+
async getEvents(sessionId) {
|
|
23
|
+
const filePath = path.join(this.baseDir, `${sessionId}.jsonl`);
|
|
24
|
+
try {
|
|
25
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
26
|
+
return content
|
|
27
|
+
.trim()
|
|
28
|
+
.split("\n")
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.map((line) => JSON.parse(line));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SessionState } from "../types/agent.js";
|
|
2
|
+
export declare class SessionStore {
|
|
3
|
+
private baseDir;
|
|
4
|
+
constructor(baseDir: string);
|
|
5
|
+
save(session: SessionState): Promise<void>;
|
|
6
|
+
load(sessionId: string): Promise<SessionState | null>;
|
|
7
|
+
list(includeArchived?: boolean): Promise<SessionState[]>;
|
|
8
|
+
archive(sessionId: string): Promise<void>;
|
|
9
|
+
getLatest(): Promise<SessionState | null>;
|
|
10
|
+
private normalize;
|
|
11
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { conversationItemsToMessages, messagesToConversationItems, } from "../types/conversation.js";
|
|
4
|
+
export class SessionStore {
|
|
5
|
+
baseDir;
|
|
6
|
+
constructor(baseDir) {
|
|
7
|
+
this.baseDir = baseDir;
|
|
8
|
+
}
|
|
9
|
+
async save(session) {
|
|
10
|
+
await fs.mkdir(this.baseDir, { recursive: true });
|
|
11
|
+
const filePath = path.join(this.baseDir, `${session.id}.json`);
|
|
12
|
+
await fs.writeFile(filePath, JSON.stringify(this.normalize(session), null, 2));
|
|
13
|
+
}
|
|
14
|
+
async load(sessionId) {
|
|
15
|
+
const filePath = path.join(this.baseDir, `${sessionId}.json`);
|
|
16
|
+
try {
|
|
17
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
18
|
+
return this.normalize(JSON.parse(content));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async list(includeArchived = false) {
|
|
25
|
+
try {
|
|
26
|
+
const files = await fs.readdir(this.baseDir);
|
|
27
|
+
const sessions = [];
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
if (!file.endsWith(".json"))
|
|
30
|
+
continue;
|
|
31
|
+
const content = await fs.readFile(path.join(this.baseDir, file), "utf-8");
|
|
32
|
+
const session = this.normalize(JSON.parse(content));
|
|
33
|
+
if (!includeArchived && session.status === "archived")
|
|
34
|
+
continue;
|
|
35
|
+
sessions.push(session);
|
|
36
|
+
}
|
|
37
|
+
sessions.sort((a, b) => new Date(b.lastActiveAt).getTime() -
|
|
38
|
+
new Date(a.lastActiveAt).getTime());
|
|
39
|
+
return sessions;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async archive(sessionId) {
|
|
46
|
+
const session = await this.load(sessionId);
|
|
47
|
+
if (!session) {
|
|
48
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
49
|
+
}
|
|
50
|
+
session.status = "archived";
|
|
51
|
+
await this.save(session);
|
|
52
|
+
}
|
|
53
|
+
async getLatest() {
|
|
54
|
+
const sessions = await this.list();
|
|
55
|
+
return sessions[0] ?? null;
|
|
56
|
+
}
|
|
57
|
+
normalize(session) {
|
|
58
|
+
if (!Array.isArray(session.conversationItems)) {
|
|
59
|
+
session.conversationItems = messagesToConversationItems(session.messages ?? []);
|
|
60
|
+
}
|
|
61
|
+
session.messages = conversationItemsToMessages(session.conversationItems);
|
|
62
|
+
return session;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
export const deleteFileTool = {
|
|
3
|
+
name: "delete_file",
|
|
4
|
+
description: "Delete a file at the given path. Fails if the file does not exist.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
path: { type: "string", description: "Absolute or relative file path" },
|
|
9
|
+
},
|
|
10
|
+
required: ["path"],
|
|
11
|
+
},
|
|
12
|
+
async execute(input) {
|
|
13
|
+
const filePath = input.path;
|
|
14
|
+
try {
|
|
15
|
+
await fs.unlink(filePath);
|
|
16
|
+
return { content: `File deleted: ${filePath}` };
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return {
|
|
20
|
+
content: `Error deleting file: ${err.message}`,
|
|
21
|
+
isError: true,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
export const editFileTool = {
|
|
3
|
+
name: "edit_file",
|
|
4
|
+
description: "Edit a file by replacing an exact string match. The old_text must appear exactly once in the file. Use read_file first to see the current content.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
path: { type: "string", description: "Absolute or relative file path" },
|
|
9
|
+
old_text: {
|
|
10
|
+
type: "string",
|
|
11
|
+
description: "The exact text to find and replace",
|
|
12
|
+
},
|
|
13
|
+
new_text: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "The text to replace it with",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ["path", "old_text", "new_text"],
|
|
19
|
+
},
|
|
20
|
+
async execute(input) {
|
|
21
|
+
const filePath = input.path;
|
|
22
|
+
const oldText = input.old_text;
|
|
23
|
+
const newText = input.new_text;
|
|
24
|
+
try {
|
|
25
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
26
|
+
const occurrences = content.split(oldText).length - 1;
|
|
27
|
+
if (occurrences === 0) {
|
|
28
|
+
return {
|
|
29
|
+
content: `Error: old_text not found in ${filePath}. Use read_file to check the current content.`,
|
|
30
|
+
isError: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (occurrences > 1) {
|
|
34
|
+
return {
|
|
35
|
+
content: `Error: old_text found ${occurrences} times in ${filePath}. Provide a more specific string.`,
|
|
36
|
+
isError: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const newContent = content.replace(oldText, newText);
|
|
40
|
+
await fs.writeFile(filePath, newContent);
|
|
41
|
+
return { content: `File edited: ${filePath}` };
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return {
|
|
45
|
+
content: `Error editing file: ${err.message}`,
|
|
46
|
+
isError: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|