agent-profiler 0.1.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/LICENSE +21 -0
- package/agent-profiler-0.1.0.tgz +0 -0
- package/dist/adapters/codex.js +111 -0
- package/dist/adapters/cursor.js +98 -0
- package/dist/cli.js +101 -0
- package/dist/commands/auditContext.js +46 -0
- package/dist/commands/hook.js +76 -0
- package/dist/commands/init.js +256 -0
- package/dist/commands/last.js +260 -0
- package/dist/commands/status.js +253 -0
- package/dist/core/contextAudit.js +83 -0
- package/dist/core/db.js +276 -0
- package/dist/core/eventMetadata.js +131 -0
- package/dist/core/gitWorkspace.js +74 -0
- package/dist/core/normalize.js +1 -0
- package/dist/core/profile.js +28 -0
- package/dist/core/schema.sql +56 -0
- package/dist/core/tokens.js +5 -0
- package/docs/agent-profiler-mvp-handoff.md +980 -0
- package/google-home.png +0 -0
- package/package.json +26 -0
- package/src/adapters/codex.ts +131 -0
- package/src/adapters/cursor.ts +115 -0
- package/src/cli.ts +109 -0
- package/src/commands/auditContext.ts +62 -0
- package/src/commands/hook.ts +104 -0
- package/src/commands/init.ts +324 -0
- package/src/commands/last.ts +326 -0
- package/src/commands/status.ts +345 -0
- package/src/core/contextAudit.ts +102 -0
- package/src/core/db.ts +491 -0
- package/src/core/eventMetadata.ts +184 -0
- package/src/core/gitWorkspace.ts +92 -0
- package/src/core/normalize.ts +29 -0
- package/src/core/profile.ts +35 -0
- package/src/core/schema.sql +56 -0
- package/src/core/tokens.ts +4 -0
- package/src/types/better-sqlite3.d.ts +26 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
return {};
|
|
7
|
+
}
|
|
8
|
+
function pickFirstString(values) {
|
|
9
|
+
for (const value of values) {
|
|
10
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
/** Split Codex-style `mcp__server__tool` or loose `MCP: server — tool`. */
|
|
17
|
+
export function parseMcpToolName(canonical) {
|
|
18
|
+
const t = canonical.trim();
|
|
19
|
+
if (!t)
|
|
20
|
+
return { mcpServer: null, mcpTool: null };
|
|
21
|
+
if (t.startsWith("mcp__")) {
|
|
22
|
+
const parts = t.split("__").filter(Boolean);
|
|
23
|
+
if (parts.length >= 3) {
|
|
24
|
+
return { mcpServer: parts[1] ?? null, mcpTool: parts.slice(2).join("__") || null };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (t.toLowerCase().startsWith("mcp:")) {
|
|
28
|
+
const rest = t.slice(4).trim();
|
|
29
|
+
const sep = rest.indexOf(":");
|
|
30
|
+
if (sep > 0) {
|
|
31
|
+
return {
|
|
32
|
+
mcpServer: rest.slice(0, sep).trim() || null,
|
|
33
|
+
mcpTool: rest.slice(sep + 1).trim() || null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { mcpServer: null, mcpTool: null };
|
|
38
|
+
}
|
|
39
|
+
function fingerprintPrompt(text) {
|
|
40
|
+
const trimmed = text.trim();
|
|
41
|
+
if (!trimmed)
|
|
42
|
+
return null;
|
|
43
|
+
return crypto.createHash("sha256").update(trimmed, "utf8").digest("hex");
|
|
44
|
+
}
|
|
45
|
+
function hookToInteractionKind(source, hookEventName) {
|
|
46
|
+
const cursorMap = {
|
|
47
|
+
beforeSubmitPrompt: { kind: "user_prompt_submit", toolPhase: null },
|
|
48
|
+
afterAgentResponse: { kind: "model_output", toolPhase: null },
|
|
49
|
+
afterAgentThought: { kind: "model_thought", toolPhase: null },
|
|
50
|
+
preToolUse: { kind: "tool_request", toolPhase: "pre" },
|
|
51
|
+
postToolUse: { kind: "tool_result_event", toolPhase: "post" },
|
|
52
|
+
postToolUseFailure: { kind: "tool_failure_event", toolPhase: "failure" },
|
|
53
|
+
beforeMCPExecution: { kind: "mcp_request", toolPhase: "pre" },
|
|
54
|
+
afterMCPExecution: { kind: "mcp_result_event", toolPhase: "post" },
|
|
55
|
+
beforeShellExecution: { kind: "shell_command_request", toolPhase: "pre" },
|
|
56
|
+
afterShellExecution: { kind: "shell_output", toolPhase: null },
|
|
57
|
+
afterFileEdit: { kind: "file_edit", toolPhase: null },
|
|
58
|
+
beforeReadFile: { kind: "file_read_request", toolPhase: "pre" },
|
|
59
|
+
start: { kind: "session_start", toolPhase: null },
|
|
60
|
+
sessionStart: { kind: "session_start", toolPhase: null },
|
|
61
|
+
stop: { kind: "session_stop", toolPhase: null },
|
|
62
|
+
sessionEnd: { kind: "session_end", toolPhase: null },
|
|
63
|
+
preCompact: { kind: "context_compact", toolPhase: null },
|
|
64
|
+
};
|
|
65
|
+
const codexMap = {
|
|
66
|
+
SessionStart: { kind: "session_start", toolPhase: null },
|
|
67
|
+
UserPromptSubmit: { kind: "user_prompt_submit", toolPhase: null },
|
|
68
|
+
PreToolUse: { kind: "tool_request", toolPhase: "pre" },
|
|
69
|
+
PostToolUse: { kind: "tool_result_event", toolPhase: "post" },
|
|
70
|
+
Stop: { kind: "session_stop", toolPhase: null },
|
|
71
|
+
};
|
|
72
|
+
const mapped = source === "codex" ? codexMap[hookEventName] : cursorMap[hookEventName];
|
|
73
|
+
if (mapped)
|
|
74
|
+
return mapped;
|
|
75
|
+
return { kind: `other:${hookEventName}`, toolPhase: null };
|
|
76
|
+
}
|
|
77
|
+
function extractCorrelationId(payload) {
|
|
78
|
+
return (pickFirstString([
|
|
79
|
+
payload.tool_use_id,
|
|
80
|
+
payload.toolUseId,
|
|
81
|
+
payload.toolCallId,
|
|
82
|
+
payload.callId,
|
|
83
|
+
payload.id,
|
|
84
|
+
payload.invocationId,
|
|
85
|
+
]) ?? null);
|
|
86
|
+
}
|
|
87
|
+
function extractToolCanonicalName(payload) {
|
|
88
|
+
const name = pickFirstString([
|
|
89
|
+
payload.tool_name,
|
|
90
|
+
payload.toolName,
|
|
91
|
+
payload.tool,
|
|
92
|
+
payload.name,
|
|
93
|
+
payload.type,
|
|
94
|
+
]) ?? null;
|
|
95
|
+
return name?.trim() || null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Derives query-friendly fields + MCP split + span phase for tool correlation.
|
|
99
|
+
*/
|
|
100
|
+
export function deriveIngestFields(source, hookEventName, rawPayload, rawJsonText, normalized) {
|
|
101
|
+
const { kind, toolPhase } = hookToInteractionKind(source, hookEventName);
|
|
102
|
+
const payload = asRecord(rawPayload);
|
|
103
|
+
const correlationId = extractCorrelationId(payload);
|
|
104
|
+
let toolCanonicalName = extractToolCanonicalName(payload);
|
|
105
|
+
if (!toolCanonicalName && payload.tool_input && typeof payload.tool_input === "object") {
|
|
106
|
+
const ti = payload.tool_input;
|
|
107
|
+
toolCanonicalName =
|
|
108
|
+
pickFirstString([ti.command, ti.tool, ti.name])?.trim() ||
|
|
109
|
+
toolCanonicalName;
|
|
110
|
+
}
|
|
111
|
+
const { mcpServer, mcpTool } = toolCanonicalName
|
|
112
|
+
? parseMcpToolName(toolCanonicalName)
|
|
113
|
+
: { mcpServer: null, mcpTool: null };
|
|
114
|
+
let promptFingerprint = null;
|
|
115
|
+
if (kind === "user_prompt_submit") {
|
|
116
|
+
const prompt = pickFirstString([payload.prompt, payload.text, payload.message]) ??
|
|
117
|
+
normalized.observableText;
|
|
118
|
+
promptFingerprint = fingerprintPrompt(prompt);
|
|
119
|
+
}
|
|
120
|
+
const payloadByteLength = Buffer.byteLength(rawJsonText, "utf8");
|
|
121
|
+
return {
|
|
122
|
+
interactionKind: kind,
|
|
123
|
+
correlationId,
|
|
124
|
+
toolCanonicalName,
|
|
125
|
+
mcpServer,
|
|
126
|
+
mcpTool,
|
|
127
|
+
payloadByteLength,
|
|
128
|
+
promptFingerprint,
|
|
129
|
+
toolPhase,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
function trimmedOrNull(value) {
|
|
4
|
+
if (typeof value !== "string")
|
|
5
|
+
return undefined;
|
|
6
|
+
const t = value.trim();
|
|
7
|
+
return t.length > 0 ? t : undefined;
|
|
8
|
+
}
|
|
9
|
+
function asRecord(value) {
|
|
10
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Absolute path for “where this event ran”: adapter repoPath, then common payload cwd keys, then process.cwd().
|
|
17
|
+
*/
|
|
18
|
+
export function resolveHookWorkspacePath(normalizedRepoPath, rawPayload) {
|
|
19
|
+
const payload = asRecord(rawPayload);
|
|
20
|
+
const candidates = [
|
|
21
|
+
trimmedOrNull(normalizedRepoPath),
|
|
22
|
+
trimmedOrNull(payload.cwd),
|
|
23
|
+
trimmedOrNull(payload.workspacePath),
|
|
24
|
+
trimmedOrNull(payload.workspace),
|
|
25
|
+
trimmedOrNull(payload.projectPath),
|
|
26
|
+
trimmedOrNull(payload.rootPath),
|
|
27
|
+
];
|
|
28
|
+
for (const c of candidates) {
|
|
29
|
+
if (!c)
|
|
30
|
+
continue;
|
|
31
|
+
return path.isAbsolute(c) ? path.normalize(c) : path.resolve(process.cwd(), c);
|
|
32
|
+
}
|
|
33
|
+
return path.resolve(process.cwd());
|
|
34
|
+
}
|
|
35
|
+
function gitOutput(workspacePath, args) {
|
|
36
|
+
try {
|
|
37
|
+
const r = spawnSync("git", ["-C", workspacePath, ...args], {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
timeout: 2500,
|
|
40
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
41
|
+
});
|
|
42
|
+
if (r.error || r.status !== 0)
|
|
43
|
+
return null;
|
|
44
|
+
const out = (r.stdout ?? "").trim();
|
|
45
|
+
return out.length > 0 ? out : null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Best-effort git context for `workspacePath`. Non-repo → git* fields null; `workspacePath` still set.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveWorkspaceGitMeta(workspacePath) {
|
|
55
|
+
const inside = gitOutput(workspacePath, ["rev-parse", "--is-inside-work-tree"]);
|
|
56
|
+
if (inside !== "true") {
|
|
57
|
+
return {
|
|
58
|
+
workspacePath,
|
|
59
|
+
gitRepoRoot: null,
|
|
60
|
+
gitRepoName: null,
|
|
61
|
+
gitBranch: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const root = gitOutput(workspacePath, ["rev-parse", "--show-toplevel"]);
|
|
65
|
+
const branch = gitOutput(workspacePath, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
66
|
+
const gitRepoRoot = root ? path.normalize(root) : null;
|
|
67
|
+
const gitRepoName = gitRepoRoot ? path.basename(gitRepoRoot) : null;
|
|
68
|
+
return {
|
|
69
|
+
workspacePath,
|
|
70
|
+
gitRepoRoot,
|
|
71
|
+
gitRepoName,
|
|
72
|
+
gitBranch: branch,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function readJsonFile(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function getLocalProfileDir(cwd = process.cwd()) {
|
|
13
|
+
return path.join(cwd, ".agent-profiler");
|
|
14
|
+
}
|
|
15
|
+
export function getHomeProfileDir() {
|
|
16
|
+
return path.join(os.homedir(), ".agent-profiler");
|
|
17
|
+
}
|
|
18
|
+
export function getPreferredConfigPath(cwd = process.cwd()) {
|
|
19
|
+
const localConfig = path.join(getLocalProfileDir(cwd), "config.json");
|
|
20
|
+
if (fs.existsSync(localConfig))
|
|
21
|
+
return localConfig;
|
|
22
|
+
return path.join(getHomeProfileDir(), "config.json");
|
|
23
|
+
}
|
|
24
|
+
export function getConfiguredDatabasePath(cwd = process.cwd()) {
|
|
25
|
+
const configPath = getPreferredConfigPath(cwd);
|
|
26
|
+
const config = readJsonFile(configPath);
|
|
27
|
+
return config?.databasePath ?? null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
2
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3
|
+
created_at TEXT NOT NULL,
|
|
4
|
+
source TEXT NOT NULL,
|
|
5
|
+
source_event TEXT NOT NULL,
|
|
6
|
+
repo_path TEXT,
|
|
7
|
+
session_id TEXT,
|
|
8
|
+
turn_id TEXT,
|
|
9
|
+
model TEXT,
|
|
10
|
+
role TEXT NOT NULL,
|
|
11
|
+
estimated_input_tokens INTEGER DEFAULT 0,
|
|
12
|
+
estimated_output_tokens INTEGER DEFAULT 0,
|
|
13
|
+
estimated_total_tokens INTEGER DEFAULT 0,
|
|
14
|
+
payload_hash TEXT NOT NULL,
|
|
15
|
+
raw_payload TEXT NOT NULL,
|
|
16
|
+
workspace_path TEXT,
|
|
17
|
+
git_repo_root TEXT,
|
|
18
|
+
git_repo_name TEXT,
|
|
19
|
+
git_branch TEXT,
|
|
20
|
+
interaction_kind TEXT,
|
|
21
|
+
correlation_id TEXT,
|
|
22
|
+
tool_canonical_name TEXT,
|
|
23
|
+
mcp_server TEXT,
|
|
24
|
+
mcp_tool TEXT,
|
|
25
|
+
payload_byte_length INTEGER,
|
|
26
|
+
prompt_fingerprint TEXT
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE TABLE IF NOT EXISTS interaction_spans (
|
|
30
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
31
|
+
session_key TEXT NOT NULL,
|
|
32
|
+
source TEXT NOT NULL,
|
|
33
|
+
correlation_id TEXT NOT NULL,
|
|
34
|
+
turn_id TEXT,
|
|
35
|
+
tool_canonical_name TEXT,
|
|
36
|
+
mcp_server TEXT,
|
|
37
|
+
mcp_tool TEXT,
|
|
38
|
+
pre_event_id INTEGER,
|
|
39
|
+
post_event_id INTEGER,
|
|
40
|
+
failure_event_id INTEGER,
|
|
41
|
+
arg_token_estimate INTEGER DEFAULT 0,
|
|
42
|
+
result_token_estimate INTEGER DEFAULT 0,
|
|
43
|
+
workspace_path TEXT,
|
|
44
|
+
git_repo_root TEXT,
|
|
45
|
+
git_repo_name TEXT,
|
|
46
|
+
git_branch TEXT,
|
|
47
|
+
started_at TEXT,
|
|
48
|
+
completed_at TEXT,
|
|
49
|
+
UNIQUE(session_key, source, correlation_id)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_events_session
|
|
53
|
+
ON events(session_id, created_at);
|
|
54
|
+
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_events_source
|
|
56
|
+
ON events(source, created_at);
|