@jmylchreest/aide-plugin 0.0.54 → 0.0.56
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/bin/aide-wrapper.ts +360 -0
- package/package.json +6 -3
- package/src/cli/mcp.ts +17 -22
- package/src/core/aide-client.ts +26 -16
- package/src/core/context-guard.ts +101 -1
- package/src/core/read-tracking.ts +151 -0
- package/src/core/session-init.ts +2 -2
- package/src/core/session-summary-logic.ts +73 -58
- package/src/core/skill-matcher.ts +5 -12
- package/src/core/tool-tracking.ts +3 -2
- package/src/lib/hud.ts +85 -86
- package/src/lib/logger.ts +38 -9
- package/src/opencode/hooks.ts +43 -13
- package/src/opencode/index.ts +3 -2
- package/bin/aide-wrapper.sh +0 -159
- package/src/lib/skills-registry.ts +0 -362
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read tracking — platform-agnostic core logic.
|
|
3
|
+
*
|
|
4
|
+
* Tracks file reads per session and checks file freshness against
|
|
5
|
+
* the aide code index. Used by both Claude Code hooks and OpenCode plugin
|
|
6
|
+
* to provide smart read hints (suggest code_outline/code_symbols over
|
|
7
|
+
* redundant file re-reads).
|
|
8
|
+
*
|
|
9
|
+
* Gated behind AIDE_CODE_WATCH=1 (file watcher must be enabled).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execFileSync } from "child_process";
|
|
13
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
14
|
+
import { setState, getState } from "./aide-client.js";
|
|
15
|
+
import { debug } from "../lib/logger.js";
|
|
16
|
+
|
|
17
|
+
const SOURCE = "read-tracking";
|
|
18
|
+
|
|
19
|
+
/** Prefix for state keys tracking file reads */
|
|
20
|
+
const STATE_KEY_PREFIX = "file-read:";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Result from checking file freshness against the code index.
|
|
24
|
+
*/
|
|
25
|
+
export interface ReadCheckResult {
|
|
26
|
+
indexed: boolean;
|
|
27
|
+
fresh: boolean;
|
|
28
|
+
symbols: number;
|
|
29
|
+
outline_available: boolean;
|
|
30
|
+
estimated_tokens: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Normalize a file path to a relative path from cwd.
|
|
35
|
+
* Ensures consistent state keys regardless of absolute/relative input.
|
|
36
|
+
*/
|
|
37
|
+
function toRelativePath(cwd: string, filePath: string): string {
|
|
38
|
+
const abs = isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
|
|
39
|
+
return relative(cwd, abs);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Record that a file was read in this session.
|
|
44
|
+
* Sets a state key so subsequent reads can be detected.
|
|
45
|
+
*
|
|
46
|
+
* No-op if AIDE_CODE_WATCH is not enabled.
|
|
47
|
+
*/
|
|
48
|
+
export function recordFileRead(
|
|
49
|
+
binary: string,
|
|
50
|
+
cwd: string,
|
|
51
|
+
filePath: string,
|
|
52
|
+
): void {
|
|
53
|
+
if (process.env.AIDE_CODE_WATCH !== "1") return;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const relPath = toRelativePath(cwd, filePath);
|
|
57
|
+
const key = STATE_KEY_PREFIX + relPath;
|
|
58
|
+
setState(binary, cwd, key, new Date().toISOString());
|
|
59
|
+
debug(SOURCE, `Recorded read: ${relPath}`);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
debug(SOURCE, `Failed to record read: ${err}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a file was previously read in this session.
|
|
67
|
+
* Returns the ISO timestamp of the last read, or null if not read.
|
|
68
|
+
*
|
|
69
|
+
* Returns null if AIDE_CODE_WATCH is not enabled.
|
|
70
|
+
*/
|
|
71
|
+
export function getPreviousRead(
|
|
72
|
+
binary: string,
|
|
73
|
+
cwd: string,
|
|
74
|
+
filePath: string,
|
|
75
|
+
): string | null {
|
|
76
|
+
if (process.env.AIDE_CODE_WATCH !== "1") return null;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const relPath = toRelativePath(cwd, filePath);
|
|
80
|
+
const key = STATE_KEY_PREFIX + relPath;
|
|
81
|
+
return getState(binary, cwd, key);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
debug(SOURCE, `Failed to check previous read: ${err}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check whether a file is indexed and whether its content is fresh
|
|
90
|
+
* (unchanged since last indexing) by calling `aide code read-check`.
|
|
91
|
+
*
|
|
92
|
+
* Returns null on any error (binary not found, command failed, etc.).
|
|
93
|
+
*/
|
|
94
|
+
export function checkFileReadFreshness(
|
|
95
|
+
binary: string,
|
|
96
|
+
cwd: string,
|
|
97
|
+
filePath: string,
|
|
98
|
+
): ReadCheckResult | null {
|
|
99
|
+
try {
|
|
100
|
+
const relPath = toRelativePath(cwd, filePath);
|
|
101
|
+
const output = execFileSync(binary, ["code", "read-check", relPath, "--json"], {
|
|
102
|
+
cwd,
|
|
103
|
+
encoding: "utf-8",
|
|
104
|
+
timeout: 5000,
|
|
105
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
106
|
+
});
|
|
107
|
+
const result = JSON.parse(output.trim()) as ReadCheckResult;
|
|
108
|
+
debug(SOURCE, `Read check ${relPath}: indexed=${result.indexed} fresh=${result.fresh} symbols=${result.symbols}`);
|
|
109
|
+
return result;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
debug(SOURCE, `Read check failed: ${err}`);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Record a token event via `aide token record`.
|
|
118
|
+
* Fire-and-forget — errors are logged but not propagated.
|
|
119
|
+
*/
|
|
120
|
+
export function recordTokenEvent(
|
|
121
|
+
binary: string,
|
|
122
|
+
cwd: string,
|
|
123
|
+
eventType: string,
|
|
124
|
+
tool: string,
|
|
125
|
+
filePath: string,
|
|
126
|
+
tokens: number,
|
|
127
|
+
tokensSaved: number = 0,
|
|
128
|
+
): void {
|
|
129
|
+
try {
|
|
130
|
+
const args = ["token", "record", eventType, tool, filePath, String(tokens)];
|
|
131
|
+
if (tokensSaved > 0) {
|
|
132
|
+
args.push(String(tokensSaved));
|
|
133
|
+
}
|
|
134
|
+
execFileSync(binary, args, {
|
|
135
|
+
cwd,
|
|
136
|
+
timeout: 3000,
|
|
137
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
138
|
+
});
|
|
139
|
+
debug(SOURCE, `Token event: ${eventType} ${tool} ${filePath} tokens=${tokens} saved=${tokensSaved}`);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
debug(SOURCE, `Failed to record token event: ${err}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Estimate tokens for a file by its size, using the default ratio.
|
|
147
|
+
* This is a rough client-side estimate; the Go binary has per-language ratios.
|
|
148
|
+
*/
|
|
149
|
+
export function estimateTokensFromSize(sizeBytes: number): number {
|
|
150
|
+
return Math.round(sizeBytes / 3.0);
|
|
151
|
+
}
|
package/src/core/session-init.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
unlinkSync,
|
|
16
16
|
statSync,
|
|
17
17
|
} from "fs";
|
|
18
|
-
import { join } from "path";
|
|
18
|
+
import { basename, join } from "path";
|
|
19
19
|
import { execFileSync } from "child_process";
|
|
20
20
|
import { homedir } from "os";
|
|
21
21
|
import type {
|
|
@@ -182,7 +182,7 @@ export function getProjectName(cwd: string): string {
|
|
|
182
182
|
// Not a git repo or no remote
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
return cwd
|
|
185
|
+
return basename(cwd) || "unknown";
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
/**
|
|
@@ -41,6 +41,78 @@ export function getSessionCommits(cwd: string, startedAt?: string): string[] {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
interface TranscriptEntry {
|
|
45
|
+
type?: string;
|
|
46
|
+
tool_name?: string;
|
|
47
|
+
tool_input?: { file_path?: string; [key: string]: unknown };
|
|
48
|
+
content?:
|
|
49
|
+
| string
|
|
50
|
+
| {
|
|
51
|
+
text?: string;
|
|
52
|
+
tool_use?: { name?: string; input?: { file_path?: string } };
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface TranscriptData {
|
|
57
|
+
filesModified: Set<string>;
|
|
58
|
+
toolsUsed: Set<string>;
|
|
59
|
+
userMessages: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse raw JSONL transcript lines into structured data.
|
|
64
|
+
*/
|
|
65
|
+
function parseTranscript(lines: string[]): TranscriptData {
|
|
66
|
+
const entries: TranscriptEntry[] = [];
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed: unknown = JSON.parse(line);
|
|
70
|
+
if (
|
|
71
|
+
typeof parsed === "object" &&
|
|
72
|
+
parsed !== null &&
|
|
73
|
+
!Array.isArray(parsed)
|
|
74
|
+
) {
|
|
75
|
+
entries.push(parsed as TranscriptEntry);
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Skip malformed
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const filesModified = new Set<string>();
|
|
83
|
+
const toolsUsed = new Set<string>();
|
|
84
|
+
const userMessages: string[] = [];
|
|
85
|
+
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const contentObj =
|
|
88
|
+
typeof entry.content === "object" ? entry.content : null;
|
|
89
|
+
if (
|
|
90
|
+
entry.type === "tool_use" ||
|
|
91
|
+
(entry.type === "assistant" && contentObj?.tool_use)
|
|
92
|
+
) {
|
|
93
|
+
const toolName = entry.tool_name || contentObj?.tool_use?.name;
|
|
94
|
+
if (toolName) toolsUsed.add(toolName);
|
|
95
|
+
|
|
96
|
+
const toolInput = entry.tool_input || contentObj?.tool_use?.input;
|
|
97
|
+
if (toolInput?.file_path && toolName) {
|
|
98
|
+
if (["Write", "Edit"].includes(toolName)) {
|
|
99
|
+
filesModified.add(toolInput.file_path);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (entry.type === "human" || entry.type === "user") {
|
|
105
|
+
const text =
|
|
106
|
+
typeof entry.content === "string" ? entry.content : contentObj?.text;
|
|
107
|
+
if (text && text.length > 10 && text.length < 500) {
|
|
108
|
+
userMessages.push(text.slice(0, 200));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { filesModified, toolsUsed, userMessages };
|
|
114
|
+
}
|
|
115
|
+
|
|
44
116
|
/**
|
|
45
117
|
* Build a session summary from transcript data.
|
|
46
118
|
*
|
|
@@ -61,64 +133,7 @@ export function buildSessionSummary(
|
|
|
61
133
|
|
|
62
134
|
if (lines.length < 5) return null;
|
|
63
135
|
|
|
64
|
-
|
|
65
|
-
type?: string;
|
|
66
|
-
tool_name?: string;
|
|
67
|
-
tool_input?: { file_path?: string; [key: string]: unknown };
|
|
68
|
-
content?:
|
|
69
|
-
| string
|
|
70
|
-
| {
|
|
71
|
-
text?: string;
|
|
72
|
-
tool_use?: { name?: string; input?: { file_path?: string } };
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const entries: TranscriptEntry[] = [];
|
|
77
|
-
for (const line of lines) {
|
|
78
|
-
try {
|
|
79
|
-
const parsed: unknown = JSON.parse(line);
|
|
80
|
-
if (
|
|
81
|
-
typeof parsed === "object" &&
|
|
82
|
-
parsed !== null &&
|
|
83
|
-
!Array.isArray(parsed)
|
|
84
|
-
) {
|
|
85
|
-
entries.push(parsed as TranscriptEntry);
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
// Skip malformed
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const filesModified = new Set<string>();
|
|
93
|
-
const toolsUsed = new Set<string>();
|
|
94
|
-
const userMessages: string[] = [];
|
|
95
|
-
|
|
96
|
-
for (const entry of entries) {
|
|
97
|
-
const contentObj =
|
|
98
|
-
typeof entry.content === "object" ? entry.content : null;
|
|
99
|
-
if (
|
|
100
|
-
entry.type === "tool_use" ||
|
|
101
|
-
(entry.type === "assistant" && contentObj?.tool_use)
|
|
102
|
-
) {
|
|
103
|
-
const toolName = entry.tool_name || contentObj?.tool_use?.name;
|
|
104
|
-
if (toolName) toolsUsed.add(toolName);
|
|
105
|
-
|
|
106
|
-
const toolInput = entry.tool_input || contentObj?.tool_use?.input;
|
|
107
|
-
if (toolInput?.file_path && toolName) {
|
|
108
|
-
if (["Write", "Edit"].includes(toolName)) {
|
|
109
|
-
filesModified.add(toolInput.file_path);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (entry.type === "human" || entry.type === "user") {
|
|
115
|
-
const text =
|
|
116
|
-
typeof entry.content === "string" ? entry.content : contentObj?.text;
|
|
117
|
-
if (text && text.length > 10 && text.length < 500) {
|
|
118
|
-
userMessages.push(text.slice(0, 200));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
136
|
+
const { filesModified, toolsUsed, userMessages } = parseTranscript(lines);
|
|
122
137
|
|
|
123
138
|
const commits = getSessionCommits(cwd, startedAt);
|
|
124
139
|
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
9
9
|
import { join, basename, extname } from "path";
|
|
10
10
|
import { homedir } from "os";
|
|
11
|
-
import
|
|
11
|
+
import which from "which";
|
|
12
12
|
import type { Skill, SkillMatchResult } from "./types.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Cache of binary existence checks to avoid repeated
|
|
15
|
+
* Cache of binary existence checks to avoid repeated lookups.
|
|
16
16
|
* Maps binary name to boolean (exists on PATH).
|
|
17
17
|
*/
|
|
18
18
|
const binaryExistsCache = new Map<string, boolean>();
|
|
@@ -25,16 +25,9 @@ function binaryExists(name: string): boolean {
|
|
|
25
25
|
const cached = binaryExistsCache.get(name);
|
|
26
26
|
if (cached !== undefined) return cached;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
execSync(cmd, { stdio: "ignore", timeout: 2000 });
|
|
32
|
-
binaryExistsCache.set(name, true);
|
|
33
|
-
return true;
|
|
34
|
-
} catch {
|
|
35
|
-
binaryExistsCache.set(name, false);
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
28
|
+
const found = which.sync(name, { nothrow: true }) !== null;
|
|
29
|
+
binaryExistsCache.set(name, found);
|
|
30
|
+
return found;
|
|
38
31
|
}
|
|
39
32
|
|
|
40
33
|
// Skill search locations relative to cwd
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Tracks tool usage per-agent and updates session statistics.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { basename } from "path";
|
|
8
9
|
import { setState, getState } from "./aide-client.js";
|
|
9
10
|
import type { ToolUseInfo } from "./types.js";
|
|
10
11
|
|
|
@@ -31,7 +32,7 @@ export function formatToolDescription(
|
|
|
31
32
|
case "Read":
|
|
32
33
|
if (toolInput.file_path) {
|
|
33
34
|
const filename =
|
|
34
|
-
toolInput.file_path
|
|
35
|
+
basename(toolInput.file_path);
|
|
35
36
|
return `Read(${filename})`;
|
|
36
37
|
}
|
|
37
38
|
return toolName;
|
|
@@ -40,7 +41,7 @@ export function formatToolDescription(
|
|
|
40
41
|
case "Write":
|
|
41
42
|
if (toolInput.file_path) {
|
|
42
43
|
const filename =
|
|
43
|
-
toolInput.file_path
|
|
44
|
+
basename(toolInput.file_path);
|
|
44
45
|
return `${toolName}(${filename})`;
|
|
45
46
|
}
|
|
46
47
|
return toolName;
|
package/src/lib/hud.ts
CHANGED
|
@@ -290,6 +290,87 @@ async function refreshUsageCache(): Promise<void> {
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
type ElementFormatter = (
|
|
294
|
+
state: SessionState,
|
|
295
|
+
agents: AgentState[],
|
|
296
|
+
config: HudConfig,
|
|
297
|
+
cwd: string,
|
|
298
|
+
) => string | null;
|
|
299
|
+
|
|
300
|
+
const elementFormatters: Record<string, ElementFormatter> = {
|
|
301
|
+
mode: (state, _agents, config) => {
|
|
302
|
+
const modeName = state.activeMode || "idle";
|
|
303
|
+
const toolSuffix = state.lastTool ? `(${state.lastTool})` : "";
|
|
304
|
+
if (config.format === "icons") {
|
|
305
|
+
const icon = ICONS.mode[state.activeMode || "none"] || ICONS.mode.none;
|
|
306
|
+
return `${icon} ${modeName}${toolSuffix}`;
|
|
307
|
+
}
|
|
308
|
+
return `mode:${modeName}${toolSuffix}`;
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
agents: (_state, agents, config) => {
|
|
312
|
+
const runningCount = agents.filter((a) => a.status === "running").length;
|
|
313
|
+
if (runningCount <= 0) return null;
|
|
314
|
+
if (config.format === "icons") {
|
|
315
|
+
return `${ICONS.agents} ${runningCount}`;
|
|
316
|
+
}
|
|
317
|
+
return `agents:${runningCount}`;
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
duration: (state, _agents, config) => {
|
|
321
|
+
const duration = formatDuration(state.startedAt);
|
|
322
|
+
if (config.format === "icons") {
|
|
323
|
+
return `${ICONS.time} ${duration}`;
|
|
324
|
+
}
|
|
325
|
+
return duration;
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
tools: (state, _agents, config) => {
|
|
329
|
+
if (state.toolCalls <= 0) return null;
|
|
330
|
+
if (config.format === "icons") {
|
|
331
|
+
return `🔧 ${state.toolCalls}`;
|
|
332
|
+
}
|
|
333
|
+
return `tools:${state.toolCalls}`;
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
usage: (_state, _agents, config, cwd) => {
|
|
337
|
+
const cacheTTL = config.usageCacheTTL
|
|
338
|
+
? config.usageCacheTTL * 1000
|
|
339
|
+
: undefined;
|
|
340
|
+
const usage = getUsageSummary(cwd, cacheTTL);
|
|
341
|
+
if (!usage) return null;
|
|
342
|
+
|
|
343
|
+
const fmt = (n: number) =>
|
|
344
|
+
n >= 1000000
|
|
345
|
+
? `${(n / 1000000).toFixed(1)}M`
|
|
346
|
+
: n >= 1000
|
|
347
|
+
? `${(n / 1000).toFixed(0)}K`
|
|
348
|
+
: `${n}`;
|
|
349
|
+
|
|
350
|
+
let usageStr: string;
|
|
351
|
+
if (usage.fiveHourPercent !== null) {
|
|
352
|
+
// API-sourced percentages (accurate)
|
|
353
|
+
const remainStr =
|
|
354
|
+
usage.fiveHourRemain && usage.fiveHourRemain !== "expired"
|
|
355
|
+
? ` ~${usage.fiveHourRemain}`
|
|
356
|
+
: "";
|
|
357
|
+
const weekStr =
|
|
358
|
+
usage.weeklyPercent !== null
|
|
359
|
+
? ` wk:${Math.round(usage.weeklyPercent)}%`
|
|
360
|
+
: "";
|
|
361
|
+
usageStr = `5h:${Math.round(usage.fiveHourPercent)}%${remainStr}${weekStr}`;
|
|
362
|
+
} else {
|
|
363
|
+
// Fallback to weighted token counts
|
|
364
|
+
usageStr = `5h:${fmt(usage.window5hTokens)}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (config.format === "icons") {
|
|
368
|
+
return `📊 ${usageStr}`;
|
|
369
|
+
}
|
|
370
|
+
return usageStr;
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
293
374
|
/**
|
|
294
375
|
* Format HUD output based on config
|
|
295
376
|
*/
|
|
@@ -304,92 +385,10 @@ export function formatHud(
|
|
|
304
385
|
const parts: string[] = [];
|
|
305
386
|
|
|
306
387
|
for (const element of config.elements) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (config.format === "icons") {
|
|
312
|
-
const icon =
|
|
313
|
-
ICONS.mode[state.activeMode || "none"] || ICONS.mode.none;
|
|
314
|
-
parts.push(`${icon} ${modeName}${toolSuffix}`);
|
|
315
|
-
} else {
|
|
316
|
-
parts.push(`mode:${modeName}${toolSuffix}`);
|
|
317
|
-
}
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
case "agents": {
|
|
322
|
-
const runningCount = agents.filter(
|
|
323
|
-
(a) => a.status === "running",
|
|
324
|
-
).length;
|
|
325
|
-
if (runningCount > 0) {
|
|
326
|
-
if (config.format === "icons") {
|
|
327
|
-
parts.push(`${ICONS.agents} ${runningCount}`);
|
|
328
|
-
} else {
|
|
329
|
-
parts.push(`agents:${runningCount}`);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
case "duration": {
|
|
336
|
-
const duration = formatDuration(state.startedAt);
|
|
337
|
-
if (config.format === "icons") {
|
|
338
|
-
parts.push(`${ICONS.time} ${duration}`);
|
|
339
|
-
} else {
|
|
340
|
-
parts.push(duration);
|
|
341
|
-
}
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
case "tools":
|
|
346
|
-
if (state.toolCalls > 0) {
|
|
347
|
-
if (config.format === "icons") {
|
|
348
|
-
parts.push(`🔧 ${state.toolCalls}`);
|
|
349
|
-
} else {
|
|
350
|
-
parts.push(`tools:${state.toolCalls}`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
break;
|
|
354
|
-
|
|
355
|
-
case "usage": {
|
|
356
|
-
const cacheTTL = config.usageCacheTTL
|
|
357
|
-
? config.usageCacheTTL * 1000
|
|
358
|
-
: undefined;
|
|
359
|
-
const usage = getUsageSummary(cwd, cacheTTL);
|
|
360
|
-
if (usage) {
|
|
361
|
-
const fmt = (n: number) =>
|
|
362
|
-
n >= 1000000
|
|
363
|
-
? `${(n / 1000000).toFixed(1)}M`
|
|
364
|
-
: n >= 1000
|
|
365
|
-
? `${(n / 1000).toFixed(0)}K`
|
|
366
|
-
: `${n}`;
|
|
367
|
-
|
|
368
|
-
let usageStr: string;
|
|
369
|
-
if (usage.fiveHourPercent !== null) {
|
|
370
|
-
// API-sourced percentages (accurate)
|
|
371
|
-
const remainStr =
|
|
372
|
-
usage.fiveHourRemain && usage.fiveHourRemain !== "expired"
|
|
373
|
-
? ` ~${usage.fiveHourRemain}`
|
|
374
|
-
: "";
|
|
375
|
-
const weekStr =
|
|
376
|
-
usage.weeklyPercent !== null
|
|
377
|
-
? ` wk:${Math.round(usage.weeklyPercent)}%`
|
|
378
|
-
: "";
|
|
379
|
-
usageStr = `5h:${Math.round(usage.fiveHourPercent)}%${remainStr}${weekStr}`;
|
|
380
|
-
} else {
|
|
381
|
-
// Fallback to weighted token counts
|
|
382
|
-
usageStr = `5h:${fmt(usage.window5hTokens)}`;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (config.format === "icons") {
|
|
386
|
-
parts.push(`📊 ${usageStr}`);
|
|
387
|
-
} else {
|
|
388
|
-
parts.push(usageStr);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
break;
|
|
392
|
-
}
|
|
388
|
+
const formatter = elementFormatters[element];
|
|
389
|
+
if (formatter) {
|
|
390
|
+
const part = formatter(state, agents, config, cwd);
|
|
391
|
+
if (part) parts.push(part);
|
|
393
392
|
}
|
|
394
393
|
}
|
|
395
394
|
|
package/src/lib/logger.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* AIDE Debug Logger
|
|
3
3
|
*
|
|
4
4
|
* Provides timing and tracing for hooks and operations.
|
|
5
|
-
* Disabled by default
|
|
5
|
+
* Disabled by default. Enable with either:
|
|
6
|
+
* 1. AIDE_DEBUG=1 environment variable
|
|
7
|
+
* 2. touch .aide/.debug sentinel file (no restart needed)
|
|
6
8
|
*
|
|
7
9
|
* Usage:
|
|
8
10
|
* import { Logger } from '../lib/logger';
|
|
@@ -13,7 +15,8 @@
|
|
|
13
15
|
* log.flush(); // Write all logs to file
|
|
14
16
|
*
|
|
15
17
|
* Enable logging:
|
|
16
|
-
* AIDE_DEBUG=1 claude
|
|
18
|
+
* AIDE_DEBUG=1 claude # env var (requires restart)
|
|
19
|
+
* touch .aide/.debug # sentinel file (live toggle)
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
22
|
import { existsSync, mkdirSync, appendFileSync, writeFileSync } from "fs";
|
|
@@ -46,10 +49,11 @@ export class Logger {
|
|
|
46
49
|
private sessionStart: number;
|
|
47
50
|
|
|
48
51
|
constructor(source: string, cwd?: string) {
|
|
49
|
-
const debugEnv = process.env.AIDE_DEBUG || "";
|
|
50
|
-
this.enabled = debugEnv === "1" || debugEnv === "true";
|
|
51
52
|
this.source = source;
|
|
52
53
|
this.cwd = cwd || process.cwd();
|
|
54
|
+
// Set debugLogCwd so isDebugEnabled() can check the sentinel file
|
|
55
|
+
if (cwd) setDebugCwd(cwd);
|
|
56
|
+
this.enabled = isDebugEnabled();
|
|
53
57
|
this.logDir = join(this.cwd, ".aide", "_logs");
|
|
54
58
|
this.logFile = join(this.logDir, "startup.log");
|
|
55
59
|
this.sessionStart = Date.now();
|
|
@@ -329,16 +333,39 @@ export function getLogger(source: string, cwd?: string): Logger {
|
|
|
329
333
|
return defaultLogger;
|
|
330
334
|
}
|
|
331
335
|
|
|
336
|
+
// Debug log state - tracks cwd for file-based logging
|
|
337
|
+
let debugLogCwd = process.cwd();
|
|
338
|
+
|
|
339
|
+
// Cache sentinel file check per cwd to avoid repeated stat calls
|
|
340
|
+
let debugSentinelCwd = "";
|
|
341
|
+
let debugSentinelResult = false;
|
|
342
|
+
|
|
332
343
|
/**
|
|
333
|
-
* Quick check if debug logging is enabled
|
|
344
|
+
* Quick check if debug logging is enabled.
|
|
345
|
+
*
|
|
346
|
+
* Checks (in order):
|
|
347
|
+
* 1. AIDE_DEBUG=1 environment variable
|
|
348
|
+
* 2. .aide/.debug sentinel file in the project root
|
|
349
|
+
*
|
|
350
|
+
* The sentinel file allows enabling debug without restarting Claude.
|
|
351
|
+
* Create it with: touch .aide/.debug
|
|
352
|
+
* Remove it with: rm .aide/.debug
|
|
334
353
|
*/
|
|
335
354
|
export function isDebugEnabled(): boolean {
|
|
336
355
|
const debugEnv = process.env.AIDE_DEBUG || "";
|
|
337
|
-
|
|
338
|
-
}
|
|
356
|
+
if (debugEnv === "1" || debugEnv === "true") return true;
|
|
339
357
|
|
|
340
|
-
//
|
|
341
|
-
|
|
358
|
+
// Check sentinel file (cached per cwd)
|
|
359
|
+
if (debugLogCwd !== debugSentinelCwd) {
|
|
360
|
+
debugSentinelCwd = debugLogCwd;
|
|
361
|
+
try {
|
|
362
|
+
debugSentinelResult = existsSync(join(debugLogCwd, ".aide", ".debug"));
|
|
363
|
+
} catch {
|
|
364
|
+
debugSentinelResult = false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return debugSentinelResult;
|
|
368
|
+
}
|
|
342
369
|
|
|
343
370
|
/**
|
|
344
371
|
* Set the working directory for debug logging.
|
|
@@ -346,6 +373,8 @@ let debugLogCwd = process.cwd();
|
|
|
346
373
|
*/
|
|
347
374
|
export function setDebugCwd(cwd: string): void {
|
|
348
375
|
debugLogCwd = cwd;
|
|
376
|
+
// Invalidate sentinel cache so next isDebugEnabled() re-checks
|
|
377
|
+
debugSentinelCwd = "";
|
|
349
378
|
}
|
|
350
379
|
|
|
351
380
|
/**
|