@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.
Files changed (66) hide show
  1. package/README.md +353 -0
  2. package/dist/agent/agents.d.ts +16 -0
  3. package/dist/agent/agents.js +115 -0
  4. package/dist/agent/context-budget.d.ts +7 -0
  5. package/dist/agent/context-budget.js +17 -0
  6. package/dist/agent/context-builder.d.ts +34 -0
  7. package/dist/agent/context-builder.js +175 -0
  8. package/dist/agent/executor.d.ts +13 -0
  9. package/dist/agent/executor.js +65 -0
  10. package/dist/agent/loop.d.ts +54 -0
  11. package/dist/agent/loop.js +548 -0
  12. package/dist/agent/policies.d.ts +25 -0
  13. package/dist/agent/policies.js +177 -0
  14. package/dist/agent/session.d.ts +12 -0
  15. package/dist/agent/session.js +42 -0
  16. package/dist/attachments.d.ts +3 -0
  17. package/dist/attachments.js +73 -0
  18. package/dist/cli/index.d.ts +5 -0
  19. package/dist/cli/index.js +327 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +4 -0
  22. package/dist/models/anthropic.d.ts +15 -0
  23. package/dist/models/anthropic.js +195 -0
  24. package/dist/models/openai-responses.d.ts +62 -0
  25. package/dist/models/openai-responses.js +377 -0
  26. package/dist/models/openai.d.ts +32 -0
  27. package/dist/models/openai.js +330 -0
  28. package/dist/models/provider.d.ts +33 -0
  29. package/dist/models/provider.js +1 -0
  30. package/dist/models/resolve.d.ts +48 -0
  31. package/dist/models/resolve.js +211 -0
  32. package/dist/security/sensitive-content.d.ts +6 -0
  33. package/dist/security/sensitive-content.js +59 -0
  34. package/dist/skills/skills.d.ts +62 -0
  35. package/dist/skills/skills.js +371 -0
  36. package/dist/storage/event-store.d.ts +7 -0
  37. package/dist/storage/event-store.js +36 -0
  38. package/dist/storage/session-store.d.ts +11 -0
  39. package/dist/storage/session-store.js +64 -0
  40. package/dist/tools/delete-file.d.ts +2 -0
  41. package/dist/tools/delete-file.js +25 -0
  42. package/dist/tools/edit-file.d.ts +2 -0
  43. package/dist/tools/edit-file.js +50 -0
  44. package/dist/tools/read-file.d.ts +2 -0
  45. package/dist/tools/read-file.js +122 -0
  46. package/dist/tools/registry.d.ts +9 -0
  47. package/dist/tools/registry.js +122 -0
  48. package/dist/tools/run-command.d.ts +2 -0
  49. package/dist/tools/run-command.js +103 -0
  50. package/dist/tools/write-file.d.ts +2 -0
  51. package/dist/tools/write-file.js +29 -0
  52. package/dist/types/agent.d.ts +44 -0
  53. package/dist/types/agent.js +1 -0
  54. package/dist/types/conversation.d.ts +43 -0
  55. package/dist/types/conversation.js +201 -0
  56. package/dist/types/events.d.ts +8 -0
  57. package/dist/types/events.js +1 -0
  58. package/dist/types/messages.d.ts +39 -0
  59. package/dist/types/messages.js +1 -0
  60. package/dist/types/output.d.ts +19 -0
  61. package/dist/types/output.js +1 -0
  62. package/dist/types/stream.d.ts +55 -0
  63. package/dist/types/stream.js +1 -0
  64. package/dist/types/tools.d.ts +28 -0
  65. package/dist/types/tools.js +1 -0
  66. package/package.json +45 -0
