karajan-code 1.10.0 → 1.11.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.
@@ -1,9 +1,69 @@
1
1
  /**
2
2
  * Detects rate limit / usage cap messages from CLI agent output.
3
- * Returns { isRateLimit, agent, message } where agent is the best guess
4
- * of which CLI triggered it (or "unknown").
3
+ * Returns { isRateLimit, agent, message, cooldownUntil, cooldownMs }
4
+ * where agent is the best guess of which CLI triggered it (or "unknown").
5
5
  */
6
6
 
7
+ /**
8
+ * Extracts cooldown timing from a rate limit message string.
9
+ * Returns { cooldownUntil, cooldownMs } where cooldownUntil is an ISO string
10
+ * and cooldownMs is milliseconds to wait, or both null if not found.
11
+ */
12
+ export function parseCooldown(message) {
13
+ if (!message || typeof message !== "string") {
14
+ return { cooldownUntil: null, cooldownMs: null };
15
+ }
16
+
17
+ // 1. ISO timestamp: "try again after 2026-03-07T15:30:00Z"
18
+ // Also: "resets at 2026-03-07T15:30:00Z"
19
+ const isoMatch = message.match(
20
+ /(?:after|at)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)/i
21
+ );
22
+ if (isoMatch) {
23
+ const target = new Date(isoMatch[1]);
24
+ if (!isNaN(target.getTime())) {
25
+ const ms = Math.max(0, target.getTime() - Date.now());
26
+ return { cooldownUntil: target.toISOString(), cooldownMs: ms };
27
+ }
28
+ }
29
+
30
+ // 4. Claude specific: "resets at 2026-03-07 15:30 UTC" (space-separated date/time)
31
+ const resetMatch = message.match(
32
+ /resets?\s+at\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\s*UTC/i
33
+ );
34
+ if (resetMatch) {
35
+ const target = new Date(`${resetMatch[1]}T${resetMatch[2]}:00Z`);
36
+ if (!isNaN(target.getTime())) {
37
+ const ms = Math.max(0, target.getTime() - Date.now());
38
+ return { cooldownUntil: target.toISOString(), cooldownMs: ms };
39
+ }
40
+ }
41
+
42
+ // 2. Relative seconds: "retry after 120 seconds" / "retry in 120s" / "Retry-After: 120"
43
+ const secMatch = message.match(
44
+ /(?:retry[\s-]*after|retry\s+in|wait)\s*:?\s*(\d+)\s*(?:seconds?|secs?|s\b)/i
45
+ ) || message.match(/Retry-After:\s*(\d+)/i);
46
+ if (secMatch) {
47
+ const seconds = parseInt(secMatch[1], 10);
48
+ const ms = seconds * 1000;
49
+ const target = new Date(Date.now() + ms);
50
+ return { cooldownUntil: target.toISOString(), cooldownMs: ms };
51
+ }
52
+
53
+ // 3. Relative minutes: "retry in 5 minutes" / "wait 5 min"
54
+ const minMatch = message.match(
55
+ /(?:retry\s+in|wait|after)\s+(\d+)\s*(?:minutes?|mins?)/i
56
+ );
57
+ if (minMatch) {
58
+ const minutes = parseInt(minMatch[1], 10);
59
+ const ms = minutes * 60 * 1000;
60
+ const target = new Date(Date.now() + ms);
61
+ return { cooldownUntil: target.toISOString(), cooldownMs: ms };
62
+ }
63
+
64
+ return { cooldownUntil: null, cooldownMs: null };
65
+ }
66
+
7
67
  const RATE_LIMIT_PATTERNS = [
8
68
  // Claude CLI
9
69
  { pattern: /usage limit/i, agent: "claude" },
@@ -34,10 +94,11 @@ export function detectRateLimit({ stderr = "", stdout = "" }) {
34
94
  return {
35
95
  isRateLimit: true,
36
96
  agent,
37
- message: matchedLine.trim()
97
+ message: matchedLine.trim(),
98
+ ...parseCooldown(matchedLine)
38
99
  };
39
100
  }
40
101
  }
41
102
 
42
- return { isRateLimit: false, agent: "", message: "" };
103
+ return { isRateLimit: false, agent: "", message: "", cooldownUntil: null, cooldownMs: null };
43
104
  }
