@averagejoeslab/puppuccino-core 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1949 @@
1
+ // src/config/index.ts
2
+ import { z } from "zod";
3
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
4
+ import { join, dirname } from "path";
5
+ import { homedir } from "os";
6
+ var MCPServerSchema = z.object({
7
+ name: z.string(),
8
+ transport: z.enum(["stdio", "http", "sse"]),
9
+ command: z.string().optional(),
10
+ args: z.array(z.string()).optional(),
11
+ url: z.string().optional(),
12
+ env: z.record(z.string()).optional(),
13
+ enabled: z.boolean().default(true)
14
+ });
15
+ var HookSchema = z.object({
16
+ event: z.enum([
17
+ "SessionStart",
18
+ "UserPromptSubmit",
19
+ "PreToolUse",
20
+ "PostToolUse",
21
+ "Stop",
22
+ "PreCompact",
23
+ "SessionEnd"
24
+ ]),
25
+ matcher: z.string().optional(),
26
+ type: z.enum(["command", "prompt", "agent"]),
27
+ command: z.string().optional(),
28
+ prompt: z.string().optional(),
29
+ agent: z.string().optional()
30
+ });
31
+ var PermissionsSchema = z.object({
32
+ allowedTools: z.array(z.string()).optional(),
33
+ disallowedTools: z.array(z.string()).optional(),
34
+ autoApprove: z.array(z.string()).optional(),
35
+ yolo: z.boolean().default(false)
36
+ });
37
+ var ConfigSchema = z.object({
38
+ // Provider settings
39
+ provider: z.enum(["anthropic", "openai", "groq", "local"]).default("anthropic"),
40
+ model: z.string().default("claude-sonnet-4-20250514"),
41
+ apiKey: z.string().optional(),
42
+ baseUrl: z.string().optional(),
43
+ // MCP servers
44
+ mcpServers: z.record(MCPServerSchema).optional(),
45
+ // Permissions
46
+ permissions: PermissionsSchema.optional(),
47
+ // Hooks
48
+ hooks: z.record(z.array(HookSchema)).optional(),
49
+ // Skills
50
+ skills: z.object({
51
+ enabled: z.array(z.string()).optional(),
52
+ disabled: z.array(z.string()).optional()
53
+ }).optional(),
54
+ // Session settings
55
+ session: z.object({
56
+ persistHistory: z.boolean().default(true),
57
+ maxHistoryDays: z.number().default(30)
58
+ }).optional(),
59
+ // UI settings
60
+ theme: z.enum(["dark", "light", "kaldi"]).default("kaldi"),
61
+ // Agent settings
62
+ agent: z.object({
63
+ maxTokens: z.number().default(8192),
64
+ maxTurns: z.number().default(100),
65
+ systemPrompt: z.string().optional()
66
+ }).optional()
67
+ });
68
+ var CONFIG_PATHS = {
69
+ // Project-level (highest priority)
70
+ project: [".puppuccino.json", "puppuccino.json", ".puppuccino/config.json"],
71
+ // User-level
72
+ user: [
73
+ join(homedir(), ".config", "puppuccino", "config.json"),
74
+ join(homedir(), ".puppuccino", "config.json")
75
+ ]
76
+ };
77
+ function getDataDir() {
78
+ const platform = process.platform;
79
+ if (process.env.PUPPUCCINO_DATA_DIR) {
80
+ return process.env.PUPPUCCINO_DATA_DIR;
81
+ }
82
+ switch (platform) {
83
+ case "darwin":
84
+ return join(homedir(), "Library", "Application Support", "puppuccino");
85
+ case "win32":
86
+ return join(process.env.LOCALAPPDATA || homedir(), "puppuccino");
87
+ default:
88
+ return join(homedir(), ".local", "share", "puppuccino");
89
+ }
90
+ }
91
+ function getSessionsDir() {
92
+ return join(getDataDir(), "sessions");
93
+ }
94
+ function loadConfigFile(path) {
95
+ if (!existsSync(path)) return null;
96
+ try {
97
+ const content = readFileSync(path, "utf-8");
98
+ return JSON.parse(content);
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ function loadConfig(projectDir) {
104
+ const configs = [];
105
+ for (const path of CONFIG_PATHS.user) {
106
+ const config = loadConfigFile(path);
107
+ if (config) {
108
+ configs.push(config);
109
+ break;
110
+ }
111
+ }
112
+ if (projectDir) {
113
+ for (const filename of CONFIG_PATHS.project) {
114
+ const path = join(projectDir, filename);
115
+ const config = loadConfigFile(path);
116
+ if (config) {
117
+ configs.push(config);
118
+ break;
119
+ }
120
+ }
121
+ }
122
+ const envConfig = {};
123
+ if (process.env.ANTHROPIC_API_KEY) {
124
+ envConfig.provider = "anthropic";
125
+ envConfig.apiKey = process.env.ANTHROPIC_API_KEY;
126
+ } else if (process.env.OPENAI_API_KEY) {
127
+ envConfig.provider = "openai";
128
+ envConfig.apiKey = process.env.OPENAI_API_KEY;
129
+ }
130
+ if (process.env.PUPPUCCINO_MODEL) {
131
+ envConfig.model = process.env.PUPPUCCINO_MODEL;
132
+ }
133
+ configs.push(envConfig);
134
+ const merged = configs.reduce((acc, cfg) => ({ ...acc, ...cfg }), {});
135
+ return ConfigSchema.parse(merged);
136
+ }
137
+ function saveConfig(config, path) {
138
+ const dir = dirname(path);
139
+ if (!existsSync(dir)) {
140
+ mkdirSync(dir, { recursive: true });
141
+ }
142
+ writeFileSync(path, JSON.stringify(config, null, 2));
143
+ }
144
+ function getUserConfigPath() {
145
+ return CONFIG_PATHS.user[0];
146
+ }
147
+
148
+ // src/providers/index.ts
149
+ import { createAnthropic } from "@ai-sdk/anthropic";
150
+ import { createOpenAI } from "@ai-sdk/openai";
151
+ import { generateText, streamText, tool } from "ai";
152
+ function createProvider(config) {
153
+ const { provider, apiKey, baseUrl } = config;
154
+ switch (provider) {
155
+ case "anthropic":
156
+ return createAnthropic({
157
+ apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
158
+ baseURL: baseUrl
159
+ });
160
+ case "openai":
161
+ return createOpenAI({
162
+ apiKey: apiKey || process.env.OPENAI_API_KEY,
163
+ baseURL: baseUrl
164
+ });
165
+ case "groq":
166
+ return createOpenAI({
167
+ apiKey: apiKey || process.env.GROQ_API_KEY,
168
+ baseURL: baseUrl || "https://api.groq.com/openai/v1"
169
+ });
170
+ case "local":
171
+ return createOpenAI({
172
+ apiKey: apiKey || "local",
173
+ baseURL: baseUrl || "http://localhost:11434/v1"
174
+ });
175
+ default:
176
+ throw new Error(`Unknown provider: ${provider}`);
177
+ }
178
+ }
179
+ function convertTools(tools) {
180
+ const result = {};
181
+ for (const t of tools) {
182
+ result[t.name] = tool({
183
+ description: t.description,
184
+ parameters: t.parameters
185
+ });
186
+ }
187
+ return result;
188
+ }
189
+ function convertMessages(messages) {
190
+ return messages.map((msg) => {
191
+ if (typeof msg.content === "string") {
192
+ return {
193
+ role: msg.role,
194
+ content: msg.content
195
+ };
196
+ }
197
+ const textParts = msg.content.filter((b) => b.type === "text").map((b) => b.text || "").join("");
198
+ return {
199
+ role: msg.role,
200
+ content: textParts
201
+ };
202
+ });
203
+ }
204
+ var Provider = class {
205
+ config;
206
+ sdk;
207
+ constructor(config) {
208
+ this.config = config;
209
+ this.sdk = createProvider(config);
210
+ }
211
+ /**
212
+ * Send a chat request and get a response
213
+ */
214
+ async chat(options) {
215
+ const {
216
+ model,
217
+ messages,
218
+ tools = [],
219
+ systemPrompt,
220
+ maxTokens = 8192,
221
+ temperature = 0.7
222
+ } = options;
223
+ const result = await generateText({
224
+ model: this.sdk(model),
225
+ messages: convertMessages(messages),
226
+ system: systemPrompt,
227
+ tools: tools.length > 0 ? convertTools(tools) : void 0,
228
+ maxTokens,
229
+ temperature
230
+ });
231
+ const toolCalls = result.toolCalls?.map((tc) => ({
232
+ id: tc.toolCallId,
233
+ name: tc.toolName,
234
+ input: tc.args
235
+ })) || [];
236
+ return {
237
+ text: result.text,
238
+ toolCalls,
239
+ finishReason: result.finishReason,
240
+ usage: result.usage ? {
241
+ promptTokens: result.usage.promptTokens,
242
+ completionTokens: result.usage.completionTokens
243
+ } : void 0
244
+ };
245
+ }
246
+ /**
247
+ * Stream a chat response
248
+ */
249
+ async *chatStream(options) {
250
+ const {
251
+ model,
252
+ messages,
253
+ tools = [],
254
+ systemPrompt,
255
+ maxTokens = 8192,
256
+ temperature = 0.7
257
+ } = options;
258
+ const result = await streamText({
259
+ model: this.sdk(model),
260
+ messages: convertMessages(messages),
261
+ system: systemPrompt,
262
+ tools: tools.length > 0 ? convertTools(tools) : void 0,
263
+ maxTokens,
264
+ temperature
265
+ });
266
+ for await (const chunk of result.textStream) {
267
+ yield { type: "text", text: chunk };
268
+ }
269
+ yield { type: "finish", finishReason: await result.finishReason };
270
+ }
271
+ /**
272
+ * Get current provider type
273
+ */
274
+ get providerType() {
275
+ return this.config.provider;
276
+ }
277
+ /**
278
+ * Get current model
279
+ */
280
+ get model() {
281
+ return this.config.model;
282
+ }
283
+ };
284
+ function createProviderInstance(config) {
285
+ return new Provider(config);
286
+ }
287
+
288
+ // src/tools/index.ts
289
+ import { z as z2 } from "zod";
290
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
291
+ import { dirname as dirname2, resolve as resolve2 } from "path";
292
+ import { execSync } from "child_process";
293
+
294
+ // src/tools/glob.ts
295
+ import { readdirSync } from "fs";
296
+ import { join as join2, relative, resolve } from "path";
297
+ function globToRegex(pattern) {
298
+ let regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\{\{GLOBSTAR\}\}/g, ".*");
299
+ return new RegExp(`^${regex}$`);
300
+ }
301
+ function shouldIgnore(name) {
302
+ const ignored = [
303
+ "node_modules",
304
+ ".git",
305
+ ".svn",
306
+ ".hg",
307
+ ".DS_Store",
308
+ "dist",
309
+ "build",
310
+ "coverage",
311
+ ".turbo",
312
+ ".next",
313
+ ".nuxt"
314
+ ];
315
+ return ignored.includes(name);
316
+ }
317
+ function* walkDir(dir, base) {
318
+ try {
319
+ const entries = readdirSync(dir, { withFileTypes: true });
320
+ for (const entry of entries) {
321
+ const name = entry.name;
322
+ if (entry.isDirectory() && shouldIgnore(name)) {
323
+ continue;
324
+ }
325
+ const fullPath = join2(dir, name);
326
+ const relativePath = relative(base, fullPath);
327
+ if (entry.isDirectory()) {
328
+ yield* walkDir(fullPath, base);
329
+ } else {
330
+ yield relativePath;
331
+ }
332
+ }
333
+ } catch {
334
+ }
335
+ }
336
+ async function glob(pattern, basePath = ".") {
337
+ const absBase = resolve(basePath);
338
+ const regex = globToRegex(pattern);
339
+ const results = [];
340
+ for (const file of walkDir(absBase, absBase)) {
341
+ const normalized = file.replace(/\\/g, "/");
342
+ if (regex.test(normalized)) {
343
+ results.push(normalized);
344
+ }
345
+ }
346
+ return results.sort();
347
+ }
348
+
349
+ // src/tools/index.ts
350
+ var ReadTool = {
351
+ name: "read",
352
+ description: "Read the contents of a file. Returns the file contents with line numbers.",
353
+ parameters: z2.object({
354
+ path: z2.string().describe("The file path to read"),
355
+ offset: z2.number().optional().describe("Line number to start reading from (1-based)"),
356
+ limit: z2.number().optional().describe("Maximum number of lines to read")
357
+ }),
358
+ async execute(input) {
359
+ const { path, offset = 1, limit } = input;
360
+ if (!existsSync2(path)) {
361
+ return `Error: File not found: ${path}`;
362
+ }
363
+ try {
364
+ const content = readFileSync2(path, "utf-8");
365
+ const lines = content.split("\n");
366
+ const startIdx = Math.max(0, offset - 1);
367
+ const endIdx = limit ? startIdx + limit : lines.length;
368
+ const selectedLines = lines.slice(startIdx, endIdx);
369
+ const formatted = selectedLines.map((line, i) => {
370
+ const lineNum = startIdx + i + 1;
371
+ const padding = String(lines.length).length;
372
+ return `${String(lineNum).padStart(padding)}\u2502${line}`;
373
+ }).join("\n");
374
+ return formatted;
375
+ } catch (err) {
376
+ return `Error reading file: ${err instanceof Error ? err.message : String(err)}`;
377
+ }
378
+ }
379
+ };
380
+ var WriteTool = {
381
+ name: "write",
382
+ description: "Write content to a file. Creates the file and parent directories if they don't exist.",
383
+ parameters: z2.object({
384
+ path: z2.string().describe("The file path to write to"),
385
+ content: z2.string().describe("The content to write")
386
+ }),
387
+ async execute(input) {
388
+ const { path, content } = input;
389
+ try {
390
+ const dir = dirname2(path);
391
+ if (!existsSync2(dir)) {
392
+ mkdirSync2(dir, { recursive: true });
393
+ }
394
+ writeFileSync2(path, content, "utf-8");
395
+ return `Successfully wrote ${content.length} bytes to ${path}`;
396
+ } catch (err) {
397
+ return `Error writing file: ${err instanceof Error ? err.message : String(err)}`;
398
+ }
399
+ }
400
+ };
401
+ var EditTool = {
402
+ name: "edit",
403
+ description: "Replace text in a file. The old_string must be unique in the file unless replace_all is true.",
404
+ parameters: z2.object({
405
+ path: z2.string().describe("The file path to edit"),
406
+ old_string: z2.string().describe("The text to find and replace"),
407
+ new_string: z2.string().describe("The replacement text"),
408
+ replace_all: z2.boolean().optional().describe("Replace all occurrences (default: false)")
409
+ }),
410
+ async execute(input) {
411
+ const { path, old_string, new_string, replace_all = false } = input;
412
+ if (!existsSync2(path)) {
413
+ return `Error: File not found: ${path}`;
414
+ }
415
+ try {
416
+ const content = readFileSync2(path, "utf-8");
417
+ const count = content.split(old_string).length - 1;
418
+ if (count === 0) {
419
+ return `Error: old_string not found in file`;
420
+ }
421
+ if (count > 1 && !replace_all) {
422
+ return `Error: old_string found ${count} times. Use replace_all=true to replace all, or provide a more specific string.`;
423
+ }
424
+ const newContent = replace_all ? content.split(old_string).join(new_string) : content.replace(old_string, new_string);
425
+ writeFileSync2(path, newContent, "utf-8");
426
+ const replacements = replace_all ? count : 1;
427
+ return `Successfully replaced ${replacements} occurrence(s) in ${path}`;
428
+ } catch (err) {
429
+ return `Error editing file: ${err instanceof Error ? err.message : String(err)}`;
430
+ }
431
+ }
432
+ };
433
+ var GlobTool = {
434
+ name: "glob",
435
+ description: "Find files matching a glob pattern. Returns a list of matching file paths.",
436
+ parameters: z2.object({
437
+ pattern: z2.string().describe('The glob pattern (e.g., "**/*.ts", "src/**/*.js")'),
438
+ path: z2.string().optional().describe("The directory to search in (default: current directory)")
439
+ }),
440
+ async execute(input) {
441
+ const { pattern, path: basePath = "." } = input;
442
+ try {
443
+ const files = await glob(pattern, basePath);
444
+ if (files.length === 0) {
445
+ return "No files matched the pattern";
446
+ }
447
+ const maxResults = 100;
448
+ const limited = files.slice(0, maxResults);
449
+ const result = limited.join("\n");
450
+ if (files.length > maxResults) {
451
+ return `${result}
452
+
453
+ ... and ${files.length - maxResults} more files`;
454
+ }
455
+ return result;
456
+ } catch (err) {
457
+ return `Error searching files: ${err instanceof Error ? err.message : String(err)}`;
458
+ }
459
+ }
460
+ };
461
+ var GrepTool = {
462
+ name: "grep",
463
+ description: "Search for a pattern in files. Returns matching lines with file paths and line numbers.",
464
+ parameters: z2.object({
465
+ pattern: z2.string().describe("The regex pattern to search for"),
466
+ path: z2.string().optional().describe("The directory to search in (default: current directory)"),
467
+ glob: z2.string().optional().describe('File pattern to filter (e.g., "*.ts")')
468
+ }),
469
+ async execute(input) {
470
+ const { pattern, path: basePath = ".", glob: fileGlob } = input;
471
+ try {
472
+ const regex = new RegExp(pattern, "gi");
473
+ const results = [];
474
+ const maxResults = 50;
475
+ const searchGlob = fileGlob || "**/*";
476
+ const files = await glob(searchGlob, basePath);
477
+ outer: for (const file of files) {
478
+ const fullPath = resolve2(basePath, file);
479
+ try {
480
+ const stat = statSync2(fullPath);
481
+ if (stat.isDirectory()) continue;
482
+ if (stat.size > 1024 * 1024) continue;
483
+ } catch {
484
+ continue;
485
+ }
486
+ try {
487
+ const content = readFileSync2(fullPath, "utf-8");
488
+ const lines = content.split("\n");
489
+ for (let i = 0; i < lines.length; i++) {
490
+ if (regex.test(lines[i])) {
491
+ results.push(`${file}:${i + 1}: ${lines[i].trim()}`);
492
+ regex.lastIndex = 0;
493
+ if (results.length >= maxResults) break outer;
494
+ }
495
+ }
496
+ } catch {
497
+ continue;
498
+ }
499
+ }
500
+ if (results.length === 0) {
501
+ return "No matches found";
502
+ }
503
+ return results.join("\n");
504
+ } catch (err) {
505
+ return `Error searching: ${err instanceof Error ? err.message : String(err)}`;
506
+ }
507
+ }
508
+ };
509
+ var BashTool = {
510
+ name: "bash",
511
+ description: "Execute a shell command and return the output.",
512
+ parameters: z2.object({
513
+ command: z2.string().describe("The command to execute"),
514
+ timeout: z2.number().optional().describe("Timeout in milliseconds (default: 30000)")
515
+ }),
516
+ async execute(input) {
517
+ const { command, timeout = 3e4 } = input;
518
+ return new Promise((resolve3) => {
519
+ try {
520
+ const result = execSync(command, {
521
+ encoding: "utf-8",
522
+ timeout,
523
+ maxBuffer: 10 * 1024 * 1024,
524
+ // 10MB
525
+ stdio: ["pipe", "pipe", "pipe"]
526
+ });
527
+ resolve3(result || "(no output)");
528
+ } catch (err) {
529
+ const error = err;
530
+ if (error.stdout || error.stderr) {
531
+ resolve3(`${error.stdout || ""}${error.stderr || ""}`);
532
+ } else {
533
+ resolve3(`Error: ${error.message || String(err)}`);
534
+ }
535
+ }
536
+ });
537
+ }
538
+ };
539
+ var ListTool = {
540
+ name: "ls",
541
+ description: "List the contents of a directory.",
542
+ parameters: z2.object({
543
+ path: z2.string().optional().describe("The directory path (default: current directory)"),
544
+ all: z2.boolean().optional().describe("Include hidden files (default: false)")
545
+ }),
546
+ async execute(input) {
547
+ const { path: dirPath = ".", all = false } = input;
548
+ try {
549
+ if (!existsSync2(dirPath)) {
550
+ return `Error: Directory not found: ${dirPath}`;
551
+ }
552
+ const entries = readdirSync2(dirPath, { withFileTypes: true });
553
+ const filtered = all ? entries : entries.filter((e) => !e.name.startsWith("."));
554
+ const formatted = filtered.map((entry) => {
555
+ const suffix = entry.isDirectory() ? "/" : "";
556
+ return `${entry.name}${suffix}`;
557
+ });
558
+ return formatted.sort().join("\n");
559
+ } catch (err) {
560
+ return `Error listing directory: ${err instanceof Error ? err.message : String(err)}`;
561
+ }
562
+ }
563
+ };
564
+ var BUILTIN_TOOLS = [
565
+ ReadTool,
566
+ WriteTool,
567
+ EditTool,
568
+ GlobTool,
569
+ GrepTool,
570
+ BashTool,
571
+ ListTool
572
+ ];
573
+ var ToolRegistry = class {
574
+ tools = /* @__PURE__ */ new Map();
575
+ constructor() {
576
+ for (const tool2 of BUILTIN_TOOLS) {
577
+ this.register(tool2);
578
+ }
579
+ }
580
+ /**
581
+ * Register a tool
582
+ */
583
+ register(tool2) {
584
+ this.tools.set(tool2.name, tool2);
585
+ }
586
+ /**
587
+ * Unregister a tool
588
+ */
589
+ unregister(name) {
590
+ this.tools.delete(name);
591
+ }
592
+ /**
593
+ * Get a tool by name
594
+ */
595
+ get(name) {
596
+ return this.tools.get(name);
597
+ }
598
+ /**
599
+ * Get all registered tools
600
+ */
601
+ all() {
602
+ return Array.from(this.tools.values());
603
+ }
604
+ /**
605
+ * Check if a tool exists
606
+ */
607
+ has(name) {
608
+ return this.tools.has(name);
609
+ }
610
+ /**
611
+ * Execute a tool
612
+ */
613
+ async execute(name, input) {
614
+ const tool2 = this.get(name);
615
+ if (!tool2) {
616
+ return {
617
+ success: false,
618
+ output: "",
619
+ error: `Unknown tool: ${name}`
620
+ };
621
+ }
622
+ try {
623
+ const validated = tool2.parameters.parse(input);
624
+ const output = await tool2.execute(validated);
625
+ return {
626
+ success: true,
627
+ output
628
+ };
629
+ } catch (err) {
630
+ return {
631
+ success: false,
632
+ output: "",
633
+ error: err instanceof Error ? err.message : String(err)
634
+ };
635
+ }
636
+ }
637
+ };
638
+ function createToolRegistry() {
639
+ return new ToolRegistry();
640
+ }
641
+
642
+ // src/agent/index.ts
643
+ var DEFAULT_SYSTEM_PROMPT = `You are Kaldi, a helpful AI coding assistant. You're a Great Pyrenees who loves helping developers write great code.
644
+
645
+ You have access to tools to read, write, and edit files, search the codebase, and execute commands. Use these tools to help the user with their coding tasks.
646
+
647
+ When making changes:
648
+ - Read files before editing them to understand the context
649
+ - Make minimal, focused changes
650
+ - Explain what you're doing and why
651
+ - Be careful with destructive operations
652
+
653
+ Be friendly, helpful, and thorough in your responses.`;
654
+ async function* runAgentLoop(state) {
655
+ const {
656
+ messages,
657
+ provider,
658
+ tools,
659
+ hooks,
660
+ permissions,
661
+ systemPrompt = DEFAULT_SYSTEM_PROMPT,
662
+ maxTurns = 100,
663
+ onPermissionRequest
664
+ } = state;
665
+ let turns = 0;
666
+ while (turns < maxTurns) {
667
+ turns++;
668
+ yield { type: "thinking" };
669
+ if (hooks) {
670
+ const hookResult = await hooks.run("PreToolUse", {
671
+ messages,
672
+ turn: turns
673
+ });
674
+ if (hookResult.blocked) {
675
+ yield { type: "error", error: hookResult.reason || "Blocked by hook" };
676
+ return;
677
+ }
678
+ }
679
+ let response;
680
+ try {
681
+ response = await provider.chat({
682
+ model: provider.model,
683
+ messages,
684
+ tools: tools.all(),
685
+ systemPrompt
686
+ });
687
+ } catch (err) {
688
+ yield { type: "error", error: err instanceof Error ? err.message : String(err) };
689
+ return;
690
+ }
691
+ if (response.text) {
692
+ yield { type: "text", content: response.text };
693
+ }
694
+ if (response.toolCalls.length === 0) {
695
+ yield { type: "done", response };
696
+ return;
697
+ }
698
+ const toolResults = [];
699
+ for (const toolCall of response.toolCalls) {
700
+ yield {
701
+ type: "tool_start",
702
+ name: toolCall.name,
703
+ input: toolCall.input
704
+ };
705
+ if (permissions) {
706
+ const allowed = await permissions.check(toolCall.name, toolCall.input);
707
+ if (!allowed.permitted) {
708
+ if (onPermissionRequest) {
709
+ const granted = await onPermissionRequest(toolCall.name, toolCall.input);
710
+ if (!granted) {
711
+ yield {
712
+ type: "tool_denied",
713
+ name: toolCall.name,
714
+ reason: "Permission denied by user"
715
+ };
716
+ toolResults.push({
717
+ tool_use_id: toolCall.id,
718
+ content: "Permission denied by user"
719
+ });
720
+ continue;
721
+ }
722
+ } else {
723
+ yield {
724
+ type: "tool_denied",
725
+ name: toolCall.name,
726
+ reason: allowed.reason || "Permission denied"
727
+ };
728
+ toolResults.push({
729
+ tool_use_id: toolCall.id,
730
+ content: allowed.reason || "Permission denied"
731
+ });
732
+ continue;
733
+ }
734
+ }
735
+ }
736
+ const result = await tools.execute(toolCall.name, toolCall.input);
737
+ yield {
738
+ type: "tool_result",
739
+ name: toolCall.name,
740
+ result
741
+ };
742
+ if (hooks) {
743
+ await hooks.run("PostToolUse", {
744
+ tool: toolCall.name,
745
+ input: toolCall.input,
746
+ result
747
+ });
748
+ }
749
+ toolResults.push({
750
+ tool_use_id: toolCall.id,
751
+ content: result.success ? result.output : `Error: ${result.error}`
752
+ });
753
+ }
754
+ messages.push({
755
+ role: "assistant",
756
+ content: [
757
+ ...response.text ? [{ type: "text", text: response.text }] : [],
758
+ ...response.toolCalls.map((tc) => ({
759
+ type: "tool_use",
760
+ id: tc.id,
761
+ name: tc.name,
762
+ input: tc.input
763
+ }))
764
+ ]
765
+ });
766
+ messages.push({
767
+ role: "user",
768
+ content: toolResults.map((tr) => ({
769
+ type: "tool_result",
770
+ tool_use_id: tr.tool_use_id,
771
+ content: tr.content
772
+ }))
773
+ });
774
+ }
775
+ yield { type: "error", error: `Max turns (${maxTurns}) reached` };
776
+ }
777
+ async function runAgent(state) {
778
+ const events = [];
779
+ for await (const event of runAgentLoop(state)) {
780
+ events.push(event);
781
+ }
782
+ return {
783
+ events,
784
+ messages: state.messages
785
+ };
786
+ }
787
+ function createAgentState(provider, tools, options) {
788
+ return {
789
+ messages: [],
790
+ provider,
791
+ tools,
792
+ ...options
793
+ };
794
+ }
795
+
796
+ // src/hooks/index.ts
797
+ import { execSync as execSync2 } from "child_process";
798
+ async function runCommandHook(command, context) {
799
+ try {
800
+ const input = JSON.stringify(context);
801
+ const output = execSync2(command, {
802
+ encoding: "utf-8",
803
+ input,
804
+ timeout: 3e4,
805
+ stdio: ["pipe", "pipe", "pipe"]
806
+ });
807
+ try {
808
+ const result = JSON.parse(output);
809
+ return {
810
+ blocked: result.blocked === true,
811
+ reason: result.reason,
812
+ output: result.output || output
813
+ };
814
+ } catch {
815
+ return {
816
+ blocked: false,
817
+ output
818
+ };
819
+ }
820
+ } catch (err) {
821
+ const error = err;
822
+ if (error.status === 2) {
823
+ return {
824
+ blocked: true,
825
+ reason: error.message || "Hook blocked execution"
826
+ };
827
+ }
828
+ return {
829
+ blocked: false,
830
+ output: error.message || String(err)
831
+ };
832
+ }
833
+ }
834
+ async function runPromptHook(prompt, context) {
835
+ return {
836
+ blocked: false,
837
+ output: `Prompt hook: ${prompt}`
838
+ };
839
+ }
840
+ var HooksRunner = class {
841
+ hooks = /* @__PURE__ */ new Map();
842
+ constructor(hooks) {
843
+ if (hooks) {
844
+ for (const [event, eventHooks] of Object.entries(hooks)) {
845
+ this.hooks.set(event, eventHooks);
846
+ }
847
+ }
848
+ }
849
+ /**
850
+ * Add a hook
851
+ */
852
+ add(event, hook) {
853
+ const existing = this.hooks.get(event) || [];
854
+ existing.push(hook);
855
+ this.hooks.set(event, existing);
856
+ }
857
+ /**
858
+ * Remove all hooks for an event
859
+ */
860
+ clear(event) {
861
+ this.hooks.delete(event);
862
+ }
863
+ /**
864
+ * Run all hooks for an event
865
+ */
866
+ async run(event, context = {}) {
867
+ const hooks = this.hooks.get(event) || [];
868
+ const fullContext = { event, ...context };
869
+ for (const hook of hooks) {
870
+ if (hook.matcher && context.tool) {
871
+ const pattern = new RegExp(hook.matcher);
872
+ if (!pattern.test(context.tool)) {
873
+ continue;
874
+ }
875
+ }
876
+ let result;
877
+ switch (hook.type) {
878
+ case "command":
879
+ if (hook.command) {
880
+ result = await runCommandHook(hook.command, fullContext);
881
+ } else {
882
+ result = { blocked: false };
883
+ }
884
+ break;
885
+ case "prompt":
886
+ if (hook.prompt) {
887
+ result = await runPromptHook(hook.prompt, fullContext);
888
+ } else {
889
+ result = { blocked: false };
890
+ }
891
+ break;
892
+ case "agent":
893
+ result = { blocked: false };
894
+ break;
895
+ default:
896
+ result = { blocked: false };
897
+ }
898
+ if (result.blocked) {
899
+ return result;
900
+ }
901
+ }
902
+ return { blocked: false };
903
+ }
904
+ /**
905
+ * Get all hooks for an event
906
+ */
907
+ get(event) {
908
+ return this.hooks.get(event) || [];
909
+ }
910
+ };
911
+ function createHooksRunner(hooks) {
912
+ return new HooksRunner(hooks);
913
+ }
914
+
915
+ // src/permissions/index.ts
916
+ function matchPattern(pattern, toolName, input) {
917
+ if (!pattern.includes("(")) {
918
+ return pattern === toolName || pattern === "*";
919
+ }
920
+ const match = pattern.match(/^(\w+)\((.*)\)$/);
921
+ if (!match) return false;
922
+ const [, name, argPattern] = match;
923
+ if (name !== toolName && name !== "*") {
924
+ return false;
925
+ }
926
+ const argRegex = new RegExp(argPattern);
927
+ for (const value of Object.values(input)) {
928
+ if (typeof value === "string" && argRegex.test(value)) {
929
+ return true;
930
+ }
931
+ }
932
+ return false;
933
+ }
934
+ var PermissionChecker = class {
935
+ config;
936
+ constructor(config) {
937
+ this.config = config || { yolo: false };
938
+ }
939
+ /**
940
+ * Check if a tool execution is permitted
941
+ */
942
+ async check(toolName, input) {
943
+ if (this.config.yolo) {
944
+ return { permitted: true };
945
+ }
946
+ if (this.config.disallowedTools) {
947
+ for (const pattern of this.config.disallowedTools) {
948
+ if (matchPattern(pattern, toolName, input)) {
949
+ return {
950
+ permitted: false,
951
+ reason: `Tool ${toolName} is disallowed by pattern: ${pattern}`
952
+ };
953
+ }
954
+ }
955
+ }
956
+ if (this.config.autoApprove) {
957
+ for (const pattern of this.config.autoApprove) {
958
+ if (matchPattern(pattern, toolName, input)) {
959
+ return { permitted: true };
960
+ }
961
+ }
962
+ }
963
+ if (this.config.allowedTools && this.config.allowedTools.length > 0) {
964
+ for (const pattern of this.config.allowedTools) {
965
+ if (matchPattern(pattern, toolName, input)) {
966
+ return { permitted: true };
967
+ }
968
+ }
969
+ return {
970
+ permitted: false,
971
+ reason: `Tool ${toolName} is not in allowed list`,
972
+ requiresConfirmation: true
973
+ };
974
+ }
975
+ const dangerousTools = ["bash", "write", "edit"];
976
+ if (dangerousTools.includes(toolName)) {
977
+ return {
978
+ permitted: false,
979
+ reason: `Tool ${toolName} requires confirmation`,
980
+ requiresConfirmation: true
981
+ };
982
+ }
983
+ return { permitted: true };
984
+ }
985
+ /**
986
+ * Update configuration
987
+ */
988
+ updateConfig(config) {
989
+ this.config = { ...this.config, ...config };
990
+ }
991
+ /**
992
+ * Enable YOLO mode
993
+ */
994
+ enableYolo() {
995
+ this.config.yolo = true;
996
+ }
997
+ /**
998
+ * Disable YOLO mode
999
+ */
1000
+ disableYolo() {
1001
+ this.config.yolo = false;
1002
+ }
1003
+ /**
1004
+ * Add an auto-approve pattern
1005
+ */
1006
+ addAutoApprove(pattern) {
1007
+ if (!this.config.autoApprove) {
1008
+ this.config.autoApprove = [];
1009
+ }
1010
+ this.config.autoApprove.push(pattern);
1011
+ }
1012
+ };
1013
+ function createPermissionChecker(config) {
1014
+ return new PermissionChecker(config);
1015
+ }
1016
+
1017
+ // src/session/index.ts
1018
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync3, readdirSync as readdirSync3, unlinkSync } from "fs";
1019
+ import { join as join4 } from "path";
1020
+ import { randomUUID } from "crypto";
1021
+ function getSessionPath(id) {
1022
+ const dir = getSessionsDir();
1023
+ return join4(dir, `${id}.jsonl`);
1024
+ }
1025
+ function ensureSessionsDir() {
1026
+ const dir = getSessionsDir();
1027
+ if (!existsSync3(dir)) {
1028
+ mkdirSync3(dir, { recursive: true });
1029
+ }
1030
+ }
1031
+ function createSession(options) {
1032
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1033
+ return {
1034
+ id: randomUUID(),
1035
+ name: options?.name,
1036
+ createdAt: now,
1037
+ updatedAt: now,
1038
+ projectPath: options?.projectPath,
1039
+ model: options?.model,
1040
+ messageCount: 0,
1041
+ messages: []
1042
+ };
1043
+ }
1044
+ function saveSession(session) {
1045
+ ensureSessionsDir();
1046
+ const path = getSessionPath(session.id);
1047
+ const lines = [];
1048
+ const meta = {
1049
+ type: "meta",
1050
+ timestamp: session.updatedAt,
1051
+ data: {
1052
+ id: session.id,
1053
+ name: session.name,
1054
+ createdAt: session.createdAt,
1055
+ updatedAt: session.updatedAt,
1056
+ projectPath: session.projectPath,
1057
+ model: session.model,
1058
+ messageCount: session.messages.length
1059
+ }
1060
+ };
1061
+ lines.push(JSON.stringify(meta));
1062
+ for (const message of session.messages) {
1063
+ const entry = {
1064
+ type: "message",
1065
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1066
+ data: message
1067
+ };
1068
+ lines.push(JSON.stringify(entry));
1069
+ }
1070
+ writeFileSync3(path, lines.join("\n") + "\n");
1071
+ }
1072
+ function appendMessage(sessionId, message) {
1073
+ ensureSessionsDir();
1074
+ const path = getSessionPath(sessionId);
1075
+ const entry = {
1076
+ type: "message",
1077
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1078
+ data: message
1079
+ };
1080
+ const line = JSON.stringify(entry) + "\n";
1081
+ if (existsSync3(path)) {
1082
+ const content = readFileSync3(path, "utf-8");
1083
+ writeFileSync3(path, content + line);
1084
+ } else {
1085
+ writeFileSync3(path, line);
1086
+ }
1087
+ }
1088
+ function loadSession(id) {
1089
+ const path = getSessionPath(id);
1090
+ if (!existsSync3(path)) {
1091
+ return null;
1092
+ }
1093
+ const content = readFileSync3(path, "utf-8");
1094
+ const lines = content.trim().split("\n").filter(Boolean);
1095
+ let meta = null;
1096
+ const messages = [];
1097
+ for (const line of lines) {
1098
+ try {
1099
+ const entry = JSON.parse(line);
1100
+ if (entry.type === "meta") {
1101
+ meta = entry.data;
1102
+ } else if (entry.type === "message") {
1103
+ messages.push(entry.data);
1104
+ }
1105
+ } catch {
1106
+ }
1107
+ }
1108
+ if (!meta) {
1109
+ meta = {
1110
+ id,
1111
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1112
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1113
+ messageCount: messages.length
1114
+ };
1115
+ }
1116
+ return {
1117
+ ...meta,
1118
+ messages
1119
+ };
1120
+ }
1121
+ function listSessions() {
1122
+ ensureSessionsDir();
1123
+ const dir = getSessionsDir();
1124
+ const files = readdirSync3(dir).filter((f) => f.endsWith(".jsonl"));
1125
+ const sessions = [];
1126
+ for (const file of files) {
1127
+ const id = file.replace(".jsonl", "");
1128
+ const session = loadSession(id);
1129
+ if (session) {
1130
+ sessions.push({
1131
+ id: session.id,
1132
+ name: session.name,
1133
+ createdAt: session.createdAt,
1134
+ updatedAt: session.updatedAt,
1135
+ projectPath: session.projectPath,
1136
+ model: session.model,
1137
+ messageCount: session.messageCount
1138
+ });
1139
+ }
1140
+ }
1141
+ return sessions.sort(
1142
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
1143
+ );
1144
+ }
1145
+ function deleteSession(id) {
1146
+ const path = getSessionPath(id);
1147
+ if (existsSync3(path)) {
1148
+ unlinkSync(path);
1149
+ return true;
1150
+ }
1151
+ return false;
1152
+ }
1153
+ function getRecentSession() {
1154
+ const sessions = listSessions();
1155
+ if (sessions.length === 0) {
1156
+ return null;
1157
+ }
1158
+ return loadSession(sessions[0].id);
1159
+ }
1160
+ function forkSession(id) {
1161
+ const original = loadSession(id);
1162
+ if (!original) {
1163
+ return null;
1164
+ }
1165
+ const forked = createSession({
1166
+ name: original.name ? `${original.name} (fork)` : void 0,
1167
+ projectPath: original.projectPath,
1168
+ model: original.model
1169
+ });
1170
+ forked.messages = [...original.messages];
1171
+ forked.messageCount = original.messageCount;
1172
+ saveSession(forked);
1173
+ return forked;
1174
+ }
1175
+ var SessionManager = class {
1176
+ currentSession = null;
1177
+ /**
1178
+ * Start a new session
1179
+ */
1180
+ start(options) {
1181
+ this.currentSession = createSession(options);
1182
+ return this.currentSession;
1183
+ }
1184
+ /**
1185
+ * Resume an existing session
1186
+ */
1187
+ resume(id) {
1188
+ const session = loadSession(id);
1189
+ if (session) {
1190
+ this.currentSession = session;
1191
+ }
1192
+ return session;
1193
+ }
1194
+ /**
1195
+ * Resume the most recent session
1196
+ */
1197
+ resumeRecent() {
1198
+ const session = getRecentSession();
1199
+ if (session) {
1200
+ this.currentSession = session;
1201
+ }
1202
+ return session;
1203
+ }
1204
+ /**
1205
+ * Get current session
1206
+ */
1207
+ get current() {
1208
+ return this.currentSession;
1209
+ }
1210
+ /**
1211
+ * Add a message to current session
1212
+ */
1213
+ addMessage(message) {
1214
+ if (!this.currentSession) {
1215
+ throw new Error("No active session");
1216
+ }
1217
+ this.currentSession.messages.push(message);
1218
+ this.currentSession.messageCount = this.currentSession.messages.length;
1219
+ this.currentSession.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1220
+ }
1221
+ /**
1222
+ * Save current session
1223
+ */
1224
+ save() {
1225
+ if (this.currentSession) {
1226
+ saveSession(this.currentSession);
1227
+ }
1228
+ }
1229
+ /**
1230
+ * Clear current session messages
1231
+ */
1232
+ clear() {
1233
+ if (this.currentSession) {
1234
+ this.currentSession.messages = [];
1235
+ this.currentSession.messageCount = 0;
1236
+ this.currentSession.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1237
+ }
1238
+ }
1239
+ /**
1240
+ * End current session
1241
+ */
1242
+ end() {
1243
+ if (this.currentSession) {
1244
+ this.save();
1245
+ this.currentSession = null;
1246
+ }
1247
+ }
1248
+ };
1249
+ function createSessionManager() {
1250
+ return new SessionManager();
1251
+ }
1252
+
1253
+ // src/mcp/index.ts
1254
+ import { spawn as spawn2 } from "child_process";
1255
+ import { EventEmitter } from "events";
1256
+ import { z as z3 } from "zod";
1257
+ var MCPConnection = class extends EventEmitter {
1258
+ server;
1259
+ process = null;
1260
+ messageId = 0;
1261
+ pendingRequests = /* @__PURE__ */ new Map();
1262
+ buffer = "";
1263
+ tools = [];
1264
+ initialized = false;
1265
+ constructor(server) {
1266
+ super();
1267
+ this.server = server;
1268
+ }
1269
+ /**
1270
+ * Connect to the MCP server
1271
+ */
1272
+ async connect() {
1273
+ if (this.server.transport !== "stdio") {
1274
+ throw new Error(`Transport ${this.server.transport} not yet supported`);
1275
+ }
1276
+ if (!this.server.command) {
1277
+ throw new Error("Command is required for stdio transport");
1278
+ }
1279
+ this.process = spawn2(this.server.command, this.server.args || [], {
1280
+ stdio: ["pipe", "pipe", "pipe"],
1281
+ env: {
1282
+ ...process.env,
1283
+ ...this.server.env
1284
+ }
1285
+ });
1286
+ this.process.stdout?.on("data", (data) => {
1287
+ this.handleData(data.toString());
1288
+ });
1289
+ this.process.stderr?.on("data", (data) => {
1290
+ this.emit("log", data.toString());
1291
+ });
1292
+ this.process.on("exit", (code) => {
1293
+ this.emit("exit", code);
1294
+ this.initialized = false;
1295
+ });
1296
+ await this.initialize();
1297
+ }
1298
+ /**
1299
+ * Handle incoming data
1300
+ */
1301
+ handleData(data) {
1302
+ this.buffer += data;
1303
+ const lines = this.buffer.split("\n");
1304
+ this.buffer = lines.pop() || "";
1305
+ for (const line of lines) {
1306
+ if (!line.trim()) continue;
1307
+ try {
1308
+ const message = JSON.parse(line);
1309
+ if ("id" in message && this.pendingRequests.has(message.id)) {
1310
+ const pending = this.pendingRequests.get(message.id);
1311
+ this.pendingRequests.delete(message.id);
1312
+ if (message.error) {
1313
+ pending.reject(new Error(message.error.message));
1314
+ } else {
1315
+ pending.resolve(message.result);
1316
+ }
1317
+ } else if ("method" in message && !("id" in message)) {
1318
+ this.emit("notification", message);
1319
+ }
1320
+ } catch (err) {
1321
+ this.emit("error", err);
1322
+ }
1323
+ }
1324
+ }
1325
+ /**
1326
+ * Send a request and wait for response
1327
+ */
1328
+ async request(method, params) {
1329
+ if (!this.process?.stdin) {
1330
+ throw new Error("Not connected");
1331
+ }
1332
+ const id = ++this.messageId;
1333
+ const request = {
1334
+ jsonrpc: "2.0",
1335
+ id,
1336
+ method,
1337
+ params
1338
+ };
1339
+ return new Promise((resolve3, reject) => {
1340
+ this.pendingRequests.set(id, { resolve: resolve3, reject });
1341
+ this.process.stdin.write(JSON.stringify(request) + "\n");
1342
+ setTimeout(() => {
1343
+ if (this.pendingRequests.has(id)) {
1344
+ this.pendingRequests.delete(id);
1345
+ reject(new Error("Request timeout"));
1346
+ }
1347
+ }, 3e4);
1348
+ });
1349
+ }
1350
+ /**
1351
+ * Initialize the MCP connection
1352
+ */
1353
+ async initialize() {
1354
+ await this.request("initialize", {
1355
+ protocolVersion: "2024-11-05",
1356
+ capabilities: {
1357
+ tools: {}
1358
+ },
1359
+ clientInfo: {
1360
+ name: "puppuccino",
1361
+ version: "0.1.0"
1362
+ }
1363
+ });
1364
+ this.process?.stdin?.write(JSON.stringify({
1365
+ jsonrpc: "2.0",
1366
+ method: "notifications/initialized"
1367
+ }) + "\n");
1368
+ const result = await this.request("tools/list");
1369
+ this.tools = result.tools || [];
1370
+ this.initialized = true;
1371
+ this.emit("ready");
1372
+ }
1373
+ /**
1374
+ * Get tools from this server
1375
+ */
1376
+ getTools() {
1377
+ return this.tools.map((t) => this.convertTool(t));
1378
+ }
1379
+ /**
1380
+ * Convert MCP tool to our Tool format
1381
+ */
1382
+ convertTool(mcpTool) {
1383
+ const properties = {};
1384
+ for (const [key, schema] of Object.entries(mcpTool.inputSchema.properties)) {
1385
+ const s = schema;
1386
+ let zodType;
1387
+ switch (s.type) {
1388
+ case "string":
1389
+ zodType = z3.string();
1390
+ break;
1391
+ case "number":
1392
+ zodType = z3.number();
1393
+ break;
1394
+ case "boolean":
1395
+ zodType = z3.boolean();
1396
+ break;
1397
+ case "array":
1398
+ zodType = z3.array(z3.unknown());
1399
+ break;
1400
+ case "object":
1401
+ zodType = z3.object({});
1402
+ break;
1403
+ default:
1404
+ zodType = z3.unknown();
1405
+ }
1406
+ if (s.description) {
1407
+ zodType = zodType.describe(s.description);
1408
+ }
1409
+ if (!mcpTool.inputSchema.required?.includes(key)) {
1410
+ zodType = zodType.optional();
1411
+ }
1412
+ properties[key] = zodType;
1413
+ }
1414
+ const parameters = z3.object(properties);
1415
+ return {
1416
+ name: `mcp__${this.server.name}__${mcpTool.name}`,
1417
+ description: mcpTool.description,
1418
+ parameters,
1419
+ execute: async (input) => {
1420
+ const result = await this.callTool(mcpTool.name, input);
1421
+ return typeof result === "string" ? result : JSON.stringify(result);
1422
+ }
1423
+ };
1424
+ }
1425
+ /**
1426
+ * Call a tool on this server
1427
+ */
1428
+ async callTool(name, input) {
1429
+ const result = await this.request(
1430
+ "tools/call",
1431
+ { name, arguments: input }
1432
+ );
1433
+ const textContent = result.content?.filter((c) => c.type === "text").map((c) => c.text).join("\n");
1434
+ return textContent || result;
1435
+ }
1436
+ /**
1437
+ * Disconnect from the server
1438
+ */
1439
+ disconnect() {
1440
+ if (this.process) {
1441
+ this.process.kill();
1442
+ this.process = null;
1443
+ }
1444
+ this.initialized = false;
1445
+ this.tools = [];
1446
+ }
1447
+ /**
1448
+ * Check if connected
1449
+ */
1450
+ get isConnected() {
1451
+ return this.initialized;
1452
+ }
1453
+ };
1454
+ var MCPClient = class {
1455
+ connections = /* @__PURE__ */ new Map();
1456
+ /**
1457
+ * Add and connect to an MCP server
1458
+ */
1459
+ async addServer(server) {
1460
+ if (!server.enabled) return;
1461
+ const connection = new MCPConnection(server);
1462
+ await connection.connect();
1463
+ this.connections.set(server.name, connection);
1464
+ }
1465
+ /**
1466
+ * Remove an MCP server
1467
+ */
1468
+ removeServer(name) {
1469
+ const connection = this.connections.get(name);
1470
+ if (connection) {
1471
+ connection.disconnect();
1472
+ this.connections.delete(name);
1473
+ }
1474
+ }
1475
+ /**
1476
+ * Get all tools from all servers
1477
+ */
1478
+ getTools() {
1479
+ const tools = [];
1480
+ for (const connection of this.connections.values()) {
1481
+ if (connection.isConnected) {
1482
+ tools.push(...connection.getTools());
1483
+ }
1484
+ }
1485
+ return tools;
1486
+ }
1487
+ /**
1488
+ * Call a tool (parses the mcp__server__tool format)
1489
+ */
1490
+ async callTool(name, input) {
1491
+ const parts = name.split("__");
1492
+ if (parts.length !== 3 || parts[0] !== "mcp") {
1493
+ throw new Error(`Invalid MCP tool name: ${name}`);
1494
+ }
1495
+ const [, serverName, toolName] = parts;
1496
+ const connection = this.connections.get(serverName);
1497
+ if (!connection) {
1498
+ throw new Error(`MCP server not found: ${serverName}`);
1499
+ }
1500
+ return connection.callTool(toolName, input);
1501
+ }
1502
+ /**
1503
+ * Disconnect all servers
1504
+ */
1505
+ disconnectAll() {
1506
+ for (const connection of this.connections.values()) {
1507
+ connection.disconnect();
1508
+ }
1509
+ this.connections.clear();
1510
+ }
1511
+ /**
1512
+ * Get connected server names
1513
+ */
1514
+ get servers() {
1515
+ return Array.from(this.connections.keys());
1516
+ }
1517
+ };
1518
+ function createMCPClient() {
1519
+ return new MCPClient();
1520
+ }
1521
+
1522
+ // src/skills/index.ts
1523
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync4 } from "fs";
1524
+ import { join as join5, dirname as dirname3 } from "path";
1525
+ import { homedir as homedir2 } from "os";
1526
+ var SKILL_LOCATIONS = [
1527
+ // Personal skills
1528
+ join5(homedir2(), ".puppuccino", "skills"),
1529
+ join5(homedir2(), ".config", "puppuccino", "skills"),
1530
+ // Project skills (will be prefixed with project path)
1531
+ ".puppuccino/skills"
1532
+ ];
1533
+ function parseFrontmatter(content) {
1534
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
1535
+ const match = content.match(frontmatterRegex);
1536
+ if (!match) {
1537
+ return { meta: {}, content };
1538
+ }
1539
+ const [, yaml, rest] = match;
1540
+ const meta = {};
1541
+ for (const line of yaml.split("\n")) {
1542
+ const colonIdx = line.indexOf(":");
1543
+ if (colonIdx === -1) continue;
1544
+ const key = line.slice(0, colonIdx).trim();
1545
+ let value = line.slice(colonIdx + 1).trim();
1546
+ if (value === "true") value = true;
1547
+ else if (value === "false") value = false;
1548
+ if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
1549
+ value = value.slice(1, -1).split(",").map((s) => s.trim());
1550
+ }
1551
+ const camelKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1552
+ meta[camelKey] = value;
1553
+ }
1554
+ return { meta, content: rest.trim() };
1555
+ }
1556
+ function loadSkillFromDir(dir) {
1557
+ const skillFile = join5(dir, "SKILL.md");
1558
+ if (!existsSync4(skillFile)) {
1559
+ return null;
1560
+ }
1561
+ const content = readFileSync4(skillFile, "utf-8");
1562
+ const { meta, content: skillContent } = parseFrontmatter(content);
1563
+ const name = meta.name || dirname3(dir).split("/").pop() || "";
1564
+ return {
1565
+ name,
1566
+ description: meta.description,
1567
+ disableModelInvocation: meta.disableModelInvocation,
1568
+ userInvocable: meta.userInvocable !== false,
1569
+ // default true
1570
+ allowedTools: meta.allowedTools,
1571
+ model: meta.model,
1572
+ context: meta.context,
1573
+ agent: meta.agent,
1574
+ content: skillContent,
1575
+ path: skillFile
1576
+ };
1577
+ }
1578
+ function loadSkillsFromDir(dir) {
1579
+ if (!existsSync4(dir)) {
1580
+ return [];
1581
+ }
1582
+ const skills = [];
1583
+ try {
1584
+ const entries = readdirSync4(dir, { withFileTypes: true });
1585
+ for (const entry of entries) {
1586
+ if (entry.isDirectory()) {
1587
+ const skill = loadSkillFromDir(join5(dir, entry.name));
1588
+ if (skill) {
1589
+ skills.push(skill);
1590
+ }
1591
+ }
1592
+ }
1593
+ } catch {
1594
+ }
1595
+ return skills;
1596
+ }
1597
+ var SkillsLoader = class {
1598
+ skills = /* @__PURE__ */ new Map();
1599
+ projectPath;
1600
+ constructor(projectPath) {
1601
+ this.projectPath = projectPath;
1602
+ this.reload();
1603
+ }
1604
+ /**
1605
+ * Reload all skills
1606
+ */
1607
+ reload() {
1608
+ this.skills.clear();
1609
+ const locations = [...SKILL_LOCATIONS].reverse();
1610
+ for (const location of locations) {
1611
+ let dir;
1612
+ if (location.startsWith(".")) {
1613
+ if (!this.projectPath) continue;
1614
+ dir = join5(this.projectPath, location);
1615
+ } else {
1616
+ dir = location;
1617
+ }
1618
+ const skills = loadSkillsFromDir(dir);
1619
+ for (const skill of skills) {
1620
+ this.skills.set(skill.name, skill);
1621
+ }
1622
+ }
1623
+ }
1624
+ /**
1625
+ * Get a skill by name
1626
+ */
1627
+ get(name) {
1628
+ return this.skills.get(name);
1629
+ }
1630
+ /**
1631
+ * Get all skills
1632
+ */
1633
+ all() {
1634
+ return Array.from(this.skills.values());
1635
+ }
1636
+ /**
1637
+ * Get user-invocable skills
1638
+ */
1639
+ userInvocable() {
1640
+ return this.all().filter((s) => s.userInvocable !== false);
1641
+ }
1642
+ /**
1643
+ * Get model-invocable skills
1644
+ */
1645
+ modelInvocable() {
1646
+ return this.all().filter((s) => !s.disableModelInvocation);
1647
+ }
1648
+ /**
1649
+ * Check if a skill exists
1650
+ */
1651
+ has(name) {
1652
+ return this.skills.has(name);
1653
+ }
1654
+ /**
1655
+ * Expand a skill's content with arguments
1656
+ */
1657
+ expand(name, args) {
1658
+ const skill = this.get(name);
1659
+ if (!skill) return null;
1660
+ let content = skill.content;
1661
+ if (args) {
1662
+ const argParts = args.split(/\s+/);
1663
+ content = content.replace(/\$ARGUMENTS/g, args);
1664
+ content = content.replace(/\$ARGUMENTS\[(\d+)\]/g, (_, n) => argParts[parseInt(n)] || "");
1665
+ content = content.replace(/\$(\d+)/g, (_, n) => argParts[parseInt(n) - 1] || "");
1666
+ }
1667
+ return content;
1668
+ }
1669
+ };
1670
+ function createSkillsLoader(projectPath) {
1671
+ return new SkillsLoader(projectPath);
1672
+ }
1673
+
1674
+ // src/context/index.ts
1675
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync5 } from "fs";
1676
+ import { join as join6, basename } from "path";
1677
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
1678
+ "node_modules",
1679
+ ".git",
1680
+ ".svn",
1681
+ ".hg",
1682
+ "dist",
1683
+ "build",
1684
+ ".next",
1685
+ ".nuxt",
1686
+ ".turbo",
1687
+ "coverage",
1688
+ "__pycache__",
1689
+ ".pytest_cache",
1690
+ "venv",
1691
+ ".venv"
1692
+ ]);
1693
+ function buildTree(dir, depth = 3, current = 0) {
1694
+ if (current >= depth) return [];
1695
+ try {
1696
+ const entries = readdirSync5(dir, { withFileTypes: true });
1697
+ const result = [];
1698
+ for (const entry of entries) {
1699
+ if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) {
1700
+ continue;
1701
+ }
1702
+ if (entry.isDirectory()) {
1703
+ result.push({
1704
+ name: entry.name,
1705
+ type: "directory",
1706
+ children: buildTree(join6(dir, entry.name), depth, current + 1)
1707
+ });
1708
+ } else {
1709
+ result.push({
1710
+ name: entry.name,
1711
+ type: "file"
1712
+ });
1713
+ }
1714
+ }
1715
+ return result.sort((a, b) => {
1716
+ if (a.type === b.type) return a.name.localeCompare(b.name);
1717
+ return a.type === "directory" ? -1 : 1;
1718
+ });
1719
+ } catch {
1720
+ return [];
1721
+ }
1722
+ }
1723
+ function formatTree(tree, prefix = "") {
1724
+ const lines = [];
1725
+ for (let i = 0; i < tree.length; i++) {
1726
+ const item = tree[i];
1727
+ const isLast = i === tree.length - 1;
1728
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1729
+ const childPrefix = isLast ? " " : "\u2502 ";
1730
+ const suffix = item.type === "directory" ? "/" : "";
1731
+ lines.push(`${prefix}${connector}${item.name}${suffix}`);
1732
+ if (item.children && item.children.length > 0) {
1733
+ lines.push(formatTree(item.children, prefix + childPrefix));
1734
+ }
1735
+ }
1736
+ return lines.join("\n");
1737
+ }
1738
+ function findGitRoot(dir) {
1739
+ let current = dir;
1740
+ while (current !== "/") {
1741
+ if (existsSync5(join6(current, ".git"))) {
1742
+ return current;
1743
+ }
1744
+ current = join6(current, "..");
1745
+ }
1746
+ return void 0;
1747
+ }
1748
+ function loadProjectContext(projectPath) {
1749
+ const name = basename(projectPath);
1750
+ const gitRoot = findGitRoot(projectPath);
1751
+ let packageJson;
1752
+ const packageJsonPath = join6(projectPath, "package.json");
1753
+ if (existsSync5(packageJsonPath)) {
1754
+ try {
1755
+ packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
1756
+ } catch {
1757
+ }
1758
+ }
1759
+ let readme;
1760
+ const readmeNames = ["README.md", "README.txt", "README", "readme.md"];
1761
+ for (const readmeName of readmeNames) {
1762
+ const readmePath = join6(projectPath, readmeName);
1763
+ if (existsSync5(readmePath)) {
1764
+ try {
1765
+ readme = readFileSync5(readmePath, "utf-8");
1766
+ if (readme.length > 2e3) {
1767
+ readme = readme.slice(0, 2e3) + "\n...(truncated)";
1768
+ }
1769
+ } catch {
1770
+ }
1771
+ break;
1772
+ }
1773
+ }
1774
+ const tree = buildTree(projectPath);
1775
+ const structure = formatTree(tree);
1776
+ return {
1777
+ path: projectPath,
1778
+ name,
1779
+ gitRoot,
1780
+ packageJson,
1781
+ readme,
1782
+ structure
1783
+ };
1784
+ }
1785
+ function formatProjectContext(context) {
1786
+ const sections = [];
1787
+ sections.push(`## Project: ${context.name}`);
1788
+ sections.push(`Path: ${context.path}`);
1789
+ if (context.gitRoot) {
1790
+ sections.push(`Git root: ${context.gitRoot}`);
1791
+ }
1792
+ if (context.packageJson) {
1793
+ const pkg = context.packageJson;
1794
+ if (pkg.description) {
1795
+ sections.push(`
1796
+ Description: ${pkg.description}`);
1797
+ }
1798
+ if (pkg.scripts) {
1799
+ sections.push(`
1800
+ Available scripts: ${Object.keys(pkg.scripts).join(", ")}`);
1801
+ }
1802
+ }
1803
+ sections.push(`
1804
+ ## Project Structure
1805
+ \`\`\`
1806
+ ${context.structure}
1807
+ \`\`\``);
1808
+ if (context.readme) {
1809
+ sections.push(`
1810
+ ## README
1811
+ ${context.readme}`);
1812
+ }
1813
+ return sections.join("\n");
1814
+ }
1815
+ var Memory = class {
1816
+ entries = [];
1817
+ maxEntries = 100;
1818
+ /**
1819
+ * Add an entry
1820
+ */
1821
+ add(type, content) {
1822
+ this.entries.push({
1823
+ type,
1824
+ content,
1825
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1826
+ });
1827
+ if (this.entries.length > this.maxEntries) {
1828
+ this.entries = this.entries.slice(-this.maxEntries);
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Get all entries
1833
+ */
1834
+ all() {
1835
+ return [...this.entries];
1836
+ }
1837
+ /**
1838
+ * Get entries by type
1839
+ */
1840
+ byType(type) {
1841
+ return this.entries.filter((e) => e.type === type);
1842
+ }
1843
+ /**
1844
+ * Format memory for system prompt
1845
+ */
1846
+ format() {
1847
+ if (this.entries.length === 0) return "";
1848
+ const sections = ["## Memory"];
1849
+ const facts = this.byType("fact");
1850
+ if (facts.length > 0) {
1851
+ sections.push("\n### Facts");
1852
+ for (const fact of facts) {
1853
+ sections.push(`- ${fact.content}`);
1854
+ }
1855
+ }
1856
+ const preferences = this.byType("preference");
1857
+ if (preferences.length > 0) {
1858
+ sections.push("\n### User Preferences");
1859
+ for (const pref of preferences) {
1860
+ sections.push(`- ${pref.content}`);
1861
+ }
1862
+ }
1863
+ const summaries = this.byType("summary");
1864
+ if (summaries.length > 0) {
1865
+ sections.push("\n### Previous Context");
1866
+ for (const summary of summaries.slice(-3)) {
1867
+ sections.push(`- ${summary.content}`);
1868
+ }
1869
+ }
1870
+ return sections.join("\n");
1871
+ }
1872
+ /**
1873
+ * Clear all entries
1874
+ */
1875
+ clear() {
1876
+ this.entries = [];
1877
+ }
1878
+ /**
1879
+ * Export entries
1880
+ */
1881
+ export() {
1882
+ return [...this.entries];
1883
+ }
1884
+ /**
1885
+ * Import entries
1886
+ */
1887
+ import(entries) {
1888
+ this.entries = [...entries];
1889
+ }
1890
+ };
1891
+ function createMemory() {
1892
+ return new Memory();
1893
+ }
1894
+
1895
+ // src/index.ts
1896
+ var VERSION = "0.1.0";
1897
+ export {
1898
+ BUILTIN_TOOLS,
1899
+ BashTool,
1900
+ CONFIG_PATHS,
1901
+ ConfigSchema,
1902
+ DEFAULT_SYSTEM_PROMPT,
1903
+ EditTool,
1904
+ GlobTool,
1905
+ GrepTool,
1906
+ HookSchema,
1907
+ HooksRunner,
1908
+ ListTool,
1909
+ MCPClient,
1910
+ MCPConnection,
1911
+ MCPServerSchema,
1912
+ Memory,
1913
+ PermissionChecker,
1914
+ PermissionsSchema,
1915
+ Provider,
1916
+ ReadTool,
1917
+ SessionManager,
1918
+ SkillsLoader,
1919
+ ToolRegistry,
1920
+ VERSION,
1921
+ WriteTool,
1922
+ appendMessage,
1923
+ createAgentState,
1924
+ createHooksRunner,
1925
+ createMCPClient,
1926
+ createMemory,
1927
+ createPermissionChecker,
1928
+ createProvider,
1929
+ createProviderInstance,
1930
+ createSession,
1931
+ createSessionManager,
1932
+ createSkillsLoader,
1933
+ createToolRegistry,
1934
+ deleteSession,
1935
+ forkSession,
1936
+ formatProjectContext,
1937
+ getDataDir,
1938
+ getRecentSession,
1939
+ getSessionsDir,
1940
+ getUserConfigPath,
1941
+ listSessions,
1942
+ loadConfig,
1943
+ loadProjectContext,
1944
+ loadSession,
1945
+ runAgent,
1946
+ runAgentLoop,
1947
+ saveConfig,
1948
+ saveSession
1949
+ };