@@ -0,0 +1,177 @@
1
+ const SAFE_COMMAND_PATTERNS = [
2
+ /^ls\b/,
3
+ /^cat\b/,
4
+ /^head\b/,
5
+ /^tail\b/,
6
+ /^wc\b/,
7
+ /^find\b/,
8
+ /^grep\b/,
9
+ /^git\s+(status|diff|log|branch|show)\b/,
10
+ /^pwd$/,
11
+ /^echo\b/,
12
+ /^which\b/,
13
+ /^node\s+--version/,
14
+ /^npm\s+(list|ls|outdated|view)\b/,
15
+ ];
16
+ const DANGEROUS_PATTERNS = [
17
+ /rm\s+-rf\s+\//,
18
+ /^sudo\b/,
19
+ />\s*\/dev\/sd/,
20
+ /mkfs\b/,
21
+ /dd\s+if=/,
22
+ ];
23
+ const DEFAULT_POLICY_CONTEXT = {
24
+ defaultDecision: "allow",
25
+ rules: [
26
+ {
27
+ toolName: "read_file",
28
+ decision: "allow",
29
+ description: "File reads are allowed.",
30
+ },
31
+ {
32
+ toolName: "write_file",
33
+ decision: "allow",
34
+ description: "File writes and overwrites are allowed.",
35
+ },
36
+ {
37
+ toolName: "edit_file",
38
+ decision: "allow",
39
+ description: "Exact-match file edits are allowed.",
40
+ },
41
+ {
42
+ toolName: "delete_file",
43
+ decision: "allow",
44
+ description: "File deletion is allowed.",
45
+ },
46
+ {
47
+ toolName: "run_command",
48
+ decision: "deny",
49
+ description: "Commands matching dangerous patterns such as sudo, disk formatting, raw device writes, or rm -rf / are denied.",
50
+ },
51
+ {
52
+ toolName: "run_command",
53
+ decision: "allow",
54
+ description: "Common inspection commands are allowed, including ls, cat, head, tail, wc, find, rg, grep fallback when rg is unavailable, git status/diff/log/branch/show, pwd, echo, which, node --version, and npm list/ls/outdated/view.",
55
+ },
56
+ {
57
+ toolName: "run_command",
58
+ decision: "ask",
59
+ description: "Other shell commands require approval unless auto-approval is enabled.",
60
+ },
61
+ ],
62
+ };
63
+ export class PolicyEngine {
64
+ policyContext;
65
+ rules = [];
66
+ constructor(policyContext = DEFAULT_POLICY_CONTEXT) {
67
+ this.policyContext = policyContext;
68
+ }
69
+ addRule(rule) {
70
+ this.rules.push(rule);
71
+ }
72
+ evaluate(toolName, input) {
73
+ for (const rule of this.rules) {
74
+ if (rule.toolName === toolName) {
75
+ return rule.decide(input);
76
+ }
77
+ }
78
+ return "allow";
79
+ }
80
+ describe() {
81
+ return this.policyContext;
82
+ }
83
+ static withDefaults() {
84
+ const engine = new PolicyEngine();
85
+ engine.addRule({
86
+ toolName: "read_file",
87
+ decide: () => "allow",
88
+ });
89
+ engine.addRule({
90
+ toolName: "write_file",
91
+ decide: () => "allow",
92
+ });
93
+ engine.addRule({
94
+ toolName: "edit_file",
95
+ decide: () => "allow",
96
+ });
97
+ engine.addRule({
98
+ toolName: "run_command",
99
+ decide: (input) => {
100
+ const cmd = input.command.trim();
101
+ for (const pattern of DANGEROUS_PATTERNS) {
102
+ if (pattern.test(cmd))
103
+ return "deny";
104
+ }
105
+ if (isRipgrepCommand(cmd)) {
106
+ return usesRipgrepPreprocessor(cmd) ? "ask" : "allow";
107
+ }
108
+ for (const pattern of SAFE_COMMAND_PATTERNS) {
109
+ if (pattern.test(cmd))
110
+ return "allow";
111
+ }
112
+ return "ask";
113
+ },
114
+ });
115
+ return engine;
116
+ }
117
+ }
118
+ function isRipgrepCommand(cmd) {
119
+ return /^rg\b/.test(cmd);
120
+ }
121
+ function usesRipgrepPreprocessor(cmd) {
122
+ const tokens = tokenizeShellCommand(cmd);
123
+ let parsingFlags = true;
124
+ for (const token of tokens.slice(1)) {
125
+ if (!parsingFlags)
126
+ return false;
127
+ if (token === "--") {
128
+ parsingFlags = false;
129
+ continue;
130
+ }
131
+ if (token === "--pre" || token.startsWith("--pre=")) {
132
+ return true;
133
+ }
134
+ }
135
+ return false;
136
+ }
137
+ function tokenizeShellCommand(cmd) {
138
+ const tokens = [];
139
+ let current = "";
140
+ let quote = null;
141
+ for (let i = 0; i < cmd.length; i += 1) {
142
+ const char = cmd[i];
143
+ if (quote) {
144
+ if (char === quote) {
145
+ quote = null;
146
+ }
147
+ else if (char === "\\" && quote === "\"" && i + 1 < cmd.length) {
148
+ i += 1;
149
+ current += cmd[i];
150
+ }
151
+ else {
152
+ current += char;
153
+ }
154
+ continue;
155
+ }
156
+ if (char === "'" || char === "\"") {
157
+ quote = char;
158
+ continue;
159
+ }
160
+ if (/\s/.test(char)) {
161
+ if (current) {
162
+ tokens.push(current);
163
+ current = "";
164
+ }
165
+ continue;
166
+ }
167
+ if (char === "\\" && i + 1 < cmd.length) {
168
+ i += 1;
169
+ current += cmd[i];
170
+ continue;
171
+ }
172
+ current += char;
173
+ }
174
+ if (current)
175
+ tokens.push(current);
176
+ return tokens;
177
+ }
@@ -0,0 +1,12 @@
1
+ import type { SessionState } from "../types/agent.js";
2
+ import type { SessionStore } from "../storage/session-store.js";
3
+ import type { EventStore } from "../storage/event-store.js";
4
+ export declare class SessionManager {
5
+ private sessionStore;
6
+ private eventStore;
7
+ constructor(sessionStore: SessionStore, eventStore: EventStore);
8
+ createSession(workingDirectory: string, model: string): Promise<SessionState>;
9
+ resumeSession(sessionId: string): Promise<SessionState>;
10
+ listSessions(includeArchived?: boolean): Promise<SessionState[]>;
11
+ archiveSession(sessionId: string): Promise<void>;
12
+ }
@@ -0,0 +1,42 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ export class SessionManager {
3
+ sessionStore;
4
+ eventStore;
5
+ constructor(sessionStore, eventStore) {
6
+ this.sessionStore = sessionStore;
7
+ this.eventStore = eventStore;
8
+ }
9
+ async createSession(workingDirectory, model) {
10
+ const session = {
11
+ id: uuidv4(),
12
+ workingDirectory,
13
+ model,
14
+ conversationItems: [],
15
+ messages: [],
16
+ createdAt: new Date().toISOString(),
17
+ lastActiveAt: new Date().toISOString(),
18
+ status: "active",
19
+ };
20
+ await this.sessionStore.save(session);
21
+ await this.eventStore.append(session.id, "session_start", {
22
+ workingDirectory,
23
+ model,
24
+ });
25
+ return session;
26
+ }
27
+ async resumeSession(sessionId) {
28
+ const session = await this.sessionStore.load(sessionId);
29
+ if (!session) {
30
+ throw new Error(`Session not found: ${sessionId}`);
31
+ }
32
+ session.status = "active";
33
+ return session;
34
+ }
35
+ async listSessions(includeArchived = false) {
36
+ return this.sessionStore.list(includeArchived);
37
+ }
38
+ async archiveSession(sessionId) {
39
+ await this.sessionStore.archive(sessionId);
40
+ await this.eventStore.append(sessionId, "session_archived", {});
41
+ }
42
+ }
@@ -0,0 +1,3 @@
1
+ import type { ModelCapabilities } from "./models/resolve.js";
2
+ import type { AttachmentContentBlock } from "./types/messages.js";
3
+ export declare function loadAttachments(inputs: string[], capabilities: ModelCapabilities, cwd: string): Promise<AttachmentContentBlock[]>;
@@ -0,0 +1,73 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const IMAGE_MEDIA_TYPES = new Map([
4
+ [".gif", "image/gif"],
5
+ [".jpeg", "image/jpeg"],
6
+ [".jpg", "image/jpeg"],
7
+ [".png", "image/png"],
8
+ [".webp", "image/webp"],
9
+ ]);
10
+ const PDF_MEDIA_TYPE = "application/pdf";
11
+ export async function loadAttachments(inputs, capabilities, cwd) {
12
+ if (inputs.length === 0)
13
+ return [];
14
+ const attachmentCapabilities = capabilities.attachments;
15
+ if (!attachmentCapabilities) {
16
+ throw new Error("The selected model does not support attachments.");
17
+ }
18
+ const attachments = [];
19
+ for (const input of inputs) {
20
+ const attachment = await loadAttachment(input, cwd);
21
+ if (attachment.type === "image" && !attachmentCapabilities.images) {
22
+ throw new Error(`The selected model does not support image attachments: ${input}`);
23
+ }
24
+ if (attachment.type === "file" && !attachmentCapabilities.files) {
25
+ throw new Error(`The selected model does not support file attachments: ${input}`);
26
+ }
27
+ attachments.push(attachment);
28
+ }
29
+ return attachments;
30
+ }
31
+ async function loadAttachment(input, cwd) {
32
+ const name = path.basename(input);
33
+ const mediaType = mediaTypeFromPath(input);
34
+ if (!mediaType) {
35
+ throw new Error(`Unsupported attachment type: ${input}. Supported: png, jpg, jpeg, webp, gif, pdf.`);
36
+ }
37
+ const source = isHttpUrl(input)
38
+ ? urlSource(input)
39
+ : await dataSource(path.resolve(cwd, input), mediaType);
40
+ if (mediaType === PDF_MEDIA_TYPE) {
41
+ return { type: "file", name, mediaType, source };
42
+ }
43
+ return { type: "image", name, source };
44
+ }
45
+ function mediaTypeFromPath(input) {
46
+ const extension = path.extname(urlPathname(input)).toLowerCase();
47
+ return IMAGE_MEDIA_TYPES.get(extension) ?? (extension === ".pdf" ? PDF_MEDIA_TYPE : undefined);
48
+ }
49
+ function urlPathname(input) {
50
+ if (!isHttpUrl(input))
51
+ return input;
52
+ return new URL(input).pathname;
53
+ }
54
+ function isHttpUrl(input) {
55
+ try {
56
+ const url = new URL(input);
57
+ return url.protocol === "http:" || url.protocol === "https:";
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ function urlSource(url) {
64
+ return { type: "url", url };
65
+ }
66
+ async function dataSource(filePath, mediaType) {
67
+ const bytes = await fs.readFile(filePath);
68
+ return {
69
+ type: "data",
70
+ mediaType,
71
+ data: bytes.toString("base64"),
72
+ };
73
+ }
@@ -0,0 +1,5 @@
1
+ import { Command } from "commander";
2
+ import { type ModelOption, type ModelOptionsResult } from "../models/resolve.js";
3
+ export declare function formatModelOptions(options?: readonly ModelOption[]): string;
4
+ export declare function formatModelOptionsJson(result: ModelOptionsResult): string;
5
+ export declare function createCLI(): Command;
@@ -0,0 +1,327 @@
1
+ import readline from "node:readline";
2
+ import fs from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+ import { EventStore } from "../storage/event-store.js";
8
+ import { SessionStore } from "../storage/session-store.js";
9
+ import { ToolRegistry } from "../tools/registry.js";
10
+ import { readFileTool } from "../tools/read-file.js";
11
+ import { writeFileTool } from "../tools/write-file.js";
12
+ import { editFileTool } from "../tools/edit-file.js";
13
+ import { runCommandTool } from "../tools/run-command.js";
14
+ import { deleteFileTool } from "../tools/delete-file.js";
15
+ import { PolicyEngine } from "../agent/policies.js";
16
+ import { ContextBuilder } from "../agent/context-builder.js";
17
+ import { getGlobalAgentsPath, getRepositoryAgentsPath, } from "../agent/agents.js";
18
+ import { Executor } from "../agent/executor.js";
19
+ import { AgentLoop, } from "../agent/loop.js";
20
+ import { SessionManager } from "../agent/session.js";
21
+ import { loadAttachments } from "../attachments.js";
22
+ import { createProvider, getModelCapabilities, getModelOptions, groupModelOptions, MODEL_OPTIONS, } from "../models/resolve.js";
23
+ import { loadSkills } from "../skills/skills.js";
24
+ const DEFAULT_MODEL = "ollama/glm-4.7-flash:latest";
25
+ const AGENTS_TEMPLATE = `# AGENTS.md
26
+
27
+ Describe the coding guidelines, project conventions, and operational constraints Anita should follow.
28
+ `;
29
+ function getStoragePaths(cwd) {
30
+ const base = resolveStorageBase(cwd);
31
+ return {
32
+ events: path.join(base, "events"),
33
+ sessions: path.join(base, "sessions"),
34
+ };
35
+ }
36
+ /*
37
+ * Resolve the session-storage directory, preferring the canonical `.anita`
38
+ * location. Because `.anita` may already exist for skills, presence of a
39
+ * `sessions/` subdirectory is used to detect real storage: fall back to the
40
+ * legacy `.coding-agent` directory only when it holds existing sessions.
41
+ */
42
+ function resolveStorageBase(cwd) {
43
+ const primary = path.join(cwd, ".anita");
44
+ if (existsSync(path.join(primary, "sessions")))
45
+ return primary;
46
+ const legacy = path.join(cwd, ".coding-agent");
47
+ if (existsSync(path.join(legacy, "sessions")))
48
+ return legacy;
49
+ return primary;
50
+ }
51
+ export function formatModelOptions(options = MODEL_OPTIONS) {
52
+ return groupModelOptions(options)
53
+ .map((group) => {
54
+ const lines = group.options.map((option) => ` ${option.value.padEnd(38)} ${option.label}${formatCapabilities(option)}`);
55
+ return [group.group, ...lines].join("\n");
56
+ })
57
+ .join("\n\n");
58
+ }
59
+ export function formatModelOptionsJson(result) {
60
+ return JSON.stringify({
61
+ models: result.options,
62
+ ollamaDiscoveryFailed: result.ollamaDiscoveryFailed,
63
+ }, null, 2);
64
+ }
65
+ function formatCapabilities(option) {
66
+ const attachments = option.capabilities?.attachments;
67
+ if (!attachments)
68
+ return "";
69
+ const supported = [
70
+ attachments.images ? "images" : undefined,
71
+ attachments.files ? "files" : undefined,
72
+ ].filter((item) => item !== undefined);
73
+ return supported.length > 0 ? ` [${supported.join(", ")}]` : "";
74
+ }
75
+ async function createAgentsFile(filePath, force) {
76
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
77
+ try {
78
+ await fs.writeFile(filePath, AGENTS_TEMPLATE, {
79
+ encoding: "utf-8",
80
+ flag: force ? "w" : "wx",
81
+ });
82
+ }
83
+ catch (err) {
84
+ if (isAlreadyExistsError(err)) {
85
+ throw new Error(`AGENTS.md already exists at ${filePath}. Use --force to overwrite it.`);
86
+ }
87
+ throw err;
88
+ }
89
+ }
90
+ function isAlreadyExistsError(err) {
91
+ return err instanceof Error && "code" in err && err.code === "EEXIST";
92
+ }
93
+ async function askApproval(toolName, input) {
94
+ const rl = readline.createInterface({
95
+ input: process.stdin,
96
+ output: process.stdout,
97
+ });
98
+ const summary = toolName === "run_command" ? input.command : JSON.stringify(input);
99
+ const answer = await new Promise((resolve) => {
100
+ rl.question(chalk.yellow(`Allow ${toolName}: ${summary}? [y/n] `), resolve);
101
+ });
102
+ rl.close();
103
+ return answer.toLowerCase().startsWith("y");
104
+ }
105
+ export function createCLI() {
106
+ const program = new Command();
107
+ program
108
+ .name("anita")
109
+ .description("An AI coding agent")
110
+ .version("0.3.0")
111
+ .option("--model <model>", "Model to use (provider/model)", DEFAULT_MODEL)
112
+ .option("--system-prompt <prompt>", "Additional system prompt instructions")
113
+ .option("--stream-json", "Emit structured JSON events to stdout")
114
+ .option("--auto-approve", "Auto-approve tool calls (dangerous commands are still denied)");
115
+ program
116
+ .command("models")
117
+ .description("List supported model choices")
118
+ .option("--json", "Emit machine-readable JSON")
119
+ .action(async (options) => {
120
+ const result = await getModelOptions();
121
+ if (options.json) {
122
+ console.log(formatModelOptionsJson(result));
123
+ return;
124
+ }
125
+ console.log(formatModelOptions(result.options));
126
+ if (result.ollamaDiscoveryFailed) {
127
+ console.error(chalk.gray("Note: Local Ollama discovery unavailable; showing built-in local models."));
128
+ }
129
+ });
130
+ program
131
+ .command("agents")
132
+ .description("Manage AGENTS.md instruction files")
133
+ .command("init")
134
+ .description("Create an AGENTS.md instruction file")
135
+ .option("--global", "Create ~/.anita/AGENTS.md instead of repository AGENTS.md")
136
+ .option("--force", "Overwrite an existing AGENTS.md")
137
+ .action(async (options) => {
138
+ const filePath = options.global
139
+ ? getGlobalAgentsPath()
140
+ : getRepositoryAgentsPath(process.cwd());
141
+ try {
142
+ await createAgentsFile(filePath, options.force ?? false);
143
+ console.log(chalk.green(`Created ${filePath}`));
144
+ }
145
+ catch (err) {
146
+ console.error(chalk.red(err.message));
147
+ process.exit(1);
148
+ }
149
+ });
150
+ program
151
+ .command("chat")
152
+ .argument("<message>", "Message to send to the agent")
153
+ .option("--resume <sessionId>", "Resume a previous session")
154
+ .option("--model <model>", "Model to use (provider/model)")
155
+ .option("--system-prompt <prompt>", "Additional system prompt instructions")
156
+ .option("--stream-json", "Emit structured JSON events to stdout")
157
+ .option("--auto-approve", "Auto-approve tool calls (dangerous commands are still denied)")
158
+ .option("--attach <path-or-url>", "Attach an image or PDF to the user message (repeatable)", (val, prev) => prev.concat(val), [])
159
+ .option("--skill <path>", "Load a skill from a file or directory (repeatable)", (val, prev) => prev.concat(val), [])
160
+ .option("--no-skills", "Disable automatic skill discovery (explicit --skill paths still load)")
161
+ .action(async (message, options) => {
162
+ const parentOpts = program.opts();
163
+ const model = options.model ?? parentOpts.model;
164
+ const autoApprove = options.autoApprove ?? parentOpts.autoApprove;
165
+ const streamJson = options.streamJson ?? parentOpts.streamJson ?? false;
166
+ const systemPrompt = options.systemPrompt ?? parentOpts.systemPrompt;
167
+ const cwd = process.cwd();
168
+ const paths = getStoragePaths(cwd);
169
+ const eventStore = new EventStore(paths.events);
170
+ const sessionStore = new SessionStore(paths.sessions);
171
+ const sessionManager = new SessionManager(sessionStore, eventStore);
172
+ // Create or resume session
173
+ const session = options.resume
174
+ ? await sessionManager.resumeSession(options.resume)
175
+ : await sessionManager.createSession(cwd, model);
176
+ const explicitModel = options.model ?? (program.getOptionValueSource("model") === "cli" ? parentOpts.model : undefined);
177
+ const modelString = options.resume ? (explicitModel ?? session.model) : model;
178
+ const provider = createProvider(modelString);
179
+ const attachments = await loadAttachments(options.attach ?? [], getModelCapabilities(modelString), cwd);
180
+ // Set up tools
181
+ const registry = new ToolRegistry();
182
+ registry.register(readFileTool);
183
+ registry.register(writeFileTool);
184
+ registry.register(editFileTool);
185
+ registry.register(runCommandTool);
186
+ registry.register(deleteFileTool);
187
+ const policyEngine = PolicyEngine.withDefaults();
188
+ // Load skills
189
+ const skillPaths = options.skill ?? [];
190
+ const includeDefaults = options.skills !== false;
191
+ const { skills, diagnostics } = loadSkills({ cwd, skillPaths, includeDefaults });
192
+ // Log diagnostics for skill loading issues
193
+ for (const diag of diagnostics) {
194
+ if (diag.type === "error" || diag.type === "collision") {
195
+ console.error(chalk.yellow(`Skill warning: ${diag.message} (${diag.path})`));
196
+ }
197
+ }
198
+ // Log skills_loaded event
199
+ if (skills.length > 0) {
200
+ await eventStore.append(session.id, "skills_loaded", {
201
+ skills: skills.map((s) => ({
202
+ name: s.name,
203
+ description: s.description,
204
+ filePath: s.filePath,
205
+ })),
206
+ });
207
+ if (!streamJson) {
208
+ console.log(chalk.gray(`Skills: ${skills.map((s) => s.name).join(", ")}`));
209
+ }
210
+ }
211
+ const contextBuilder = new ContextBuilder(cwd, {
212
+ approvalMode: autoApprove ? "auto" : "prompt",
213
+ networkAccess: "unknown",
214
+ shell: process.env.SHELL,
215
+ writeScope: "Prefer the working directory and its descendants unless the user explicitly asks for another path.",
216
+ policyContext: policyEngine.describe(),
217
+ skills,
218
+ systemPrompt,
219
+ });
220
+ const approvalFn = autoApprove ? async () => true : askApproval;
221
+ const executor = new Executor(registry, policyEngine, eventStore, approvalFn);
222
+ const loop = new AgentLoop(provider, executor, contextBuilder, registry, eventStore, sessionStore, streamJson);
223
+ if (!streamJson) {
224
+ console.log(chalk.gray(`Session: ${session.id}`));
225
+ if (session.title) {
226
+ console.log(chalk.gray(`Title: ${session.title}`));
227
+ }
228
+ console.log(chalk.gray(`Model: ${modelString}`));
229
+ console.log();
230
+ }
231
+ const abortController = new AbortController();
232
+ let interrupted = false;
233
+ const onSigint = () => {
234
+ if (interrupted) {
235
+ // A second Ctrl-C forces an immediate exit if cancellation hangs.
236
+ process.exit(130);
237
+ }
238
+ interrupted = true;
239
+ abortController.abort();
240
+ if (!streamJson) {
241
+ console.log(chalk.yellow("\nCancelling run… (press Ctrl-C again to force quit)"));
242
+ }
243
+ };
244
+ process.on("SIGINT", onSigint);
245
+ try {
246
+ await loop.run(session, message, attachments, abortController.signal);
247
+ }
248
+ catch (err) {
249
+ const errorMessage = err.message;
250
+ if (streamJson) {
251
+ console.log(JSON.stringify({
252
+ type: "run.failed",
253
+ sessionId: session.id,
254
+ error: errorMessage,
255
+ timestamp: new Date().toISOString(),
256
+ }));
257
+ }
258
+ else {
259
+ console.error(chalk.red(`Error: ${errorMessage}`));
260
+ }
261
+ process.exit(1);
262
+ }
263
+ finally {
264
+ process.off("SIGINT", onSigint);
265
+ }
266
+ if (interrupted) {
267
+ process.exit(130);
268
+ }
269
+ });
270
+ program
271
+ .command("sessions")
272
+ .description("List past sessions")
273
+ .option("--archived", "Include archived sessions")
274
+ .action(async (options) => {
275
+ const cwd = process.cwd();
276
+ const paths = getStoragePaths(cwd);
277
+ const sessionStore = new SessionStore(paths.sessions);
278
+ const sessions = await sessionStore.list(options.archived ?? false);
279
+ if (sessions.length === 0) {
280
+ console.log("No sessions found.");
281
+ return;
282
+ }
283
+ for (const s of sessions) {
284
+ const msgCount = s.messages.length;
285
+ const title = s.title ?? '(untitled)';
286
+ const archived = s.status === "archived" ? chalk.yellow(" [archived]") : "";
287
+ console.log(`${chalk.cyan(s.id.slice(0, 8))} ${chalk.white.bold(title)}${archived} ${chalk.gray(`${s.model} ${msgCount} msgs ${s.lastActiveAt}`)}`);
288
+ }
289
+ });
290
+ program
291
+ .command("archive")
292
+ .argument("<sessionId>", "Session ID to archive")
293
+ .description("Archive a session")
294
+ .action(async (sessionId) => {
295
+ const cwd = process.cwd();
296
+ const paths = getStoragePaths(cwd);
297
+ const eventStore = new EventStore(paths.events);
298
+ const sessionStore = new SessionStore(paths.sessions);
299
+ const sessionManager = new SessionManager(sessionStore, eventStore);
300
+ try {
301
+ await sessionManager.archiveSession(sessionId);
302
+ console.log(chalk.green(`Session ${sessionId.slice(0, 8)} archived.`));
303
+ }
304
+ catch (err) {
305
+ console.error(chalk.red(err.message));
306
+ process.exit(1);
307
+ }
308
+ });
309
+ program
310
+ .command("events")
311
+ .argument("<sessionId>", "Session ID to view events for")
312
+ .description("View event log for a session")
313
+ .action(async (sessionId) => {
314
+ const cwd = process.cwd();
315
+ const paths = getStoragePaths(cwd);
316
+ const eventStore = new EventStore(paths.events);
317
+ const events = await eventStore.getEvents(sessionId);
318
+ if (events.length === 0) {
319
+ console.log("No events found.");
320
+ return;
321
+ }
322
+ for (const e of events) {
323
+ console.log(`${chalk.gray(e.timestamp)} ${chalk.yellow(e.type)} ${JSON.stringify(e.data).slice(0, 120)}`);
324
+ }
325
+ });
326
+ return program;
327
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { createCLI } from "./cli/index.js";
3
+ const program = createCLI();
4
+ program.parse();
@@ -0,0 +1,15 @@
1
+ import type { ChatParams, ModelProvider, ModelStreamEvent } from "./provider.js";
2
+ import type { ModelResponse } from "../types/agent.js";
3
+ export declare class AnthropicProvider implements ModelProvider {
4
+ private client;
5
+ private model;
6
+ constructor(model: string);
7
+ chat(params: ChatParams): Promise<ModelResponse>;
8
+ streamChat(params: ChatParams): AsyncIterable<ModelStreamEvent>;
9
+ private toAnthropicMessage;
10
+ private toAnthropicTool;
11
+ private fromAnthropicBlock;
12
+ private mapStopReason;
13
+ private parseToolInput;
14
+ private isRecord;
15
+ }