squads-cli 0.4.10 → 0.4.11

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 CHANGED
@@ -3,7 +3,1037 @@ import { createRequire } from "module";
3
3
  var require2 = createRequire(import.meta.url);
4
4
  var pkg = require2("../package.json");
5
5
  var version = pkg.version;
6
+
7
+ // src/lib/squad-parser.ts
8
+ import { readFileSync, existsSync, readdirSync, writeFileSync } from "fs";
9
+ import { join, basename } from "path";
10
+ import matter from "gray-matter";
11
+ function findSquadsDir() {
12
+ let dir = process.cwd();
13
+ for (let i = 0; i < 5; i++) {
14
+ const squadsPath = join(dir, ".agents", "squads");
15
+ if (existsSync(squadsPath)) {
16
+ return squadsPath;
17
+ }
18
+ const parent = join(dir, "..");
19
+ if (parent === dir) break;
20
+ dir = parent;
21
+ }
22
+ return null;
23
+ }
24
+ function findProjectRoot() {
25
+ const squadsDir = findSquadsDir();
26
+ if (!squadsDir) return null;
27
+ return join(squadsDir, "..", "..");
28
+ }
29
+ function listSquads(squadsDir) {
30
+ const squads = [];
31
+ const entries = readdirSync(squadsDir, { withFileTypes: true });
32
+ for (const entry of entries) {
33
+ if (entry.isDirectory() && !entry.name.startsWith("_")) {
34
+ const squadFile = join(squadsDir, entry.name, "SQUAD.md");
35
+ if (existsSync(squadFile)) {
36
+ squads.push(entry.name);
37
+ }
38
+ }
39
+ }
40
+ return squads;
41
+ }
42
+ function listAgents(squadsDir, squadName) {
43
+ const agents = [];
44
+ const dirs = squadName ? [squadName] : readdirSync(squadsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_")).map((e) => e.name);
45
+ for (const dir of dirs) {
46
+ const squadPath = join(squadsDir, dir);
47
+ if (!existsSync(squadPath)) continue;
48
+ const files = readdirSync(squadPath);
49
+ for (const file of files) {
50
+ if (file.endsWith(".md") && file !== "SQUAD.md") {
51
+ const agentName = file.replace(".md", "");
52
+ agents.push({
53
+ name: agentName,
54
+ role: `Agent in ${dir}`,
55
+ trigger: "manual",
56
+ filePath: join(squadPath, file)
57
+ });
58
+ }
59
+ }
60
+ }
61
+ return agents;
62
+ }
63
+ function parseSquadFile(filePath) {
64
+ const rawContent = readFileSync(filePath, "utf-8");
65
+ const { data: frontmatter, content: bodyContent } = matter(rawContent);
66
+ const fm = frontmatter;
67
+ const lines = bodyContent.split("\n");
68
+ const squad = {
69
+ name: fm.name || basename(filePath).replace(".md", ""),
70
+ mission: fm.mission || "",
71
+ agents: [],
72
+ pipelines: [],
73
+ triggers: { scheduled: [], event: [], manual: [] },
74
+ dependencies: [],
75
+ outputPath: "",
76
+ goals: [],
77
+ // Apply frontmatter fields
78
+ effort: fm.effort,
79
+ context: fm.context,
80
+ repo: fm.repo,
81
+ stack: fm.stack
82
+ };
83
+ let currentSection = "";
84
+ let inTable = false;
85
+ let tableHeaders = [];
86
+ for (const line of lines) {
87
+ if (line.startsWith("# Squad:")) {
88
+ squad.name = line.replace("# Squad:", "").trim().toLowerCase();
89
+ continue;
90
+ }
91
+ if (line.startsWith("## ")) {
92
+ currentSection = line.replace("## ", "").trim().toLowerCase();
93
+ inTable = false;
94
+ continue;
95
+ }
96
+ if (currentSection === "mission" && line.trim() && !line.startsWith("#")) {
97
+ if (!squad.mission) {
98
+ squad.mission = line.trim();
99
+ }
100
+ }
101
+ const effortMatch = line.match(/^effort:\s*(high|medium|low)/i);
102
+ if (effortMatch && !squad.effort) {
103
+ squad.effort = effortMatch[1].toLowerCase();
104
+ }
105
+ if (currentSection.includes("agent") || currentSection.includes("orchestrator") || currentSection.includes("evaluator") || currentSection.includes("builder") || currentSection.includes("priority")) {
106
+ if (line.includes("|") && line.includes("Agent")) {
107
+ inTable = true;
108
+ tableHeaders = line.split("|").map((h) => h.trim().toLowerCase());
109
+ continue;
110
+ }
111
+ if (inTable && line.includes("|") && !line.includes("---")) {
112
+ const cells = line.split("|").map((c) => c.trim().replace(/`/g, ""));
113
+ const agentIdx = tableHeaders.findIndex((h) => h === "agent");
114
+ const roleIdx = tableHeaders.findIndex((h) => h === "role");
115
+ const triggerIdx = tableHeaders.findIndex((h) => h === "trigger");
116
+ const statusIdx = tableHeaders.findIndex((h) => h === "status");
117
+ const effortIdx = tableHeaders.findIndex((h) => h === "effort");
118
+ if (agentIdx >= 0 && cells[agentIdx]) {
119
+ const effortValue = effortIdx >= 0 ? cells[effortIdx]?.toLowerCase() : void 0;
120
+ const effort = ["high", "medium", "low"].includes(effortValue || "") ? effortValue : void 0;
121
+ squad.agents.push({
122
+ name: cells[agentIdx],
123
+ role: roleIdx >= 0 ? cells[roleIdx] : "",
124
+ trigger: triggerIdx >= 0 ? cells[triggerIdx] : "manual",
125
+ status: statusIdx >= 0 ? cells[statusIdx] : "active",
126
+ effort
127
+ });
128
+ }
129
+ }
130
+ }
131
+ if (line.includes("\u2192") && line.includes("`")) {
132
+ const pipelineMatch = line.match(/`([^`]+)`\s*→\s*`([^`]+)`/g);
133
+ if (pipelineMatch) {
134
+ const agentNames = line.match(/`([^`]+)`/g)?.map((m) => m.replace(/`/g, "")) || [];
135
+ if (agentNames.length >= 2) {
136
+ squad.pipelines.push({
137
+ name: "default",
138
+ agents: agentNames
139
+ });
140
+ }
141
+ }
142
+ }
143
+ if (line.toLowerCase().includes("pipeline:")) {
144
+ const pipelineContent = line.split(":")[1];
145
+ if (pipelineContent && pipelineContent.includes("\u2192")) {
146
+ const agentNames = pipelineContent.match(/`([^`]+)`/g)?.map((m) => m.replace(/`/g, "")) || [];
147
+ if (agentNames.length >= 2) {
148
+ squad.pipelines.push({
149
+ name: "default",
150
+ agents: agentNames
151
+ });
152
+ }
153
+ }
154
+ }
155
+ if (line.toLowerCase().includes("primary") && line.includes("`")) {
156
+ const match = line.match(/`([^`]+)`/);
157
+ if (match) {
158
+ squad.outputPath = match[1].replace(/\/$/, "");
159
+ }
160
+ }
161
+ if (currentSection === "goals") {
162
+ const goalMatch = line.match(/^-\s*\[([ x])\]\s*(.+)$/);
163
+ if (goalMatch) {
164
+ const completed = goalMatch[1] === "x";
165
+ let description = goalMatch[2].trim();
166
+ let progress;
167
+ const progressMatch = description.match(/\(progress:\s*([^)]+)\)/i);
168
+ if (progressMatch) {
169
+ progress = progressMatch[1];
170
+ description = description.replace(progressMatch[0], "").trim();
171
+ }
172
+ squad.goals.push({
173
+ description,
174
+ completed,
175
+ progress
176
+ });
177
+ }
178
+ }
179
+ }
180
+ return squad;
181
+ }
182
+ function loadSquad(squadName) {
183
+ const squadsDir = findSquadsDir();
184
+ if (!squadsDir) return null;
185
+ const squadFile = join(squadsDir, squadName, "SQUAD.md");
186
+ if (!existsSync(squadFile)) return null;
187
+ return parseSquadFile(squadFile);
188
+ }
189
+ function loadAgentDefinition(agentPath) {
190
+ if (!existsSync(agentPath)) return "";
191
+ return readFileSync(agentPath, "utf-8");
192
+ }
193
+ function addGoalToSquad(squadName, goal) {
194
+ const squadsDir = findSquadsDir();
195
+ if (!squadsDir) return false;
196
+ const squadFile = join(squadsDir, squadName, "SQUAD.md");
197
+ if (!existsSync(squadFile)) return false;
198
+ let content = readFileSync(squadFile, "utf-8");
199
+ if (!content.includes("## Goals")) {
200
+ const insertPoint = content.indexOf("## Dependencies");
201
+ if (insertPoint > 0) {
202
+ content = content.slice(0, insertPoint) + `## Goals
203
+
204
+ - [ ] ${goal}
205
+
206
+ ` + content.slice(insertPoint);
207
+ } else {
208
+ content += `
209
+ ## Goals
210
+
211
+ - [ ] ${goal}
212
+ `;
213
+ }
214
+ } else {
215
+ const goalsIdx = content.indexOf("## Goals");
216
+ const nextSectionIdx = content.indexOf("\n## ", goalsIdx + 1);
217
+ const endIdx = nextSectionIdx > 0 ? nextSectionIdx : content.length;
218
+ const goalsSection = content.slice(goalsIdx, endIdx);
219
+ const lastGoalMatch = goalsSection.match(/^-\s*\[[ x]\].+$/gm);
220
+ if (lastGoalMatch) {
221
+ const lastGoal = lastGoalMatch[lastGoalMatch.length - 1];
222
+ const lastGoalIdx = content.lastIndexOf(lastGoal, endIdx);
223
+ const insertPos = lastGoalIdx + lastGoal.length;
224
+ content = content.slice(0, insertPos) + `
225
+ - [ ] ${goal}` + content.slice(insertPos);
226
+ } else {
227
+ const headerEnd = goalsIdx + "## Goals".length;
228
+ content = content.slice(0, headerEnd) + `
229
+
230
+ - [ ] ${goal}` + content.slice(headerEnd);
231
+ }
232
+ }
233
+ writeFileSync(squadFile, content);
234
+ return true;
235
+ }
236
+ function updateGoalInSquad(squadName, goalIndex, updates) {
237
+ const squadsDir = findSquadsDir();
238
+ if (!squadsDir) return false;
239
+ const squadFile = join(squadsDir, squadName, "SQUAD.md");
240
+ if (!existsSync(squadFile)) return false;
241
+ const content = readFileSync(squadFile, "utf-8");
242
+ const lines = content.split("\n");
243
+ let currentSection = "";
244
+ let goalCount = 0;
245
+ for (let i = 0; i < lines.length; i++) {
246
+ const line = lines[i];
247
+ if (line.startsWith("## ")) {
248
+ currentSection = line.replace("## ", "").trim().toLowerCase();
249
+ continue;
250
+ }
251
+ if (currentSection === "goals") {
252
+ const goalMatch = line.match(/^-\s*\[([ x])\]\s*(.+)$/);
253
+ if (goalMatch) {
254
+ if (goalCount === goalIndex) {
255
+ let newLine = "- [" + (updates.completed ? "x" : " ") + "] " + goalMatch[2];
256
+ if (updates.progress !== void 0) {
257
+ newLine = newLine.replace(/\s*\(progress:\s*[^)]+\)/i, "");
258
+ if (updates.progress) {
259
+ newLine += ` (progress: ${updates.progress})`;
260
+ }
261
+ }
262
+ lines[i] = newLine;
263
+ writeFileSync(squadFile, lines.join("\n"));
264
+ return true;
265
+ }
266
+ goalCount++;
267
+ }
268
+ }
269
+ }
270
+ return false;
271
+ }
272
+
273
+ // src/lib/condenser/tokens.ts
274
+ var RATIOS = {
275
+ english: 4,
276
+ // Standard English text
277
+ code: 3.5,
278
+ // Code tends to have more tokens per char
279
+ json: 3,
280
+ // JSON has many punctuation tokens
281
+ mixed: 3.75
282
+ // Default for mixed content
283
+ };
284
+ function estimateTokens(text, type = "mixed") {
285
+ if (!text) return 0;
286
+ const ratio = RATIOS[type];
287
+ return Math.ceil(text.length / ratio);
288
+ }
289
+ function estimateMessageTokens(message) {
290
+ let tokens = 4;
291
+ if (typeof message.content === "string") {
292
+ tokens += estimateTokens(message.content);
293
+ } else if (Array.isArray(message.content)) {
294
+ for (const part of message.content) {
295
+ if (part.text) {
296
+ tokens += estimateTokens(part.text);
297
+ }
298
+ tokens += 10;
299
+ }
300
+ }
301
+ return tokens;
302
+ }
303
+ var MODEL_LIMITS = {
304
+ // Anthropic models
305
+ "claude-opus-4-5-20251101": 2e5,
306
+ "claude-sonnet-4-20250514": 2e5,
307
+ "claude-3-5-haiku-20241022": 2e5,
308
+ // Aliases
309
+ opus: 2e5,
310
+ sonnet: 2e5,
311
+ haiku: 2e5,
312
+ // Default fallback
313
+ default: 2e5
314
+ };
315
+ function getModelLimit(model) {
316
+ return MODEL_LIMITS[model] ?? MODEL_LIMITS.default;
317
+ }
318
+ function createTracker(model = "default") {
319
+ return {
320
+ used: 0,
321
+ limit: getModelLimit(model),
322
+ percentage: 0,
323
+ breakdown: {
324
+ system: 0,
325
+ user: 0,
326
+ assistant: 0,
327
+ tools: 0
328
+ }
329
+ };
330
+ }
331
+ function updateTracker(tracker, content, category = "assistant") {
332
+ const tokens = estimateTokens(content);
333
+ tracker.used += tokens;
334
+ tracker.breakdown[category] += tokens;
335
+ tracker.percentage = tracker.used / tracker.limit;
336
+ }
337
+ function updateTrackerFromMessage(tracker, message) {
338
+ const tokens = estimateMessageTokens(message);
339
+ const category = mapRoleToCategory(message.role);
340
+ tracker.used += tokens;
341
+ tracker.breakdown[category] += tokens;
342
+ tracker.percentage = tracker.used / tracker.limit;
343
+ }
344
+ function mapRoleToCategory(role) {
345
+ switch (role) {
346
+ case "system":
347
+ return "system";
348
+ case "user":
349
+ return "user";
350
+ case "assistant":
351
+ return "assistant";
352
+ case "tool":
353
+ case "tool_result":
354
+ return "tools";
355
+ default:
356
+ return "assistant";
357
+ }
358
+ }
359
+ var DEFAULT_THRESHOLDS = {
360
+ light: 0.7,
361
+ medium: 0.85,
362
+ heavy: 0.95
363
+ };
364
+ function getCompressionLevel(tracker, thresholds = DEFAULT_THRESHOLDS) {
365
+ if (tracker.percentage >= thresholds.heavy) return "heavy";
366
+ if (tracker.percentage >= thresholds.medium) return "medium";
367
+ if (tracker.percentage >= thresholds.light) return "light";
368
+ return "none";
369
+ }
370
+ function formatTrackerStatus(tracker) {
371
+ const pct = (tracker.percentage * 100).toFixed(1);
372
+ const used = (tracker.used / 1e3).toFixed(1);
373
+ const limit = (tracker.limit / 1e3).toFixed(0);
374
+ const level = getCompressionLevel(tracker);
375
+ const levelIndicator = level === "none" ? "" : level === "light" ? " [!]" : level === "medium" ? " [!!]" : " [!!!]";
376
+ return `${used}K / ${limit}K tokens (${pct}%)${levelIndicator}`;
377
+ }
378
+
379
+ // src/lib/condenser/deduplication.ts
380
+ function hashContent(content) {
381
+ const prefix = content.slice(0, 100);
382
+ return `${content.length}:${prefix}`;
383
+ }
384
+ var FileDeduplicator = class {
385
+ /** Map of file path to all reads of that file */
386
+ reads = /* @__PURE__ */ new Map();
387
+ /** Current turn index */
388
+ currentTurn = 0;
389
+ /**
390
+ * Record a file read.
391
+ *
392
+ * @param path - File path that was read
393
+ * @param content - File content that was read
394
+ */
395
+ trackRead(path, content) {
396
+ const record = {
397
+ path,
398
+ turnIndex: this.currentTurn,
399
+ tokenCount: estimateTokens(content, "code"),
400
+ contentHash: hashContent(content)
401
+ };
402
+ const existing = this.reads.get(path) || [];
403
+ existing.push(record);
404
+ this.reads.set(path, existing);
405
+ }
406
+ /**
407
+ * Advance to next turn.
408
+ */
409
+ nextTurn() {
410
+ this.currentTurn++;
411
+ }
412
+ /**
413
+ * Get current turn index.
414
+ */
415
+ getTurn() {
416
+ return this.currentTurn;
417
+ }
418
+ /**
419
+ * Check if a file has been read before.
420
+ *
421
+ * @param path - File path to check
422
+ * @returns Previous read record if exists
423
+ */
424
+ getPreviousRead(path) {
425
+ const reads = this.reads.get(path);
426
+ if (!reads || reads.length === 0) return void 0;
427
+ return reads[reads.length - 1];
428
+ }
429
+ /**
430
+ * Get all files that have been read multiple times.
431
+ *
432
+ * @returns Map of path to read count
433
+ */
434
+ getDuplicateReads() {
435
+ const duplicates = /* @__PURE__ */ new Map();
436
+ for (const [path, reads] of this.reads) {
437
+ if (reads.length > 1) {
438
+ duplicates.set(path, reads.length);
439
+ }
440
+ }
441
+ return duplicates;
442
+ }
443
+ /**
444
+ * Calculate potential token savings from deduplication.
445
+ */
446
+ getPotentialSavings() {
447
+ let savings = 0;
448
+ for (const reads of this.reads.values()) {
449
+ if (reads.length > 1) {
450
+ for (let i = 0; i < reads.length - 1; i++) {
451
+ savings += reads[i].tokenCount;
452
+ }
453
+ }
454
+ }
455
+ return savings;
456
+ }
457
+ /**
458
+ * Generate a deduplication reference message.
459
+ *
460
+ * @param path - File path
461
+ * @param previousTurn - Turn where file was previously read
462
+ */
463
+ static createReference(path, previousTurn) {
464
+ return `[File "${path}" was read at turn ${previousTurn}. Content unchanged.]`;
465
+ }
466
+ /**
467
+ * Reset tracker state.
468
+ */
469
+ reset() {
470
+ this.reads.clear();
471
+ this.currentTurn = 0;
472
+ }
473
+ /**
474
+ * Get statistics for debugging.
475
+ */
476
+ getStats() {
477
+ let totalReads = 0;
478
+ let duplicateReads = 0;
479
+ for (const reads of this.reads.values()) {
480
+ totalReads += reads.length;
481
+ if (reads.length > 1) {
482
+ duplicateReads += reads.length - 1;
483
+ }
484
+ }
485
+ return {
486
+ filesTracked: this.reads.size,
487
+ totalReads,
488
+ duplicateReads,
489
+ potentialSavings: this.getPotentialSavings()
490
+ };
491
+ }
492
+ };
493
+
494
+ // src/lib/condenser/pruning.ts
495
+ var DEFAULT_CONFIG = {
496
+ protectRecent: 4e4,
497
+ minimumPrunable: 2e4,
498
+ protectedTools: ["skill", "memory", "goal"]
499
+ };
500
+ function createPrunedPlaceholder(toolName, tokensSaved) {
501
+ return `[Tool output pruned: ${toolName} (~${Math.round(tokensSaved / 1e3)}K tokens)]`;
502
+ }
503
+ var TokenPruner = class {
504
+ config;
505
+ constructor(config = {}) {
506
+ this.config = { ...DEFAULT_CONFIG, ...config };
507
+ }
508
+ /**
509
+ * Prune messages to reduce token count.
510
+ *
511
+ * Strategy:
512
+ * 1. Scan messages backward from newest to oldest
513
+ * 2. Accumulate tokens for tool outputs
514
+ * 3. Mark outputs beyond protection window for pruning
515
+ * 4. Replace pruned outputs with placeholders
516
+ *
517
+ * @param messages - Messages to prune
518
+ * @returns Pruned messages (new array, originals not mutated)
519
+ */
520
+ pruneMessages(messages) {
521
+ const annotated = messages.map((msg) => ({
522
+ ...msg,
523
+ _tokens: estimateMessageTokens(msg)
524
+ }));
525
+ const analysis = this.analyzePrunability(annotated);
526
+ if (analysis.prunableTokens < this.config.minimumPrunable) {
527
+ return messages;
528
+ }
529
+ return this.applyPruning(annotated, analysis.protectionIndex);
530
+ }
531
+ /**
532
+ * Analyze which messages can be pruned.
533
+ */
534
+ analyzePrunability(messages) {
535
+ let protectedTokens = 0;
536
+ let protectionIndex = messages.length;
537
+ for (let i = messages.length - 1; i >= 0; i--) {
538
+ const msg = messages[i];
539
+ if (!this.isToolResult(msg)) {
540
+ protectedTokens += msg._tokens;
541
+ continue;
542
+ }
543
+ if (this.isProtectedTool(msg)) {
544
+ protectedTokens += msg._tokens;
545
+ continue;
546
+ }
547
+ if (protectedTokens + msg._tokens <= this.config.protectRecent) {
548
+ protectedTokens += msg._tokens;
549
+ } else {
550
+ protectionIndex = i + 1;
551
+ break;
552
+ }
553
+ }
554
+ let prunableTokens = 0;
555
+ for (let i = 0; i < protectionIndex; i++) {
556
+ const msg = messages[i];
557
+ if (this.isToolResult(msg) && !this.isProtectedTool(msg)) {
558
+ prunableTokens += msg._tokens;
559
+ }
560
+ }
561
+ return { prunableTokens, protectedTokens, protectionIndex };
562
+ }
563
+ /**
564
+ * Apply pruning to messages before the protection index.
565
+ */
566
+ applyPruning(messages, protectionIndex) {
567
+ return messages.map((msg, i) => {
568
+ if (i >= protectionIndex) {
569
+ const { _tokens, _prunable, ...clean } = msg;
570
+ return clean;
571
+ }
572
+ if (!this.isToolResult(msg)) {
573
+ const { _tokens, _prunable, ...clean } = msg;
574
+ return clean;
575
+ }
576
+ if (this.isProtectedTool(msg)) {
577
+ const { _tokens, _prunable, ...clean } = msg;
578
+ return clean;
579
+ }
580
+ return this.createPrunedMessage(msg);
581
+ });
582
+ }
583
+ /**
584
+ * Create a pruned version of a message.
585
+ */
586
+ createPrunedMessage(msg) {
587
+ const toolName = this.getToolName(msg);
588
+ const placeholder = createPrunedPlaceholder(toolName, msg._tokens);
589
+ if (typeof msg.content === "string") {
590
+ return {
591
+ role: msg.role,
592
+ content: placeholder
593
+ };
594
+ }
595
+ const content = msg.content.map((part) => {
596
+ if (part.type === "tool_result" && part.text) {
597
+ return {
598
+ ...part,
599
+ text: placeholder,
600
+ _pruned: true,
601
+ _prunedAt: Date.now()
602
+ };
603
+ }
604
+ return part;
605
+ });
606
+ return {
607
+ role: msg.role,
608
+ content
609
+ };
610
+ }
611
+ /**
612
+ * Check if a message is a tool result.
613
+ */
614
+ isToolResult(msg) {
615
+ if (msg.role === "tool") return true;
616
+ if (Array.isArray(msg.content)) {
617
+ return msg.content.some((part) => part.type === "tool_result");
618
+ }
619
+ return false;
620
+ }
621
+ /**
622
+ * Check if a tool is in the protected list.
623
+ */
624
+ isProtectedTool(msg) {
625
+ const toolName = this.getToolName(msg);
626
+ return this.config.protectedTools.includes(toolName.toLowerCase());
627
+ }
628
+ /**
629
+ * Extract tool name from a message.
630
+ */
631
+ getToolName(msg) {
632
+ if (Array.isArray(msg.content)) {
633
+ for (const part of msg.content) {
634
+ if (part.name) return part.name;
635
+ }
636
+ }
637
+ if (typeof msg.content === "string") {
638
+ const match = msg.content.match(/Tool (?:output|result).*?:\s*(\w+)/i);
639
+ if (match) return match[1];
640
+ }
641
+ return "unknown";
642
+ }
643
+ /**
644
+ * Get statistics about potential pruning.
645
+ */
646
+ getStats(messages) {
647
+ const annotated = messages.map((msg) => ({
648
+ ...msg,
649
+ _tokens: estimateMessageTokens(msg)
650
+ }));
651
+ const totalTokens = annotated.reduce((sum, msg) => sum + msg._tokens, 0);
652
+ const analysis = this.analyzePrunability(annotated);
653
+ return {
654
+ totalTokens,
655
+ prunableTokens: analysis.prunableTokens,
656
+ protectedTokens: analysis.protectedTokens,
657
+ savingsPercentage: totalTokens > 0 ? analysis.prunableTokens / totalTokens * 100 : 0
658
+ };
659
+ }
660
+ };
661
+
662
+ // src/lib/condenser/summarizer.ts
663
+ import Anthropic from "@anthropic-ai/sdk";
664
+ var DEFAULT_CONFIG2 = {
665
+ keepFirst: 4,
666
+ keepLast: 20,
667
+ model: "claude-3-5-haiku-20241022",
668
+ // Cheap and fast
669
+ maxSummaryTokens: 2e3
670
+ };
671
+ var SUMMARIZATION_PROMPT = `You are summarizing a conversation between a user and an AI assistant to reduce context length while preserving essential information.
672
+
673
+ <conversation_to_summarize>
674
+ {{MIDDLE_CONTENT}}
675
+ </conversation_to_summarize>
676
+
677
+ Create a concise summary that preserves:
678
+ 1. **User's goals** - What the user is trying to accomplish
679
+ 2. **Progress made** - Key actions taken and their outcomes
680
+ 3. **Current state** - What's done vs. what still needs to be done
681
+ 4. **Critical context** - File paths, error messages, decisions made
682
+ 5. **Blockers** - Any issues that need to be resolved
683
+
684
+ Format your summary as a structured note that the assistant can reference to continue the work.
685
+
686
+ Keep the summary under {{MAX_TOKENS}} tokens. Focus on actionable information.`;
687
+ var ConversationSummarizer = class {
688
+ config;
689
+ client = null;
690
+ constructor(config = {}) {
691
+ this.config = { ...DEFAULT_CONFIG2, ...config };
692
+ }
693
+ /**
694
+ * Get or create Anthropic client.
695
+ */
696
+ getClient() {
697
+ if (!this.client) {
698
+ this.client = new Anthropic();
699
+ }
700
+ return this.client;
701
+ }
702
+ /**
703
+ * Summarize messages to reduce token count.
704
+ *
705
+ * Strategy:
706
+ * 1. Keep first N messages (system prompt, initial context)
707
+ * 2. Keep last M messages (recent context, current task)
708
+ * 3. Summarize everything in between
709
+ *
710
+ * @param messages - Messages to summarize
711
+ * @returns Summarized messages
712
+ */
713
+ async summarize(messages) {
714
+ if (messages.length <= this.config.keepFirst + this.config.keepLast) {
715
+ return messages;
716
+ }
717
+ const firstMessages = messages.slice(0, this.config.keepFirst);
718
+ const middleMessages = messages.slice(this.config.keepFirst, -this.config.keepLast);
719
+ const lastMessages = messages.slice(-this.config.keepLast);
720
+ const summary = await this.generateSummary(middleMessages);
721
+ const summaryMessage = {
722
+ role: "user",
723
+ content: `[Context Summary - ${middleMessages.length} messages condensed]
724
+
725
+ ${summary}`
726
+ };
727
+ return [...firstMessages, summaryMessage, ...lastMessages];
728
+ }
729
+ /**
730
+ * Generate a summary of the middle messages.
731
+ */
732
+ async generateSummary(messages) {
733
+ const middleContent = this.formatMessagesForSummary(messages);
734
+ const prompt = SUMMARIZATION_PROMPT.replace("{{MIDDLE_CONTENT}}", middleContent).replace(
735
+ "{{MAX_TOKENS}}",
736
+ String(this.config.maxSummaryTokens)
737
+ );
738
+ try {
739
+ const client = this.getClient();
740
+ const response = await client.messages.create({
741
+ model: this.config.model,
742
+ max_tokens: this.config.maxSummaryTokens,
743
+ messages: [
744
+ {
745
+ role: "user",
746
+ content: prompt
747
+ }
748
+ ]
749
+ });
750
+ const textBlock = response.content.find((block) => block.type === "text");
751
+ if (textBlock && "text" in textBlock) {
752
+ return textBlock.text;
753
+ }
754
+ return "[Summary generation failed - no text in response]";
755
+ } catch (error) {
756
+ const message = error instanceof Error ? error.message : String(error);
757
+ return `[Summary generation failed: ${message}]`;
758
+ }
759
+ }
760
+ /**
761
+ * Format messages for the summarization prompt.
762
+ */
763
+ formatMessagesForSummary(messages) {
764
+ return messages.map((msg, i) => {
765
+ const role = msg.role.toUpperCase();
766
+ const content = this.extractContent(msg);
767
+ return `[${i + 1}] ${role}:
768
+ ${content}`;
769
+ }).join("\n\n---\n\n");
770
+ }
771
+ /**
772
+ * Extract text content from a message.
773
+ */
774
+ extractContent(msg) {
775
+ if (typeof msg.content === "string") {
776
+ return this.truncateContent(msg.content);
777
+ }
778
+ const parts = [];
779
+ for (const part of msg.content) {
780
+ if (part.text) {
781
+ parts.push(this.truncateContent(part.text));
782
+ }
783
+ }
784
+ return parts.join("\n");
785
+ }
786
+ /**
787
+ * Truncate very long content for summary input.
788
+ */
789
+ truncateContent(content, maxChars = 2e3) {
790
+ if (content.length <= maxChars) return content;
791
+ return content.slice(0, maxChars) + `
792
+ [...truncated ${content.length - maxChars} chars]`;
793
+ }
794
+ /**
795
+ * Estimate the cost of summarization.
796
+ *
797
+ * @param messages - Messages that would be summarized
798
+ * @returns Estimated cost in USD
799
+ */
800
+ estimateCost(messages) {
801
+ if (messages.length <= this.config.keepFirst + this.config.keepLast) {
802
+ return 0;
803
+ }
804
+ const middleMessages = messages.slice(this.config.keepFirst, -this.config.keepLast);
805
+ const inputTokens = middleMessages.reduce((sum, msg) => {
806
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
807
+ return sum + estimateTokens(content);
808
+ }, 0);
809
+ const inputCost = inputTokens / 1e6 * 0.25;
810
+ const outputCost = this.config.maxSummaryTokens / 1e6 * 1.25;
811
+ return inputCost + outputCost;
812
+ }
813
+ /**
814
+ * Get statistics about potential summarization.
815
+ */
816
+ getStats(messages) {
817
+ const wouldKeep = Math.min(messages.length, this.config.keepFirst + this.config.keepLast);
818
+ const wouldSummarize = Math.max(0, messages.length - wouldKeep);
819
+ return {
820
+ totalMessages: messages.length,
821
+ wouldKeep,
822
+ wouldSummarize,
823
+ estimatedCost: this.estimateCost(messages)
824
+ };
825
+ }
826
+ };
827
+
828
+ // src/lib/condenser/index.ts
829
+ var DEFAULT_CONFIG3 = {
830
+ enabled: true,
831
+ lightThreshold: 0.7,
832
+ mediumThreshold: 0.85,
833
+ heavyThreshold: 0.95,
834
+ modelLimit: 2e5,
835
+ model: "claude-sonnet-4-20250514",
836
+ pruning: {},
837
+ summarization: {}
838
+ };
839
+ var ContextCondenser = class {
840
+ config;
841
+ tracker;
842
+ deduplicator;
843
+ pruner;
844
+ summarizer;
845
+ /** Metrics for tracking */
846
+ metrics = {
847
+ condensationCount: 0,
848
+ tokensRecovered: 0,
849
+ lastLevel: "none"
850
+ };
851
+ constructor(config = {}) {
852
+ this.config = { ...DEFAULT_CONFIG3, ...config };
853
+ this.tracker = createTracker(this.config.model);
854
+ this.tracker.limit = this.config.modelLimit;
855
+ this.deduplicator = new FileDeduplicator();
856
+ this.pruner = new TokenPruner(this.config.pruning);
857
+ this.summarizer = new ConversationSummarizer(this.config.summarization);
858
+ }
859
+ /**
860
+ * Main entry point - condense messages if needed.
861
+ *
862
+ * @param messages - Current conversation messages
863
+ * @returns Condensed messages and metadata
864
+ */
865
+ async condense(messages) {
866
+ const startTime = Date.now();
867
+ if (!this.config.enabled) {
868
+ return this.createResult(messages, messages, "none", startTime);
869
+ }
870
+ this.updateTrackerFromMessages(messages);
871
+ const level = getCompressionLevel(this.tracker, {
872
+ light: this.config.lightThreshold,
873
+ medium: this.config.mediumThreshold,
874
+ heavy: this.config.heavyThreshold
875
+ });
876
+ if (level === "none") {
877
+ return this.createResult(messages, messages, level, startTime);
878
+ }
879
+ let condensed;
880
+ switch (level) {
881
+ case "light":
882
+ condensed = this.applyDeduplication(messages);
883
+ break;
884
+ case "medium":
885
+ condensed = this.applyPruning(messages);
886
+ break;
887
+ case "heavy":
888
+ condensed = await this.applySummarization(messages);
889
+ break;
890
+ }
891
+ this.metrics.condensationCount++;
892
+ this.metrics.lastLevel = level;
893
+ return this.createResult(messages, condensed, level, startTime);
894
+ }
895
+ /**
896
+ * Apply light compression (deduplication).
897
+ */
898
+ applyDeduplication(messages) {
899
+ return messages;
900
+ }
901
+ /**
902
+ * Apply medium compression (pruning).
903
+ */
904
+ applyPruning(messages) {
905
+ return this.pruner.pruneMessages(messages);
906
+ }
907
+ /**
908
+ * Apply heavy compression (summarization).
909
+ */
910
+ async applySummarization(messages) {
911
+ const pruned = this.applyPruning(messages);
912
+ const summarized = await this.summarizer.summarize(pruned);
913
+ return summarized;
914
+ }
915
+ /**
916
+ * Update tracker from messages.
917
+ */
918
+ updateTrackerFromMessages(messages) {
919
+ this.tracker = createTracker(this.config.model);
920
+ this.tracker.limit = this.config.modelLimit;
921
+ for (const msg of messages) {
922
+ updateTrackerFromMessage(this.tracker, msg);
923
+ }
924
+ }
925
+ /**
926
+ * Create result object.
927
+ */
928
+ createResult(before, after, level, startTime) {
929
+ const tokensBefore = this.estimateTokens(before);
930
+ const tokensAfter = this.estimateTokens(after);
931
+ const savings = tokensBefore > 0 ? (tokensBefore - tokensAfter) / tokensBefore * 100 : 0;
932
+ if (level !== "none") {
933
+ this.metrics.tokensRecovered += tokensBefore - tokensAfter;
934
+ }
935
+ return {
936
+ messages: after,
937
+ level,
938
+ tokensBefore,
939
+ tokensAfter,
940
+ savingsPercentage: savings,
941
+ durationMs: Date.now() - startTime
942
+ };
943
+ }
944
+ /**
945
+ * Estimate tokens for messages.
946
+ */
947
+ estimateTokens(messages) {
948
+ const tempTracker = createTracker(this.config.model);
949
+ for (const msg of messages) {
950
+ updateTrackerFromMessage(tempTracker, msg);
951
+ }
952
+ return tempTracker.used;
953
+ }
954
+ /**
955
+ * Get current tracker status.
956
+ */
957
+ getStatus() {
958
+ return formatTrackerStatus(this.tracker);
959
+ }
960
+ /**
961
+ * Get tracker for external monitoring.
962
+ */
963
+ getTracker() {
964
+ return { ...this.tracker };
965
+ }
966
+ /**
967
+ * Get metrics.
968
+ */
969
+ getMetrics() {
970
+ return { ...this.metrics };
971
+ }
972
+ /**
973
+ * Check if compression is needed.
974
+ */
975
+ needsCompression() {
976
+ return getCompressionLevel(this.tracker, {
977
+ light: this.config.lightThreshold,
978
+ medium: this.config.mediumThreshold,
979
+ heavy: this.config.heavyThreshold
980
+ });
981
+ }
982
+ /**
983
+ * Reset condenser state.
984
+ */
985
+ reset() {
986
+ this.tracker = createTracker(this.config.model);
987
+ this.tracker.limit = this.config.modelLimit;
988
+ this.deduplicator.reset();
989
+ this.metrics = {
990
+ condensationCount: 0,
991
+ tokensRecovered: 0,
992
+ lastLevel: "none"
993
+ };
994
+ }
995
+ /**
996
+ * Get file deduplicator for integration with tool layer.
997
+ */
998
+ getDeduplicator() {
999
+ return this.deduplicator;
1000
+ }
1001
+ };
1002
+ function createCondenser(squadConfig) {
1003
+ const condenserConfig = squadConfig?.condenser || {};
1004
+ const modelConfig = squadConfig?.model || {};
1005
+ return new ContextCondenser({
1006
+ enabled: condenserConfig.enabled ?? true,
1007
+ lightThreshold: condenserConfig.light_threshold ?? 0.7,
1008
+ mediumThreshold: condenserConfig.medium_threshold ?? 0.85,
1009
+ heavyThreshold: condenserConfig.heavy_threshold ?? 0.95,
1010
+ model: modelConfig.default ?? "claude-sonnet-4-20250514",
1011
+ pruning: {
1012
+ protectRecent: condenserConfig.protect_recent ?? 4e4
1013
+ }
1014
+ });
1015
+ }
6
1016
  export {
1017
+ ContextCondenser,
1018
+ ConversationSummarizer,
1019
+ FileDeduplicator,
1020
+ TokenPruner,
1021
+ addGoalToSquad,
1022
+ createCondenser,
1023
+ createTracker,
1024
+ estimateMessageTokens,
1025
+ estimateTokens,
1026
+ findProjectRoot,
1027
+ findSquadsDir,
1028
+ formatTrackerStatus,
1029
+ getCompressionLevel,
1030
+ listAgents,
1031
+ listSquads,
1032
+ loadAgentDefinition,
1033
+ loadSquad,
1034
+ parseSquadFile,
1035
+ updateGoalInSquad,
1036
+ updateTracker,
7
1037
  version
8
1038
  };
9
1039
  //# sourceMappingURL=index.js.map