@@ -40,6 +40,9 @@ function formatLine(event) {
40
40
  if (event.detail?.silenceMs !== undefined) extra.push(`silence=${Math.round(event.detail.silenceMs / 1000)}s`);
41
41
  if (event.detail?.severity) extra.push(`severity=${event.detail.severity}`);
42
42
  if (event.detail?.stream) extra.push(`stream=${event.detail.stream}`);
43
+ if (event.detail?.cooldownUntil) extra.push(`until=${event.detail.cooldownUntil}`);
44
+ if (event.detail?.retryCount !== undefined) extra.push(`retry=${event.detail.retryCount}/${event.detail.maxRetries || "?"}`);
45
+ if (event.detail?.remainingMs !== undefined) extra.push(`remaining=${Math.round(event.detail.remainingMs / 1000)}s`);
43
46
 
44
47
  const extraStr = extra.length ? ` (${extra.join(", ")})` : "";
45
48
  return `${ts} [${type}] ${stage ? `[${stage}] ` : ""}${msg}${extraStr}`;
@@ -96,9 +99,78 @@ export function createRunLog(projectDir) {
96
99
  };
97
100
  }
98
101
 
102
+ /**
103
+ * Parse the run log to extract current status information.
104
+ */
105
+ function parseRunStatus(lines) {
106
+ const status = {
107
+ currentStage: null,
108
+ currentAgent: null,
109
+ startedAt: null,
110
+ isRunning: false,
111
+ lastEvent: null,
112
+ iteration: null,
113
+ errors: []
114
+ };
115
+
116
+ for (const line of lines) {
117
+ // Detect run start
118
+ if (line.includes("[kj_run] started") || line.includes("[kj_code] started") || line.includes("[kj_plan] started")) {
119
+ status.isRunning = true;
120
+ const tool = line.includes("kj_run") ? "kj_run" : line.includes("kj_code") ? "kj_code" : "kj_plan";
121
+ status.currentStage = tool;
122
+ const tsMatch = line.match(/^(\d{2}:\d{2}:\d{2}\.\d{3})/);
123
+ if (tsMatch) status.startedAt = tsMatch[1];
124
+ }
125
+
126
+ // Detect run finish
127
+ if (line.includes("[kj_run] finished") || line.includes("[kj_code] finished") || line.includes("[kj_plan] finished") || line.includes("finished")) {
128
+ if (line.includes("[kj_run]") || line.includes("[kj_code]") || line.includes("[kj_plan]")) {
129
+ status.isRunning = false;
130
+ }
131
+ }
132
+
133
+ // Detect stage transitions
134
+ const stageStart = line.match(/\[(\w+):start\]/);
135
+ if (stageStart) {
136
+ status.currentStage = stageStart[1];
137
+ }
138
+ const stageDone = line.match(/\[(\w+):done\]|\[(\w+)\] finished/);
139
+ if (stageDone) {
140
+ const doneName = stageDone[1] || stageDone[2];
141
+ if (doneName === status.currentStage) status.currentStage = "idle";
142
+ }
143
+
144
+ // Detect agent
145
+ const agentMatch = line.match(/agent=(\w+)/);
146
+ if (agentMatch) status.currentAgent = agentMatch[1];
147
+
148
+ // Detect iteration
149
+ const iterMatch = line.match(/[Ii]teration\s+(\d+)/);
150
+ if (iterMatch) status.iteration = parseInt(iterMatch[1], 10);
151
+
152
+ // Detect errors
153
+ if (line.match(/\[.*:fail\]|\[.*error\]/i) || line.includes("ERROR")) {
154
+ status.errors.push(line.trim());
155
+ }
156
+
157
+ // Detect standby
158
+ if (line.includes("[standby]") || line.includes("standby")) {
159
+ status.currentStage = "standby";
160
+ }
161
+
162
+ status.lastEvent = line.trim();
163
+ }
164
+
165
+ // Keep only last 3 errors
166
+ if (status.errors.length > 3) status.errors = status.errors.slice(-3);
167
+
168
+ return status;
169
+ }
170
+
99
171
  /**
100
172
  * Read the current run log contents.
101
- * Returns the last N lines (default 50).
173
+ * Returns the last N lines (default 50) plus a parsed status summary.
102
174
  */
103
175
  export function readRunLog(maxLines = 50, projectDir) {
104
176
  const logPath = resolveLogPath(projectDir);
@@ -107,10 +179,12 @@ export function readRunLog(maxLines = 50, projectDir) {
107
179
  const lines = content.split("\n").filter(Boolean);
108
180
  const total = lines.length;
109
181
  const shown = lines.slice(-maxLines);
182
+ const status = parseRunStatus(lines);
110
183
  return {
111
184
  ok: true,
112
185
  path: logPath,
113
186
  totalLines: total,
187
+ status,
114
188
  lines: shown,
115
189
  summary: shown.join("\n")
116
190
  };