@teammates/cli 0.5.3 → 0.6.1
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/adapter.d.ts +7 -1
- package/dist/adapter.js +29 -4
- package/dist/adapter.test.js +10 -13
- package/dist/adapters/cli-proxy.d.ts +3 -0
- package/dist/adapters/cli-proxy.js +133 -108
- package/dist/cli-utils.d.ts +6 -0
- package/dist/cli-utils.js +17 -0
- package/dist/cli-utils.test.js +50 -1
- package/dist/cli.js +626 -20
- package/dist/compact.d.ts +29 -0
- package/dist/compact.js +131 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/log-parser.d.ts +53 -0
- package/dist/log-parser.js +228 -0
- package/dist/log-parser.test.d.ts +1 -0
- package/dist/log-parser.test.js +113 -0
- package/dist/orchestrator.d.ts +2 -0
- package/dist/orchestrator.js +4 -0
- package/dist/registry.js +4 -4
- package/dist/types.d.ts +30 -0
- package/package.json +3 -3
package/dist/compact.d.ts
CHANGED
|
@@ -67,3 +67,32 @@ export declare function buildWisdomPrompt(teammateDir: string, teammateName: str
|
|
|
67
67
|
* Returns the list of deleted filenames.
|
|
68
68
|
*/
|
|
69
69
|
export declare function purgeStaleDailies(teammateDir: string): Promise<string[]>;
|
|
70
|
+
/**
|
|
71
|
+
* Find all daily logs that are not yet compressed (no `compressed: true`
|
|
72
|
+
* frontmatter). Returns an array of { date, file } for each uncompressed log.
|
|
73
|
+
*/
|
|
74
|
+
export declare function findUncompressedDailies(teammateDir: string): Promise<{
|
|
75
|
+
date: string;
|
|
76
|
+
file: string;
|
|
77
|
+
}[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Build a prompt for an agent to compress multiple daily logs in bulk.
|
|
80
|
+
* Used during version migrations to compress all historical daily logs.
|
|
81
|
+
* Returns null if there are no uncompressed dailies.
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildMigrationCompressionPrompt(_teammateDir: string, teammateName: string, dailies: {
|
|
84
|
+
date: string;
|
|
85
|
+
file: string;
|
|
86
|
+
}[]): Promise<string | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Check if the previous day's log needs compression and return a prompt
|
|
89
|
+
* to compress it. Returns null if no compression is needed.
|
|
90
|
+
*
|
|
91
|
+
* A daily log is eligible for compression when:
|
|
92
|
+
* - Today's log does not yet exist (new day boundary)
|
|
93
|
+
* - Yesterday's log exists and is not already compressed (no `compressed: true` frontmatter)
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildDailyCompressionPrompt(teammateDir: string): Promise<{
|
|
96
|
+
date: string;
|
|
97
|
+
prompt: string;
|
|
98
|
+
} | null>;
|
package/dist/compact.js
CHANGED
|
@@ -84,6 +84,7 @@ function buildWeeklySummary(weekKey, dailies, partial = false) {
|
|
|
84
84
|
const lastDate = sorted[sorted.length - 1].date;
|
|
85
85
|
const lines = [];
|
|
86
86
|
lines.push("---");
|
|
87
|
+
lines.push("version: 0.6.0");
|
|
87
88
|
lines.push(`type: weekly`);
|
|
88
89
|
lines.push(`week: ${weekKey}`);
|
|
89
90
|
lines.push(`period: ${firstDate} to ${lastDate}`);
|
|
@@ -110,6 +111,7 @@ function buildMonthlySummary(monthKey, weeklies) {
|
|
|
110
111
|
const lastWeek = sorted[sorted.length - 1].week;
|
|
111
112
|
const lines = [];
|
|
112
113
|
lines.push("---");
|
|
114
|
+
lines.push("version: 0.6.0");
|
|
113
115
|
lines.push(`type: monthly`);
|
|
114
116
|
lines.push(`month: ${monthKey}`);
|
|
115
117
|
lines.push(`period: ${firstWeek} to ${lastWeek}`);
|
|
@@ -534,3 +536,132 @@ export async function purgeStaleDailies(teammateDir) {
|
|
|
534
536
|
}
|
|
535
537
|
return purged;
|
|
536
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* Find all daily logs that are not yet compressed (no `compressed: true`
|
|
541
|
+
* frontmatter). Returns an array of { date, file } for each uncompressed log.
|
|
542
|
+
*/
|
|
543
|
+
export async function findUncompressedDailies(teammateDir) {
|
|
544
|
+
const memoryDir = join(teammateDir, "memory");
|
|
545
|
+
const entries = await readdir(memoryDir).catch(() => []);
|
|
546
|
+
const results = [];
|
|
547
|
+
for (const entry of entries) {
|
|
548
|
+
if (!entry.endsWith(".md"))
|
|
549
|
+
continue;
|
|
550
|
+
const stem = basename(entry, ".md");
|
|
551
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(stem))
|
|
552
|
+
continue;
|
|
553
|
+
const content = await readFile(join(memoryDir, entry), "utf-8");
|
|
554
|
+
if (content.startsWith("---") && /compressed:\s*true/.test(content)) {
|
|
555
|
+
continue; // Already compressed
|
|
556
|
+
}
|
|
557
|
+
results.push({ date: stem, file: entry });
|
|
558
|
+
}
|
|
559
|
+
return results.sort((a, b) => a.date.localeCompare(b.date));
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Build a prompt for an agent to compress multiple daily logs in bulk.
|
|
563
|
+
* Used during version migrations to compress all historical daily logs.
|
|
564
|
+
* Returns null if there are no uncompressed dailies.
|
|
565
|
+
*/
|
|
566
|
+
export async function buildMigrationCompressionPrompt(_teammateDir, teammateName, dailies) {
|
|
567
|
+
if (dailies.length === 0)
|
|
568
|
+
return null;
|
|
569
|
+
const filePaths = dailies
|
|
570
|
+
.map((d) => `.teammates/${teammateName}/memory/${d.file}`)
|
|
571
|
+
.join("\n- ");
|
|
572
|
+
return `You are compressing daily work logs to save context window space. There are ${dailies.length} uncompressed daily logs that need compression.
|
|
573
|
+
|
|
574
|
+
## Rules
|
|
575
|
+
|
|
576
|
+
For EACH file listed below:
|
|
577
|
+
1. Read the file
|
|
578
|
+
2. Rewrite it into a shorter version that preserves:
|
|
579
|
+
- Task names and one-line summaries of what was done
|
|
580
|
+
- Key decisions and their rationale
|
|
581
|
+
- Files changed (as a flat list per task, not grouped subsections)
|
|
582
|
+
- Important context for future tasks
|
|
583
|
+
3. Remove:
|
|
584
|
+
- Detailed "What was done" step-by-step breakdowns
|
|
585
|
+
- Build/test status lines (unless something failed)
|
|
586
|
+
- Redundant section headers
|
|
587
|
+
4. Keep the same markdown structure (# date header, ## Task headers) but make each task entry 3-5 lines max
|
|
588
|
+
5. Start the file with this frontmatter:
|
|
589
|
+
\`\`\`
|
|
590
|
+
---
|
|
591
|
+
version: 0.6.0
|
|
592
|
+
type: daily
|
|
593
|
+
compressed: true
|
|
594
|
+
---
|
|
595
|
+
\`\`\`
|
|
596
|
+
|
|
597
|
+
## Files to Compress
|
|
598
|
+
|
|
599
|
+
- ${filePaths}
|
|
600
|
+
|
|
601
|
+
Process each file one at a time. Read it, compress it, write it back. Do NOT skip any files.`;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Check if the previous day's log needs compression and return a prompt
|
|
605
|
+
* to compress it. Returns null if no compression is needed.
|
|
606
|
+
*
|
|
607
|
+
* A daily log is eligible for compression when:
|
|
608
|
+
* - Today's log does not yet exist (new day boundary)
|
|
609
|
+
* - Yesterday's log exists and is not already compressed (no `compressed: true` frontmatter)
|
|
610
|
+
*/
|
|
611
|
+
export async function buildDailyCompressionPrompt(teammateDir) {
|
|
612
|
+
const memoryDir = join(teammateDir, "memory");
|
|
613
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
614
|
+
// Find yesterday's date
|
|
615
|
+
const yesterday = new Date();
|
|
616
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
617
|
+
const yesterdayStr = yesterday.toISOString().slice(0, 10);
|
|
618
|
+
// Check if yesterday's log exists
|
|
619
|
+
const yesterdayFile = join(memoryDir, `${yesterdayStr}.md`);
|
|
620
|
+
let content;
|
|
621
|
+
try {
|
|
622
|
+
content = await readFile(yesterdayFile, "utf-8");
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
return null; // No yesterday log
|
|
626
|
+
}
|
|
627
|
+
// Skip if already compressed
|
|
628
|
+
if (content.startsWith("---") && /compressed:\s*true/.test(content)) {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
// Skip if today's log already exists (we already passed the day boundary)
|
|
632
|
+
const todayFile = join(memoryDir, `${today}.md`);
|
|
633
|
+
try {
|
|
634
|
+
await readFile(todayFile, "utf-8");
|
|
635
|
+
// Today's log exists — this isn't a fresh day boundary, skip
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// Today's log doesn't exist — this is a new day, compress yesterday
|
|
640
|
+
}
|
|
641
|
+
const prompt = `You are compressing a daily work log to save context window space. Rewrite the log below into a shorter version that preserves:
|
|
642
|
+
- Task names and one-line summaries of what was done
|
|
643
|
+
- Key decisions and their rationale
|
|
644
|
+
- Files changed (as a flat list per task, not grouped subsections)
|
|
645
|
+
- Important context for future tasks
|
|
646
|
+
|
|
647
|
+
Remove:
|
|
648
|
+
- Detailed "What was done" step-by-step breakdowns
|
|
649
|
+
- Build/test status lines (unless something failed)
|
|
650
|
+
- Redundant section headers
|
|
651
|
+
|
|
652
|
+
Keep the same markdown structure (# date header, ## Task headers) but make each task entry 3-5 lines max.
|
|
653
|
+
|
|
654
|
+
Write the compressed version to \`.teammates/${basename(teammateDir)}/memory/${yesterdayStr}.md\`. Start the file with this frontmatter:
|
|
655
|
+
\`\`\`
|
|
656
|
+
---
|
|
657
|
+
version: 0.6.0
|
|
658
|
+
type: daily
|
|
659
|
+
compressed: true
|
|
660
|
+
---
|
|
661
|
+
\`\`\`
|
|
662
|
+
|
|
663
|
+
## Original Log
|
|
664
|
+
|
|
665
|
+
${content}`;
|
|
666
|
+
return { date: yesterdayStr, prompt };
|
|
667
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
export type { AgentAdapter, InstalledService, RecallContext, RosterEntry, } from "./adapter.js";
|
|
2
2
|
export { buildTeammatePrompt, DAILY_LOG_BUDGET_TOKENS, formatHandoffContext, queryRecallContext, syncRecallIndex, } from "./adapter.js";
|
|
3
|
-
export { autoCompactForBudget } from "./compact.js";
|
|
4
3
|
export { type AgentPreset, CliProxyAdapter, type CliProxyOptions, PRESETS, } from "./adapters/cli-proxy.js";
|
|
5
4
|
export { EchoAdapter } from "./adapters/echo.js";
|
|
6
5
|
export type { BannerInfo, ServiceInfo, ServiceStatus } from "./banner.js";
|
|
7
6
|
export { AnimatedBanner } from "./banner.js";
|
|
8
7
|
export type { CliArgs } from "./cli-args.js";
|
|
9
8
|
export { findTeammatesDir, PKG_VERSION, parseCliArgs } from "./cli-args.js";
|
|
9
|
+
export { autoCompactForBudget, buildDailyCompressionPrompt, buildMigrationCompressionPrompt, findUncompressedDailies, } from "./compact.js";
|
|
10
|
+
export type { LogEntry } from "./log-parser.js";
|
|
11
|
+
export { buildConversationLog, formatLogTimeline, parseClaudeDebugLog, parseCodexOutput, parseRawOutput, } from "./log-parser.js";
|
|
10
12
|
export { Orchestrator, type OrchestratorConfig, type TeammateStatus, } from "./orchestrator.js";
|
|
11
13
|
export type { Persona } from "./personas.js";
|
|
12
14
|
export { loadPersonas, scaffoldFromPersona } from "./personas.js";
|
|
13
15
|
export { Registry } from "./registry.js";
|
|
14
16
|
export { tp } from "./theme.js";
|
|
15
|
-
export type { DailyLog, HandoffEnvelope, OrchestratorEvent, OwnershipRules, PresenceState, QueueEntry, SandboxLevel, SlashCommand, TaskAssignment, TaskResult, TeammateConfig, TeammateType, } from "./types.js";
|
|
17
|
+
export type { DailyLog, HandoffEnvelope, InterruptState, OrchestratorEvent, OwnershipRules, PresenceState, QueueEntry, SandboxLevel, SlashCommand, TaskAssignment, TaskResult, TeammateConfig, TeammateType, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Public API for @teammates/cli
|
|
2
2
|
export { buildTeammatePrompt, DAILY_LOG_BUDGET_TOKENS, formatHandoffContext, queryRecallContext, syncRecallIndex, } from "./adapter.js";
|
|
3
|
-
export { autoCompactForBudget } from "./compact.js";
|
|
4
3
|
export { CliProxyAdapter, PRESETS, } from "./adapters/cli-proxy.js";
|
|
5
4
|
export { EchoAdapter } from "./adapters/echo.js";
|
|
6
5
|
export { AnimatedBanner } from "./banner.js";
|
|
7
6
|
export { findTeammatesDir, PKG_VERSION, parseCliArgs } from "./cli-args.js";
|
|
7
|
+
export { autoCompactForBudget, buildDailyCompressionPrompt, buildMigrationCompressionPrompt, findUncompressedDailies, } from "./compact.js";
|
|
8
|
+
export { buildConversationLog, formatLogTimeline, parseClaudeDebugLog, parseCodexOutput, parseRawOutput, } from "./log-parser.js";
|
|
8
9
|
export { Orchestrator, } from "./orchestrator.js";
|
|
9
10
|
export { loadPersonas, scaffoldFromPersona } from "./personas.js";
|
|
10
11
|
export { Registry } from "./registry.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent log parser — extracts condensed conversation timelines from agent
|
|
3
|
+
* debug logs and stdout for the interrupt-and-resume system.
|
|
4
|
+
*
|
|
5
|
+
* Each agent type has a different log format:
|
|
6
|
+
* - Claude: structured --debug-file with tool calls and results
|
|
7
|
+
* - Codex: JSONL with item.completed events
|
|
8
|
+
* - Others: raw stdout (truncated)
|
|
9
|
+
*/
|
|
10
|
+
/** A single action extracted from an agent's conversation log. */
|
|
11
|
+
export interface LogEntry {
|
|
12
|
+
/** Tool name or action type (e.g. "Write", "Read", "Search", "text") */
|
|
13
|
+
action: string;
|
|
14
|
+
/** Key parameters — file paths, search queries (NOT full file contents) */
|
|
15
|
+
summary: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse a Claude --debug-file into condensed log entries.
|
|
19
|
+
*
|
|
20
|
+
* The debug file contains the full conversation log including tool calls
|
|
21
|
+
* and their results. We extract tool names and key parameters (file paths,
|
|
22
|
+
* search queries) but NOT full file contents to keep the resume prompt compact.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseClaudeDebugLog(debugFilePath: string): LogEntry[];
|
|
25
|
+
/**
|
|
26
|
+
* Parse Codex JSONL output into condensed log entries.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseCodexOutput(stdout: string): LogEntry[];
|
|
29
|
+
/**
|
|
30
|
+
* Parse raw stdout into condensed log entries (fallback for unknown agents).
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseRawOutput(output: string): LogEntry[];
|
|
33
|
+
/**
|
|
34
|
+
* Format log entries into a condensed markdown timeline for the resume prompt.
|
|
35
|
+
*
|
|
36
|
+
* Keeps the output compact — file paths and search queries only, no content.
|
|
37
|
+
* Groups consecutive same-action entries (e.g., "Wrote 15 files: ...").
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatLogTimeline(entries: LogEntry[]): string;
|
|
40
|
+
/**
|
|
41
|
+
* Estimate the token count of a string (rough: 1 token ≈ 4 chars).
|
|
42
|
+
*/
|
|
43
|
+
export declare function estimateTokens(text: string): number;
|
|
44
|
+
/**
|
|
45
|
+
* Build a condensed conversation log from the available sources.
|
|
46
|
+
* Tries Claude debug file first, then Codex JSONL, then raw stdout.
|
|
47
|
+
* Truncates to the token budget if needed.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildConversationLog(debugFile: string | undefined, stdout: string, presetName: string, tokenBudget?: number): {
|
|
50
|
+
log: string;
|
|
51
|
+
toolCallCount: number;
|
|
52
|
+
filesChanged: string[];
|
|
53
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent log parser — extracts condensed conversation timelines from agent
|
|
3
|
+
* debug logs and stdout for the interrupt-and-resume system.
|
|
4
|
+
*
|
|
5
|
+
* Each agent type has a different log format:
|
|
6
|
+
* - Claude: structured --debug-file with tool calls and results
|
|
7
|
+
* - Codex: JSONL with item.completed events
|
|
8
|
+
* - Others: raw stdout (truncated)
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
/**
|
|
12
|
+
* Parse a Claude --debug-file into condensed log entries.
|
|
13
|
+
*
|
|
14
|
+
* The debug file contains the full conversation log including tool calls
|
|
15
|
+
* and their results. We extract tool names and key parameters (file paths,
|
|
16
|
+
* search queries) but NOT full file contents to keep the resume prompt compact.
|
|
17
|
+
*/
|
|
18
|
+
export function parseClaudeDebugLog(debugFilePath) {
|
|
19
|
+
let content;
|
|
20
|
+
try {
|
|
21
|
+
content = readFileSync(debugFilePath, "utf-8");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const entries = [];
|
|
27
|
+
// Claude debug logs contain tool_use and tool_result blocks
|
|
28
|
+
// Extract tool calls with their key parameters
|
|
29
|
+
const toolUsePattern = /Tool call:\s*(\w+)\s*\{([^}]*)\}|"type":\s*"tool_use".*?"name":\s*"(\w+)".*?"input":\s*\{([^}]*)\}/g;
|
|
30
|
+
let match;
|
|
31
|
+
while ((match = toolUsePattern.exec(content)) !== null) {
|
|
32
|
+
const toolName = match[1] || match[3];
|
|
33
|
+
const params = match[2] || match[4];
|
|
34
|
+
if (!toolName)
|
|
35
|
+
continue;
|
|
36
|
+
const summary = extractKeyParams(toolName, params);
|
|
37
|
+
entries.push({ action: toolName, summary });
|
|
38
|
+
}
|
|
39
|
+
// If structured parsing found nothing, try line-by-line patterns
|
|
40
|
+
if (entries.length === 0) {
|
|
41
|
+
for (const line of content.split("\n")) {
|
|
42
|
+
// Look for common Claude debug log patterns
|
|
43
|
+
const writeMatch = line.match(/(?:Write|Edit|Create)\s+(?:file:?\s*)?[`"]?([^\s`"]+\.\w+)/i);
|
|
44
|
+
if (writeMatch) {
|
|
45
|
+
entries.push({ action: "Write", summary: writeMatch[1] });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const readMatch = line.match(/(?:Read)\s+(?:file:?\s*)?[`"]?([^\s`"]+\.\w+)/i);
|
|
49
|
+
if (readMatch) {
|
|
50
|
+
entries.push({ action: "Read", summary: readMatch[1] });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const searchMatch = line.match(/(?:Search|Grep|Glob)\s+.*?["']([^"']+)["']/i);
|
|
54
|
+
if (searchMatch) {
|
|
55
|
+
entries.push({ action: "Search", summary: searchMatch[1] });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const bashMatch = line.match(/(?:Bash|Shell|Execute)\s+.*?["'`]([^"'`]+)["'`]/i);
|
|
59
|
+
if (bashMatch) {
|
|
60
|
+
entries.push({
|
|
61
|
+
action: "Bash",
|
|
62
|
+
summary: bashMatch[1].slice(0, 80),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return entries;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Parse Codex JSONL output into condensed log entries.
|
|
71
|
+
*/
|
|
72
|
+
export function parseCodexOutput(stdout) {
|
|
73
|
+
const entries = [];
|
|
74
|
+
for (const line of stdout.split("\n")) {
|
|
75
|
+
if (!line.trim())
|
|
76
|
+
continue;
|
|
77
|
+
try {
|
|
78
|
+
const event = JSON.parse(line);
|
|
79
|
+
if (event.type === "item.completed") {
|
|
80
|
+
if (event.item?.type === "tool_call") {
|
|
81
|
+
const toolName = event.item.name ?? "tool";
|
|
82
|
+
const summary = event.item.arguments
|
|
83
|
+
? extractKeyParams(toolName, JSON.stringify(event.item.arguments))
|
|
84
|
+
: "";
|
|
85
|
+
entries.push({ action: toolName, summary });
|
|
86
|
+
}
|
|
87
|
+
else if (event.item?.type === "agent_message" && event.item.text) {
|
|
88
|
+
entries.push({
|
|
89
|
+
action: "text",
|
|
90
|
+
summary: event.item.text.slice(0, 100),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* skip non-JSON lines */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return entries;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parse raw stdout into condensed log entries (fallback for unknown agents).
|
|
103
|
+
*/
|
|
104
|
+
export function parseRawOutput(output) {
|
|
105
|
+
const entries = [];
|
|
106
|
+
for (const line of output.split("\n")) {
|
|
107
|
+
const writeMatch = line.match(/(?:Created|Modified|Updated|Wrote|Edited)\s+(?:file:?\s*)?[`"]?([^\s`"]+\.\w+)/i);
|
|
108
|
+
if (writeMatch) {
|
|
109
|
+
entries.push({ action: "Write", summary: writeMatch[1] });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return entries;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Format log entries into a condensed markdown timeline for the resume prompt.
|
|
116
|
+
*
|
|
117
|
+
* Keeps the output compact — file paths and search queries only, no content.
|
|
118
|
+
* Groups consecutive same-action entries (e.g., "Wrote 15 files: ...").
|
|
119
|
+
*/
|
|
120
|
+
export function formatLogTimeline(entries) {
|
|
121
|
+
if (entries.length === 0)
|
|
122
|
+
return "(no tool calls captured)";
|
|
123
|
+
const lines = [];
|
|
124
|
+
let i = 0;
|
|
125
|
+
while (i < entries.length) {
|
|
126
|
+
const entry = entries[i];
|
|
127
|
+
// Group consecutive same-action entries
|
|
128
|
+
let groupEnd = i + 1;
|
|
129
|
+
while (groupEnd < entries.length &&
|
|
130
|
+
entries[groupEnd].action === entry.action) {
|
|
131
|
+
groupEnd++;
|
|
132
|
+
}
|
|
133
|
+
const groupSize = groupEnd - i;
|
|
134
|
+
if (groupSize > 3 && entry.action !== "text") {
|
|
135
|
+
// Collapse large groups
|
|
136
|
+
const summaries = entries
|
|
137
|
+
.slice(i, groupEnd)
|
|
138
|
+
.map((e) => e.summary)
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
const preview = summaries.slice(0, 3).join(", ");
|
|
141
|
+
const more = summaries.length > 3 ? ` (+${summaries.length - 3} more)` : "";
|
|
142
|
+
lines.push(`${lines.length + 1}. ${entry.action} ${groupSize} items: ${preview}${more}`);
|
|
143
|
+
i = groupEnd;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Individual entries
|
|
147
|
+
for (let j = i; j < groupEnd; j++) {
|
|
148
|
+
const e = entries[j];
|
|
149
|
+
const desc = e.summary ? ` ${e.summary}` : "";
|
|
150
|
+
lines.push(`${lines.length + 1}. ${e.action}${desc}`);
|
|
151
|
+
}
|
|
152
|
+
i = groupEnd;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Extract key parameters from a tool call — file paths and search queries,
|
|
159
|
+
* NOT full file contents. Keeps the resume prompt compact.
|
|
160
|
+
*/
|
|
161
|
+
function extractKeyParams(toolName, paramsStr) {
|
|
162
|
+
const lower = toolName.toLowerCase();
|
|
163
|
+
// Extract file_path parameter
|
|
164
|
+
const filePathMatch = paramsStr.match(/file_path["']?\s*[:=]\s*["']([^"']+)["']/);
|
|
165
|
+
if (filePathMatch)
|
|
166
|
+
return filePathMatch[1];
|
|
167
|
+
// Extract path parameter
|
|
168
|
+
const pathMatch = paramsStr.match(/["']?path["']?\s*[:=]\s*["']([^"']+)["']/);
|
|
169
|
+
if (pathMatch)
|
|
170
|
+
return pathMatch[1];
|
|
171
|
+
// Extract command for bash/shell tools
|
|
172
|
+
if (lower === "bash" || lower === "shell" || lower === "execute") {
|
|
173
|
+
const cmdMatch = paramsStr.match(/["']?command["']?\s*[:=]\s*["']([^"']+)["']/);
|
|
174
|
+
if (cmdMatch)
|
|
175
|
+
return cmdMatch[1].slice(0, 80);
|
|
176
|
+
}
|
|
177
|
+
// Extract query/pattern for search tools
|
|
178
|
+
if (lower === "search" || lower === "grep" || lower === "glob") {
|
|
179
|
+
const queryMatch = paramsStr.match(/["']?(?:query|pattern)["']?\s*[:=]\s*["']([^"']+)["']/);
|
|
180
|
+
if (queryMatch)
|
|
181
|
+
return queryMatch[1];
|
|
182
|
+
}
|
|
183
|
+
return "";
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Estimate the token count of a string (rough: 1 token ≈ 4 chars).
|
|
187
|
+
*/
|
|
188
|
+
export function estimateTokens(text) {
|
|
189
|
+
return Math.ceil(text.length / 4);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Build a condensed conversation log from the available sources.
|
|
193
|
+
* Tries Claude debug file first, then Codex JSONL, then raw stdout.
|
|
194
|
+
* Truncates to the token budget if needed.
|
|
195
|
+
*/
|
|
196
|
+
export function buildConversationLog(debugFile, stdout, presetName, tokenBudget = 8_000) {
|
|
197
|
+
let entries;
|
|
198
|
+
if (debugFile && presetName === "claude") {
|
|
199
|
+
entries = parseClaudeDebugLog(debugFile);
|
|
200
|
+
}
|
|
201
|
+
else if (presetName === "codex") {
|
|
202
|
+
entries = parseCodexOutput(stdout);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
entries = parseRawOutput(stdout);
|
|
206
|
+
}
|
|
207
|
+
// Extract files changed
|
|
208
|
+
const filesChanged = entries
|
|
209
|
+
.filter((e) => ["Write", "Edit", "Create", "write", "edit", "create"].includes(e.action))
|
|
210
|
+
.map((e) => e.summary)
|
|
211
|
+
.filter(Boolean);
|
|
212
|
+
const toolCallCount = entries.filter((e) => e.action !== "text").length;
|
|
213
|
+
let log = formatLogTimeline(entries);
|
|
214
|
+
// Truncate if over budget
|
|
215
|
+
if (estimateTokens(log) > tokenBudget) {
|
|
216
|
+
// Keep first and last entries, summarize the middle
|
|
217
|
+
const lines = log.split("\n");
|
|
218
|
+
const keepFirst = Math.floor(lines.length * 0.3);
|
|
219
|
+
const keepLast = Math.floor(lines.length * 0.2);
|
|
220
|
+
const omitted = lines.length - keepFirst - keepLast;
|
|
221
|
+
log = [
|
|
222
|
+
...lines.slice(0, keepFirst),
|
|
223
|
+
`... (${omitted} steps omitted for brevity) ...`,
|
|
224
|
+
...lines.slice(lines.length - keepLast),
|
|
225
|
+
].join("\n");
|
|
226
|
+
}
|
|
227
|
+
return { log, toolCallCount, filesChanged };
|
|
228
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildConversationLog, formatLogTimeline, parseCodexOutput, parseRawOutput, } from "./log-parser.js";
|
|
3
|
+
describe("parseRawOutput", () => {
|
|
4
|
+
it("extracts file write patterns from stdout", () => {
|
|
5
|
+
const output = [
|
|
6
|
+
"Created file: `src/foo.ts`",
|
|
7
|
+
"Modified bar.js",
|
|
8
|
+
"Some other output",
|
|
9
|
+
"Wrote `packages/cli/src/index.ts`",
|
|
10
|
+
].join("\n");
|
|
11
|
+
const entries = parseRawOutput(output);
|
|
12
|
+
expect(entries).toHaveLength(3);
|
|
13
|
+
expect(entries[0]).toEqual({ action: "Write", summary: "src/foo.ts" });
|
|
14
|
+
expect(entries[1]).toEqual({ action: "Write", summary: "bar.js" });
|
|
15
|
+
expect(entries[2]).toEqual({
|
|
16
|
+
action: "Write",
|
|
17
|
+
summary: "packages/cli/src/index.ts",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it("returns empty array for output with no file patterns", () => {
|
|
21
|
+
const entries = parseRawOutput("Just some plain text output");
|
|
22
|
+
expect(entries).toHaveLength(0);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe("parseCodexOutput", () => {
|
|
26
|
+
it("extracts tool calls from JSONL events", () => {
|
|
27
|
+
const output = [
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
type: "item.completed",
|
|
30
|
+
item: {
|
|
31
|
+
type: "tool_call",
|
|
32
|
+
name: "write_file",
|
|
33
|
+
arguments: { path: "src/foo.ts" },
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
JSON.stringify({
|
|
37
|
+
type: "item.completed",
|
|
38
|
+
item: { type: "agent_message", text: "Done with the task" },
|
|
39
|
+
}),
|
|
40
|
+
].join("\n");
|
|
41
|
+
const entries = parseCodexOutput(output);
|
|
42
|
+
expect(entries).toHaveLength(2);
|
|
43
|
+
expect(entries[0].action).toBe("write_file");
|
|
44
|
+
expect(entries[1]).toEqual({
|
|
45
|
+
action: "text",
|
|
46
|
+
summary: "Done with the task",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it("skips non-JSON lines", () => {
|
|
50
|
+
const output = "not json\n{invalid\n";
|
|
51
|
+
const entries = parseCodexOutput(output);
|
|
52
|
+
expect(entries).toHaveLength(0);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("formatLogTimeline", () => {
|
|
56
|
+
it("formats individual entries with numbered steps", () => {
|
|
57
|
+
const entries = [
|
|
58
|
+
{ action: "Read", summary: "src/foo.ts" },
|
|
59
|
+
{ action: "Write", summary: "src/bar.ts" },
|
|
60
|
+
];
|
|
61
|
+
const result = formatLogTimeline(entries);
|
|
62
|
+
expect(result).toBe("1. Read src/foo.ts\n2. Write src/bar.ts");
|
|
63
|
+
});
|
|
64
|
+
it("groups consecutive same-action entries when > 3", () => {
|
|
65
|
+
const entries = [
|
|
66
|
+
{ action: "Write", summary: "a.ts" },
|
|
67
|
+
{ action: "Write", summary: "b.ts" },
|
|
68
|
+
{ action: "Write", summary: "c.ts" },
|
|
69
|
+
{ action: "Write", summary: "d.ts" },
|
|
70
|
+
{ action: "Write", summary: "e.ts" },
|
|
71
|
+
];
|
|
72
|
+
const result = formatLogTimeline(entries);
|
|
73
|
+
expect(result).toContain("Write 5 items");
|
|
74
|
+
expect(result).toContain("a.ts, b.ts, c.ts");
|
|
75
|
+
expect(result).toContain("(+2 more)");
|
|
76
|
+
});
|
|
77
|
+
it("does not group when 3 or fewer consecutive entries", () => {
|
|
78
|
+
const entries = [
|
|
79
|
+
{ action: "Write", summary: "a.ts" },
|
|
80
|
+
{ action: "Write", summary: "b.ts" },
|
|
81
|
+
{ action: "Write", summary: "c.ts" },
|
|
82
|
+
];
|
|
83
|
+
const result = formatLogTimeline(entries);
|
|
84
|
+
expect(result).toBe("1. Write a.ts\n2. Write b.ts\n3. Write c.ts");
|
|
85
|
+
});
|
|
86
|
+
it("returns fallback for empty entries", () => {
|
|
87
|
+
expect(formatLogTimeline([])).toBe("(no tool calls captured)");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe("buildConversationLog", () => {
|
|
91
|
+
it("uses raw output parser for unknown presets", () => {
|
|
92
|
+
const stdout = "Created file: `test.ts`\nDone";
|
|
93
|
+
const result = buildConversationLog(undefined, stdout, "aider");
|
|
94
|
+
expect(result.filesChanged).toContain("test.ts");
|
|
95
|
+
expect(result.toolCallCount).toBe(1);
|
|
96
|
+
expect(result.log).toContain("Write");
|
|
97
|
+
});
|
|
98
|
+
it("groups large batch writes into a compact summary", () => {
|
|
99
|
+
// 200 file writes should be grouped, not listed individually
|
|
100
|
+
const entries = Array.from({ length: 200 }, (_, i) => `Created file: \`file${i}.ts\``).join("\n");
|
|
101
|
+
const result = buildConversationLog(undefined, entries, "generic");
|
|
102
|
+
expect(result.toolCallCount).toBe(200);
|
|
103
|
+
expect(result.log).toContain("Write 200 items");
|
|
104
|
+
// The grouped output should be compact (single line)
|
|
105
|
+
expect(result.log.split("\n")).toHaveLength(1);
|
|
106
|
+
});
|
|
107
|
+
it("handles empty output gracefully", () => {
|
|
108
|
+
const result = buildConversationLog(undefined, "", "claude");
|
|
109
|
+
expect(result.toolCallCount).toBe(0);
|
|
110
|
+
expect(result.filesChanged).toHaveLength(0);
|
|
111
|
+
expect(result.log).toBe("(no tool calls captured)");
|
|
112
|
+
});
|
|
113
|
+
});
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export declare class Orchestrator {
|
|
|
42
42
|
listTeammates(): string[];
|
|
43
43
|
/** Get the registry for direct access */
|
|
44
44
|
getRegistry(): Registry;
|
|
45
|
+
/** Get the adapter for direct access (used by /interrupt) */
|
|
46
|
+
getAdapter(): AgentAdapter;
|
|
45
47
|
/**
|
|
46
48
|
* Assign a task to a specific teammate and execute it.
|
|
47
49
|
* If the result contains a handoff, follows the chain automatically.
|
package/dist/orchestrator.js
CHANGED
|
@@ -42,6 +42,10 @@ export class Orchestrator {
|
|
|
42
42
|
getRegistry() {
|
|
43
43
|
return this.registry;
|
|
44
44
|
}
|
|
45
|
+
/** Get the adapter for direct access (used by /interrupt) */
|
|
46
|
+
getAdapter() {
|
|
47
|
+
return this.adapter;
|
|
48
|
+
}
|
|
45
49
|
/**
|
|
46
50
|
* Assign a task to a specific teammate and execute it.
|
|
47
51
|
* If the result contains a handoff, follows the chain automatically.
|
package/dist/registry.js
CHANGED
|
@@ -100,8 +100,8 @@ async function loadDailyLogs(memoryDir) {
|
|
|
100
100
|
// Only include daily logs (YYYY-MM-DD format), skip typed memory files
|
|
101
101
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(stem))
|
|
102
102
|
continue;
|
|
103
|
-
const
|
|
104
|
-
logs.push({ date: stem, content });
|
|
103
|
+
const raw = await readFile(join(memoryDir, entry), "utf-8");
|
|
104
|
+
logs.push({ date: stem, content: raw });
|
|
105
105
|
}
|
|
106
106
|
// Most recent first
|
|
107
107
|
logs.sort((a, b) => b.date.localeCompare(a.date));
|
|
@@ -124,8 +124,8 @@ async function loadWeeklyLogs(memoryDir) {
|
|
|
124
124
|
// Match YYYY-Wnn format
|
|
125
125
|
if (!/^\d{4}-W\d{2}$/.test(stem))
|
|
126
126
|
continue;
|
|
127
|
-
const
|
|
128
|
-
logs.push({ week: stem, content });
|
|
127
|
+
const raw = await readFile(join(weeklyDir, entry), "utf-8");
|
|
128
|
+
logs.push({ week: stem, content: raw });
|
|
129
129
|
}
|
|
130
130
|
// Most recent first
|
|
131
131
|
logs.sort((a, b) => b.week.localeCompare(a.week));
|