agent-profiler 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/cli.js +5 -4
- package/dist/commands/init.js +4 -0
- package/dist/commands/status.js +36 -9
- package/dist/core/db.js +164 -28
- package/dist/core/gitWorkspace.js +46 -5
- package/dist/core/packageMeta.js +20 -0
- package/dist/core/schema.sql +8 -0
- package/package.json +57 -3
- package/agent-profiler-0.1.0.tgz +0 -0
- package/docs/agent-profiler-mvp-handoff.md +0 -980
- package/google-home.png +0 -0
- package/src/adapters/codex.ts +0 -131
- package/src/adapters/cursor.ts +0 -115
- package/src/cli.ts +0 -109
- package/src/commands/auditContext.ts +0 -62
- package/src/commands/hook.ts +0 -104
- package/src/commands/init.ts +0 -324
- package/src/commands/last.ts +0 -326
- package/src/commands/status.ts +0 -345
- package/src/core/contextAudit.ts +0 -102
- package/src/core/db.ts +0 -491
- package/src/core/eventMetadata.ts +0 -184
- package/src/core/gitWorkspace.ts +0 -92
- package/src/core/normalize.ts +0 -29
- package/src/core/profile.ts +0 -35
- package/src/core/schema.sql +0 -56
- package/src/core/tokens.ts +0 -4
- package/src/types/better-sqlite3.d.ts +0 -26
- package/tsconfig.json +0 -15
package/src/commands/status.ts
DELETED
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
getDefaultDbPath,
|
|
5
|
-
getLastEventSummary,
|
|
6
|
-
openDb,
|
|
7
|
-
resolveUsableDbPath,
|
|
8
|
-
} from "../core/db.js";
|
|
9
|
-
import { getPreferredConfigPath } from "../core/profile.js";
|
|
10
|
-
|
|
11
|
-
type AgentProfilerConfig = {
|
|
12
|
-
adapters?: {
|
|
13
|
-
cursor?: {
|
|
14
|
-
enabled?: boolean;
|
|
15
|
-
hookFile?: string;
|
|
16
|
-
mode?: "dev" | "prod";
|
|
17
|
-
};
|
|
18
|
-
codex?: {
|
|
19
|
-
enabled?: boolean;
|
|
20
|
-
hookFile?: string;
|
|
21
|
-
mode?: "dev" | "prod";
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
databasePath?: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
type CursorHookCommand = { command?: string };
|
|
28
|
-
|
|
29
|
-
type CursorHooksConfig = {
|
|
30
|
-
hooks?: Record<string, string | CursorHookCommand[]>;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function hookEntryCommand(value: unknown): string {
|
|
34
|
-
if (typeof value === "string") return value;
|
|
35
|
-
if (!Array.isArray(value) || value.length === 0) return "";
|
|
36
|
-
const first = value[0];
|
|
37
|
-
if (first && typeof first === "object" && typeof first.command === "string") {
|
|
38
|
-
return first.command;
|
|
39
|
-
}
|
|
40
|
-
return "";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function hookConfigured(hooks: Record<string, string | CursorHookCommand[]>, eventName: string): boolean {
|
|
44
|
-
return hookEntryCommand(hooks[eventName]).trim().length > 0;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const REQUIRED_CURSOR_EVENTS = [
|
|
48
|
-
"beforeSubmitPrompt",
|
|
49
|
-
"afterAgentResponse",
|
|
50
|
-
"afterShellExecution",
|
|
51
|
-
"afterFileEdit",
|
|
52
|
-
"stop",
|
|
53
|
-
"preToolUse",
|
|
54
|
-
"postToolUse",
|
|
55
|
-
"postToolUseFailure",
|
|
56
|
-
"beforeMCPExecution",
|
|
57
|
-
"afterMCPExecution",
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
const REQUIRED_CODEX_EVENTS = [
|
|
61
|
-
"SessionStart",
|
|
62
|
-
"UserPromptSubmit",
|
|
63
|
-
"PreToolUse",
|
|
64
|
-
"PostToolUse",
|
|
65
|
-
"Stop",
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
type CodexHookHandler = { command?: string };
|
|
69
|
-
type CodexHookGroup = { hooks?: CodexHookHandler[] };
|
|
70
|
-
type CodexHooksFile = { hooks?: Record<string, CodexHookGroup[]> };
|
|
71
|
-
|
|
72
|
-
function codexEventConfigured(
|
|
73
|
-
hooks: Record<string, CodexHookGroup[]>,
|
|
74
|
-
eventName: string,
|
|
75
|
-
marker: string,
|
|
76
|
-
): boolean {
|
|
77
|
-
const groups = hooks[eventName] ?? [];
|
|
78
|
-
for (const g of groups) {
|
|
79
|
-
for (const h of g.hooks ?? []) {
|
|
80
|
-
if (typeof h.command === "string" && h.command.includes(marker)) return true;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function readJsonFile<T>(filePath: string): T | null {
|
|
87
|
-
try {
|
|
88
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8")) as T;
|
|
89
|
-
} catch {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function getProfilerConfigPath(resolvedDbPath: string): string {
|
|
95
|
-
return path.join(path.dirname(resolvedDbPath), "config.json");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function resolveStatusPaths(mode: "dev" | "prod"): {
|
|
99
|
-
configuredDbPath: string;
|
|
100
|
-
resolvedDbPath: string;
|
|
101
|
-
configPath: string;
|
|
102
|
-
} {
|
|
103
|
-
const preferredConfigPath =
|
|
104
|
-
mode === "prod"
|
|
105
|
-
? path.join(process.env.HOME ?? "", ".agent-profiler", "config.json")
|
|
106
|
-
: getPreferredConfigPath(process.cwd());
|
|
107
|
-
const config = readJsonFile<AgentProfilerConfig>(preferredConfigPath);
|
|
108
|
-
|
|
109
|
-
if (config?.databasePath) {
|
|
110
|
-
const resolvedFromConfig = resolveUsableDbPath(config.databasePath);
|
|
111
|
-
return {
|
|
112
|
-
configuredDbPath: config.databasePath,
|
|
113
|
-
resolvedDbPath: resolvedFromConfig,
|
|
114
|
-
configPath: preferredConfigPath,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const defaultDbPath = resolveUsableDbPath(getDefaultDbPath());
|
|
119
|
-
const defaultConfigPath = getProfilerConfigPath(defaultDbPath);
|
|
120
|
-
return {
|
|
121
|
-
configuredDbPath: getDefaultDbPath(),
|
|
122
|
-
resolvedDbPath: defaultDbPath,
|
|
123
|
-
configPath: defaultConfigPath,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function commandExistsInPath(command: string): boolean {
|
|
128
|
-
const pathValue = process.env.PATH ?? "";
|
|
129
|
-
for (const dir of pathValue.split(path.delimiter)) {
|
|
130
|
-
if (!dir) continue;
|
|
131
|
-
const fullPath = path.join(dir, command);
|
|
132
|
-
if (fs.existsSync(fullPath)) return true;
|
|
133
|
-
}
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getCursorSetupStatus(
|
|
138
|
-
configPath: string,
|
|
139
|
-
mode: "dev" | "prod",
|
|
140
|
-
): { state: string; note: string } {
|
|
141
|
-
const config = readJsonFile<AgentProfilerConfig>(configPath);
|
|
142
|
-
const cursor = config?.adapters?.cursor;
|
|
143
|
-
if (!cursor?.enabled || !cursor.hookFile) {
|
|
144
|
-
return { state: "not yet", note: "run `agent-profiler init cursor`" };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const hooks = readJsonFile<CursorHooksConfig>(cursor.hookFile)?.hooks ?? {};
|
|
148
|
-
const missing = REQUIRED_CURSOR_EVENTS.filter((eventName) => !hookConfigured(hooks, eventName));
|
|
149
|
-
|
|
150
|
-
if (missing.length > 0) {
|
|
151
|
-
return {
|
|
152
|
-
state: "partial",
|
|
153
|
-
note: `missing hooks: ${missing.join(", ")}`,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const sampleCommand = hookEntryCommand(hooks.beforeSubmitPrompt);
|
|
158
|
-
if (mode === "dev") {
|
|
159
|
-
if (!sampleCommand.startsWith("node ")) {
|
|
160
|
-
return { state: "partial", note: "dev mode expects hooks to use `node <abs>/dist/cli.js ...`" };
|
|
161
|
-
}
|
|
162
|
-
const cliPath = sampleCommand.split(" ")[1];
|
|
163
|
-
if (!cliPath || !fs.existsSync(cliPath)) {
|
|
164
|
-
return { state: "partial", note: "dev hook CLI path is missing or invalid" };
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
if (!sampleCommand.startsWith("agent-profiler ")) {
|
|
168
|
-
return { state: "partial", note: "prod mode expects hooks to use `agent-profiler ...`" };
|
|
169
|
-
}
|
|
170
|
-
if (!commandExistsInPath("agent-profiler")) {
|
|
171
|
-
return { state: "partial", note: "`agent-profiler` is not on PATH" };
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return { state: "yes", note: "configured via init" };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function getCodexSetupStatus(
|
|
179
|
-
configPath: string,
|
|
180
|
-
mode: "dev" | "prod",
|
|
181
|
-
): { state: string; note: string } {
|
|
182
|
-
const config = readJsonFile<AgentProfilerConfig>(configPath);
|
|
183
|
-
const codex = config?.adapters?.codex;
|
|
184
|
-
if (!codex?.enabled || !codex.hookFile) {
|
|
185
|
-
return { state: "not yet", note: "run `agent-profiler init codex`" };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const data = readJsonFile<CodexHooksFile>(codex.hookFile);
|
|
189
|
-
const hooks = data?.hooks ?? {};
|
|
190
|
-
const missing = REQUIRED_CODEX_EVENTS.filter(
|
|
191
|
-
(eventName) =>
|
|
192
|
-
!codexEventConfigured(hooks, eventName, `hook codex ${eventName}`),
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
if (missing.length > 0) {
|
|
196
|
-
return {
|
|
197
|
-
state: "partial",
|
|
198
|
-
note: `missing hooks: ${missing.join(", ")}`,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const userPromptGroups = hooks.UserPromptSubmit ?? [];
|
|
203
|
-
let sampleCommand = "";
|
|
204
|
-
outer: for (const g of userPromptGroups) {
|
|
205
|
-
for (const h of g.hooks ?? []) {
|
|
206
|
-
if (typeof h.command === "string" && h.command.includes("hook codex")) {
|
|
207
|
-
sampleCommand = h.command;
|
|
208
|
-
break outer;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (!sampleCommand) {
|
|
214
|
-
return { state: "partial", note: "could not find agent-profiler command in Codex hooks" };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (mode === "dev") {
|
|
218
|
-
if (!sampleCommand.startsWith("node ")) {
|
|
219
|
-
return { state: "partial", note: "dev mode expects hooks to use `node <abs>/dist/cli.js ...`" };
|
|
220
|
-
}
|
|
221
|
-
const cliPath = sampleCommand.split(" ")[1];
|
|
222
|
-
if (!cliPath || !fs.existsSync(cliPath)) {
|
|
223
|
-
return { state: "partial", note: "dev hook CLI path is missing or invalid" };
|
|
224
|
-
}
|
|
225
|
-
} else {
|
|
226
|
-
if (!sampleCommand.startsWith("agent-profiler ")) {
|
|
227
|
-
return { state: "partial", note: "prod mode expects hooks to use `agent-profiler ...`" };
|
|
228
|
-
}
|
|
229
|
-
if (!commandExistsInPath("agent-profiler")) {
|
|
230
|
-
return { state: "partial", note: "`agent-profiler` is not on PATH" };
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return { state: "yes", note: "configured via init" };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function formatTimestamp(iso: string): string {
|
|
238
|
-
const date = new Date(iso);
|
|
239
|
-
if (Number.isNaN(date.getTime())) return iso;
|
|
240
|
-
return date.toLocaleString();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function formatCount(value: number): string {
|
|
244
|
-
return new Intl.NumberFormat("en-US").format(value);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export type StatusReport = {
|
|
248
|
-
mode: "dev" | "prod";
|
|
249
|
-
databasePath: string;
|
|
250
|
-
adapters: {
|
|
251
|
-
cursor: {
|
|
252
|
-
state: string;
|
|
253
|
-
setup: string;
|
|
254
|
-
};
|
|
255
|
-
codex: {
|
|
256
|
-
state: string;
|
|
257
|
-
setup: string;
|
|
258
|
-
};
|
|
259
|
-
};
|
|
260
|
-
lastEvent:
|
|
261
|
-
| {
|
|
262
|
-
createdAt: string;
|
|
263
|
-
source: string;
|
|
264
|
-
event: string;
|
|
265
|
-
estimatedTokens: number;
|
|
266
|
-
}
|
|
267
|
-
| null;
|
|
268
|
-
dashboard: "not running";
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
export function getStatusReport(mode: "dev" | "prod" = "dev"): StatusReport {
|
|
272
|
-
const { configuredDbPath, resolvedDbPath, configPath } = resolveStatusPaths(mode);
|
|
273
|
-
const cursorSetup = getCursorSetupStatus(configPath, mode);
|
|
274
|
-
const codexSetup = getCodexSetupStatus(configPath, mode);
|
|
275
|
-
|
|
276
|
-
let lastEvent = null;
|
|
277
|
-
try {
|
|
278
|
-
const db = openDb(configuredDbPath);
|
|
279
|
-
try {
|
|
280
|
-
lastEvent = getLastEventSummary(db);
|
|
281
|
-
} finally {
|
|
282
|
-
db.close();
|
|
283
|
-
}
|
|
284
|
-
} catch {
|
|
285
|
-
lastEvent = null;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return {
|
|
289
|
-
mode,
|
|
290
|
-
databasePath: resolvedDbPath,
|
|
291
|
-
adapters: {
|
|
292
|
-
cursor: {
|
|
293
|
-
state: cursorSetup.state,
|
|
294
|
-
setup: cursorSetup.note,
|
|
295
|
-
},
|
|
296
|
-
codex: {
|
|
297
|
-
state: codexSetup.state,
|
|
298
|
-
setup: codexSetup.note,
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
lastEvent: lastEvent
|
|
302
|
-
? {
|
|
303
|
-
createdAt: lastEvent.createdAt,
|
|
304
|
-
source: lastEvent.source,
|
|
305
|
-
event: lastEvent.sourceEvent,
|
|
306
|
-
estimatedTokens: lastEvent.estimatedTotalTokens,
|
|
307
|
-
}
|
|
308
|
-
: null,
|
|
309
|
-
dashboard: "not running",
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export function runStatus(mode: "dev" | "prod" = "dev"): void {
|
|
314
|
-
const report = getStatusReport(mode);
|
|
315
|
-
|
|
316
|
-
const lines: string[] = [];
|
|
317
|
-
lines.push("Agent Profiler Status");
|
|
318
|
-
lines.push("");
|
|
319
|
-
lines.push("Mode:");
|
|
320
|
-
lines.push(` ${report.mode}`);
|
|
321
|
-
lines.push("");
|
|
322
|
-
lines.push("Database:");
|
|
323
|
-
lines.push(` ${report.databasePath}`);
|
|
324
|
-
lines.push("");
|
|
325
|
-
lines.push("Configured adapters:");
|
|
326
|
-
lines.push(` Cursor: ${report.adapters.cursor.state}`);
|
|
327
|
-
lines.push(` setup: ${report.adapters.cursor.setup}`);
|
|
328
|
-
lines.push(` Codex: ${report.adapters.codex.state}`);
|
|
329
|
-
lines.push(` setup: ${report.adapters.codex.setup}`);
|
|
330
|
-
lines.push("");
|
|
331
|
-
lines.push("Last event:");
|
|
332
|
-
if (report.lastEvent) {
|
|
333
|
-
lines.push(` ${formatTimestamp(report.lastEvent.createdAt)}`);
|
|
334
|
-
lines.push(` source: ${report.lastEvent.source}`);
|
|
335
|
-
lines.push(` event: ${report.lastEvent.event}`);
|
|
336
|
-
lines.push(` estimated tokens: ${formatCount(report.lastEvent.estimatedTokens)}`);
|
|
337
|
-
} else {
|
|
338
|
-
lines.push(" none yet");
|
|
339
|
-
}
|
|
340
|
-
lines.push("");
|
|
341
|
-
lines.push("Dashboard:");
|
|
342
|
-
lines.push(" not running");
|
|
343
|
-
|
|
344
|
-
console.log(lines.join("\n"));
|
|
345
|
-
}
|
package/src/core/contextAudit.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { estimateTokens } from "./tokens.js";
|
|
4
|
-
|
|
5
|
-
export type ContextFileEstimate = {
|
|
6
|
-
path: string;
|
|
7
|
-
estimatedTokens: number;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export type ContextAuditResult = {
|
|
11
|
-
totalEstimatedTokens: number;
|
|
12
|
-
files: ContextFileEstimate[];
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const EXACT_FILES = [
|
|
16
|
-
"AGENTS.md",
|
|
17
|
-
"CLAUDE.md",
|
|
18
|
-
".cursorrules",
|
|
19
|
-
".codex/config.toml",
|
|
20
|
-
".codex/hooks.json",
|
|
21
|
-
".claude/settings.json",
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
const DIRS = [
|
|
25
|
-
".cursor/rules",
|
|
26
|
-
".cursor/skills",
|
|
27
|
-
".claude/commands",
|
|
28
|
-
".claude/agents",
|
|
29
|
-
".claude/skills",
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
function readTextFile(filePath: string): string {
|
|
33
|
-
try {
|
|
34
|
-
return fs.readFileSync(filePath, "utf8");
|
|
35
|
-
} catch {
|
|
36
|
-
return "";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function walkFiles(dirPath: string): string[] {
|
|
41
|
-
const out: string[] = [];
|
|
42
|
-
if (!fs.existsSync(dirPath)) return out;
|
|
43
|
-
|
|
44
|
-
const stack = [dirPath];
|
|
45
|
-
while (stack.length > 0) {
|
|
46
|
-
const current = stack.pop();
|
|
47
|
-
if (!current) continue;
|
|
48
|
-
|
|
49
|
-
let entries: fs.Dirent[] = [];
|
|
50
|
-
try {
|
|
51
|
-
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
52
|
-
} catch {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
for (const entry of entries) {
|
|
57
|
-
const fullPath = path.join(current, entry.name);
|
|
58
|
-
if (entry.isDirectory()) {
|
|
59
|
-
stack.push(fullPath);
|
|
60
|
-
} else if (entry.isFile()) {
|
|
61
|
-
out.push(fullPath);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function runContextAudit(rootDir = process.cwd()): ContextAuditResult {
|
|
70
|
-
const candidates = new Set<string>();
|
|
71
|
-
|
|
72
|
-
for (const relativePath of EXACT_FILES) {
|
|
73
|
-
const fullPath = path.join(rootDir, relativePath);
|
|
74
|
-
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
|
75
|
-
candidates.add(fullPath);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
for (const relativeDir of DIRS) {
|
|
80
|
-
const fullDir = path.join(rootDir, relativeDir);
|
|
81
|
-
for (const filePath of walkFiles(fullDir)) {
|
|
82
|
-
candidates.add(filePath);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const files: ContextFileEstimate[] = [];
|
|
87
|
-
for (const fullPath of candidates) {
|
|
88
|
-
const text = readTextFile(fullPath);
|
|
89
|
-
const estimatedTokens = estimateTokens(text);
|
|
90
|
-
if (estimatedTokens > 0) {
|
|
91
|
-
files.push({
|
|
92
|
-
path: path.relative(rootDir, fullPath) || fullPath,
|
|
93
|
-
estimatedTokens,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
files.sort((a, b) => b.estimatedTokens - a.estimatedTokens);
|
|
99
|
-
const totalEstimatedTokens = files.reduce((sum, file) => sum + file.estimatedTokens, 0);
|
|
100
|
-
|
|
101
|
-
return { totalEstimatedTokens, files };
|
|
102
|
-
}
|