@kennykeni/agent-trace 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/README.md +124 -0
- package/package.json +37 -0
- package/src/cli.ts +119 -0
- package/src/core/schemas.ts +138 -0
- package/src/core/trace-hook.ts +230 -0
- package/src/core/trace-store.ts +170 -0
- package/src/core/types.ts +93 -0
- package/src/core/utils.ts +85 -0
- package/src/extensions/diffs.ts +89 -0
- package/src/extensions/helpers.ts +19 -0
- package/src/extensions/index.ts +16 -0
- package/src/extensions/line-hashes.ts +67 -0
- package/src/extensions/messages.ts +48 -0
- package/src/extensions/raw-events.ts +30 -0
- package/src/install/args.ts +71 -0
- package/src/install/claude.ts +78 -0
- package/src/install/cursor.ts +50 -0
- package/src/install/index.ts +49 -0
- package/src/install/opencode.ts +18 -0
- package/src/install/templates/opencode-plugin.ts +207 -0
- package/src/install/types.ts +14 -0
- package/src/install/utils.ts +78 -0
- package/src/providers/claude.ts +130 -0
- package/src/providers/cursor.ts +127 -0
- package/src/providers/index.ts +19 -0
- package/src/providers/opencode.ts +239 -0
- package/src/providers/types.ts +6 -0
- package/src/providers/utils.ts +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# agent-trace
|
|
2
|
+
|
|
3
|
+
A TypeScript implementation of the [Agent Trace specification](https://agent-trace.org) ([GitHub](https://github.com/cursor/agent-trace)) for capturing AI code contributions from local coding tools.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
agent-trace hooks into AI coding tools and records trace data as they edit files. Each trace captures what was changed, which model was used, and links back to the conversation that produced the code. Traces are written as JSONL to `.agent-trace/traces.jsonl` in the project root.
|
|
8
|
+
|
|
9
|
+
Supported providers:
|
|
10
|
+
|
|
11
|
+
- Cursor
|
|
12
|
+
- Claude Code
|
|
13
|
+
- OpenCode
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Requires [Bun](https://bun.sh).
|
|
18
|
+
|
|
19
|
+
Install hooks in the current git repository:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bunx @kennykeni/agent-trace init
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Install for specific providers:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bunx @kennykeni/agent-trace init --providers cursor
|
|
29
|
+
bunx @kennykeni/agent-trace init --providers claude,opencode
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Install for a specific project:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bunx @kennykeni/agent-trace init --target-root ~/my-project
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Preview what would be written:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bunx @kennykeni/agent-trace init --dry-run
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Check installation status:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bunx @kennykeni/agent-trace status
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## What `init` does
|
|
51
|
+
|
|
52
|
+
Configures the target repo's provider settings:
|
|
53
|
+
|
|
54
|
+
| Provider | Config written |
|
|
55
|
+
| ---------- | ----------------------------------------- |
|
|
56
|
+
| Cursor | `.cursor/hooks.json` |
|
|
57
|
+
| Claude Code| `.claude/settings.json` |
|
|
58
|
+
| OpenCode | `.opencode/plugins/agent-trace.ts` |
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
1. Provider hooks fire on tool events (file edits, shell commands, session lifecycle).
|
|
63
|
+
2. The hook receives event JSON on stdin and routes it through a provider adapter.
|
|
64
|
+
3. The adapter normalizes provider-specific payloads into internal trace events.
|
|
65
|
+
4. The trace pipeline converts events into spec-compliant trace records.
|
|
66
|
+
5. Records are appended to `.agent-trace/traces.jsonl`.
|
|
67
|
+
|
|
68
|
+
Additional artifacts are written by extensions under `.agent-trace/`:
|
|
69
|
+
|
|
70
|
+
- `raw/<provider>/<session>.jsonl` -- raw hook events (`raw-events` extension)
|
|
71
|
+
- `messages/<provider>/<session>.jsonl` -- captured chat messages (`messages` extension)
|
|
72
|
+
- `diffs/<provider>/<session>.patch` -- diff artifacts when available (`diffs` extension)
|
|
73
|
+
- `line-hashes/<provider>/<session>.jsonl` -- per-line content hashes (`line-hashes` extension)
|
|
74
|
+
|
|
75
|
+
## Extensions
|
|
76
|
+
|
|
77
|
+
Extensions are pluggable modules that run alongside the core trace pipeline. Four are built in: `raw-events`, `diffs`, `messages`, and `line-hashes`. All extensions are enabled by default.
|
|
78
|
+
|
|
79
|
+
To control which extensions run, create `.agent-trace/config.json` in your project root:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "extensions": ["diffs", "messages"] }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- **File absent** -- all registered extensions run (default)
|
|
86
|
+
- **`"extensions": ["diffs", "messages"]`** -- only listed extensions run
|
|
87
|
+
- **`"extensions": []`** -- no extensions run (only `traces.jsonl` is written)
|
|
88
|
+
- **Malformed JSON** -- warning logged, all extensions run
|
|
89
|
+
|
|
90
|
+
## Trace format
|
|
91
|
+
|
|
92
|
+
Traces follow the [Agent Trace spec](https://agent-trace.org). Each JSONL line contains:
|
|
93
|
+
|
|
94
|
+
- `version` -- spec version
|
|
95
|
+
- `id` -- unique trace ID (UUID)
|
|
96
|
+
- `timestamp` -- ISO 8601
|
|
97
|
+
- `vcs` -- version control info (type, revision)
|
|
98
|
+
- `tool` -- tool name and version
|
|
99
|
+
- `files[]` -- files with conversation and range attribution
|
|
100
|
+
- `metadata` -- optional implementation-specific data
|
|
101
|
+
|
|
102
|
+
Schema source: [`schemas.ts`](./src/core/schemas.ts)
|
|
103
|
+
|
|
104
|
+
## Known limitations
|
|
105
|
+
|
|
106
|
+
- **Path normalization edge cases**: Relative path conversion is string-prefix based today; some sibling-path cases can produce `..` segments.
|
|
107
|
+
- **Bun-only**: The hook runtime and CLI require Bun. Node.js is not supported.
|
|
108
|
+
- **No VCS requirement**: Works without git. When git is available, traces include the current commit SHA. Without git, VCS info is omitted.
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
bun install
|
|
114
|
+
bun test
|
|
115
|
+
bun run check
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Spec reference
|
|
119
|
+
|
|
120
|
+
This project implements the [Agent Trace specification](https://agent-trace.org) authored by [Cursor](https://github.com/cursor/agent-trace). The spec defines a vendor-neutral format for recording AI contributions alongside human authorship in version-controlled codebases.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
See the [Agent Trace spec](https://github.com/cursor/agent-trace) for specification licensing (CC BY 4.0).
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kennykeni/agent-trace",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agent-trace": "src/cli.ts"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"src/",
|
|
10
|
+
"!src/**/__tests__/"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/KennyKeni/agent-trace.git"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "bun run index.ts",
|
|
18
|
+
"dev": "bun run index.ts --dev",
|
|
19
|
+
"check": "tsc --noEmit && biome check .",
|
|
20
|
+
"check:fix": "biome check --write .",
|
|
21
|
+
"format": "biome format --write .",
|
|
22
|
+
"lint": "biome lint .",
|
|
23
|
+
"lint:fix": "biome lint --write .",
|
|
24
|
+
"test": "bun test"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"zod": "^3.24.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@biomejs/biome": "^2.3.14",
|
|
31
|
+
"@types/bun": "latest",
|
|
32
|
+
"marked": "^15.0.0",
|
|
33
|
+
"marked-shiki": "^1.2.0",
|
|
34
|
+
"shiki": "^3.0.0",
|
|
35
|
+
"zod-to-json-schema": "^3.24.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { runHook } from "./core/trace-hook";
|
|
6
|
+
import "./extensions";
|
|
7
|
+
import "./providers";
|
|
8
|
+
import { getWorkspaceRoot } from "./core/trace-store";
|
|
9
|
+
import {
|
|
10
|
+
InstallError,
|
|
11
|
+
install,
|
|
12
|
+
parseArgs,
|
|
13
|
+
printInstallSummary,
|
|
14
|
+
} from "./install";
|
|
15
|
+
import { getPackageVersion } from "./install/utils";
|
|
16
|
+
|
|
17
|
+
function printHelp(): void {
|
|
18
|
+
console.log(`agent-trace - AI code attribution tracker
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
agent-trace <command> [options]
|
|
22
|
+
|
|
23
|
+
Commands:
|
|
24
|
+
init Initialize hooks for Cursor, Claude Code, and OpenCode in a project
|
|
25
|
+
hook Run the trace hook (reads JSON from stdin)
|
|
26
|
+
status Show installed hook status
|
|
27
|
+
help Show this help message
|
|
28
|
+
|
|
29
|
+
Init options:
|
|
30
|
+
--providers <list> Comma-separated providers (cursor,claude,opencode) [default: all]
|
|
31
|
+
--target-root <dir> Target project root [default: current directory]
|
|
32
|
+
--dry-run Preview changes without writing
|
|
33
|
+
--latest Use latest version instead of pinning to current
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
agent-trace init
|
|
37
|
+
agent-trace init --providers cursor
|
|
38
|
+
agent-trace init --providers opencode
|
|
39
|
+
agent-trace init --target-root ~/my-project
|
|
40
|
+
agent-trace status`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function checkHookConfig(
|
|
44
|
+
path: string,
|
|
45
|
+
searchString: string,
|
|
46
|
+
): "installed" | "not installed" {
|
|
47
|
+
if (!existsSync(path)) return "not installed";
|
|
48
|
+
try {
|
|
49
|
+
const content = readFileSync(path, "utf-8");
|
|
50
|
+
return content.includes(searchString) ? "installed" : "not installed";
|
|
51
|
+
} catch {
|
|
52
|
+
return "not installed";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function status(): void {
|
|
57
|
+
const root = getWorkspaceRoot();
|
|
58
|
+
|
|
59
|
+
const cursorPath = join(root, ".cursor", "hooks.json");
|
|
60
|
+
const claudePath = join(root, ".claude", "settings.json");
|
|
61
|
+
const opencodePath = join(root, ".opencode", "plugins", "agent-trace.ts");
|
|
62
|
+
|
|
63
|
+
const cursorStatus = checkHookConfig(
|
|
64
|
+
cursorPath,
|
|
65
|
+
"agent-trace hook --provider cursor",
|
|
66
|
+
);
|
|
67
|
+
const claudeStatus = checkHookConfig(
|
|
68
|
+
claudePath,
|
|
69
|
+
"agent-trace hook --provider claude",
|
|
70
|
+
);
|
|
71
|
+
const opencodeStatus = checkHookConfig(opencodePath, "agent-trace");
|
|
72
|
+
const traceDir = join(root, ".agent-trace");
|
|
73
|
+
const hasTraces = existsSync(join(traceDir, "traces.jsonl"));
|
|
74
|
+
|
|
75
|
+
console.log(`Workspace: ${root}\n`);
|
|
76
|
+
console.log(`Cursor: ${cursorStatus}`);
|
|
77
|
+
console.log(`Claude: ${claudeStatus}`);
|
|
78
|
+
console.log(`OpenCode: ${opencodeStatus}`);
|
|
79
|
+
console.log(`Traces: ${hasTraces ? "present" : "none"}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const command = process.argv[2];
|
|
83
|
+
|
|
84
|
+
switch (command) {
|
|
85
|
+
case "init": {
|
|
86
|
+
try {
|
|
87
|
+
const options = parseArgs(process.argv.slice(3));
|
|
88
|
+
const changes = install(options);
|
|
89
|
+
printInstallSummary(changes, options.targetRoots);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (e instanceof InstallError) {
|
|
92
|
+
console.error(e.message);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "hook":
|
|
100
|
+
await runHook();
|
|
101
|
+
break;
|
|
102
|
+
case "status":
|
|
103
|
+
status();
|
|
104
|
+
break;
|
|
105
|
+
case "--version":
|
|
106
|
+
case "-v":
|
|
107
|
+
console.log(getPackageVersion());
|
|
108
|
+
break;
|
|
109
|
+
case "help":
|
|
110
|
+
case "--help":
|
|
111
|
+
case "-h":
|
|
112
|
+
case undefined:
|
|
113
|
+
printHelp();
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
console.error(`Unknown command: ${command}`);
|
|
117
|
+
printHelp();
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
3
|
+
|
|
4
|
+
export const SPEC_VERSION = "0.1.0";
|
|
5
|
+
|
|
6
|
+
const SCHEMA_BASE_URL = "https://agent-trace.dev/schemas/v1";
|
|
7
|
+
|
|
8
|
+
export const ContributorTypeSchema = z.enum([
|
|
9
|
+
"human",
|
|
10
|
+
"ai",
|
|
11
|
+
"mixed",
|
|
12
|
+
"unknown",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export const ToolSchema = z.object({
|
|
16
|
+
name: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Name of the tool that produced the code"),
|
|
20
|
+
version: z.string().optional().describe("Version of the tool"),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const ContributorSchema = z.object({
|
|
24
|
+
type: ContributorTypeSchema.describe("The type of contributor"),
|
|
25
|
+
model_id: z
|
|
26
|
+
.string()
|
|
27
|
+
.max(250)
|
|
28
|
+
.optional()
|
|
29
|
+
.describe(
|
|
30
|
+
"The model's unique identifier following models.dev convention (e.g., 'anthropic/claude-opus-4-5-20251101')",
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const RelatedResourceSchema = z.object({
|
|
35
|
+
type: z.string().describe("Type of related resource"),
|
|
36
|
+
url: z.string().url().describe("URL to the related resource"),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const RangeSchema = z.object({
|
|
40
|
+
start_line: z.number().int().min(1).describe("1-indexed start line number"),
|
|
41
|
+
end_line: z.number().int().min(1).describe("1-indexed end line number"),
|
|
42
|
+
content_hash: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Hash of attributed content for position-independent tracking"),
|
|
46
|
+
contributor: ContributorSchema.optional().describe(
|
|
47
|
+
"Override contributor for this specific range (e.g., for agent handoffs)",
|
|
48
|
+
),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const ConversationSchema = z.object({
|
|
52
|
+
url: z
|
|
53
|
+
.string()
|
|
54
|
+
.url()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("URL to look up the conversation that produced this code"),
|
|
57
|
+
contributor: ContributorSchema.optional().describe(
|
|
58
|
+
"The contributor for ranges in this conversation (can be overridden per-range)",
|
|
59
|
+
),
|
|
60
|
+
ranges: z
|
|
61
|
+
.array(RangeSchema)
|
|
62
|
+
.describe("Array of line ranges produced by this conversation"),
|
|
63
|
+
related: z
|
|
64
|
+
.array(RelatedResourceSchema)
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Other related resources"),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const FileSchema = z.object({
|
|
70
|
+
path: z.string().describe("Relative file path from repository root"),
|
|
71
|
+
conversations: z
|
|
72
|
+
.array(ConversationSchema)
|
|
73
|
+
.describe("Array of conversations that contributed to this file"),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const VcsTypeSchema = z.enum(["git", "jj", "hg", "svn"]);
|
|
77
|
+
|
|
78
|
+
export const VcsSchema = z.object({
|
|
79
|
+
type: VcsTypeSchema.describe(
|
|
80
|
+
"Version control system type (e.g., 'git', 'jj', 'hg')",
|
|
81
|
+
),
|
|
82
|
+
revision: z
|
|
83
|
+
.string()
|
|
84
|
+
.describe(
|
|
85
|
+
"Revision identifier (e.g., git commit SHA, jj change ID, hg changeset)",
|
|
86
|
+
),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export const TraceRecordSchema = z.object({
|
|
90
|
+
version: z
|
|
91
|
+
.string()
|
|
92
|
+
.regex(/^[0-9]+\.[0-9]+\.[0-9]+$/)
|
|
93
|
+
.describe("Agent Trace specification version (e.g., '0.1.0')"),
|
|
94
|
+
id: z.string().uuid().describe("Unique identifier for this trace record"),
|
|
95
|
+
timestamp: z
|
|
96
|
+
.string()
|
|
97
|
+
.datetime({ offset: true })
|
|
98
|
+
.describe("RFC 3339 timestamp when trace was recorded"),
|
|
99
|
+
vcs: VcsSchema.optional().describe(
|
|
100
|
+
"Version control system information for this trace",
|
|
101
|
+
),
|
|
102
|
+
tool: ToolSchema.optional().describe("The tool that generated this trace"),
|
|
103
|
+
files: z.array(FileSchema).describe("Array of files with attributed ranges"),
|
|
104
|
+
metadata: z
|
|
105
|
+
.record(z.string(), z.unknown())
|
|
106
|
+
.optional()
|
|
107
|
+
.describe(
|
|
108
|
+
"Additional metadata for implementation-specific or vendor-specific data",
|
|
109
|
+
),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export function generateJsonSchemas(): Record<string, object> {
|
|
113
|
+
const schemas: Record<string, object> = {};
|
|
114
|
+
|
|
115
|
+
// Trace Record Schema
|
|
116
|
+
schemas["trace-record.json"] = {
|
|
117
|
+
...zodToJsonSchema(TraceRecordSchema, {
|
|
118
|
+
name: "TraceRecord",
|
|
119
|
+
$refStrategy: "none",
|
|
120
|
+
}),
|
|
121
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
122
|
+
$id: `${SCHEMA_BASE_URL}/trace-record.json`,
|
|
123
|
+
title: "Agent Trace Record",
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return schemas;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type ContributorType = z.infer<typeof ContributorTypeSchema>;
|
|
130
|
+
export type VcsType = z.infer<typeof VcsTypeSchema>;
|
|
131
|
+
export type Vcs = z.infer<typeof VcsSchema>;
|
|
132
|
+
export type Tool = z.infer<typeof ToolSchema>;
|
|
133
|
+
export type Contributor = z.infer<typeof ContributorSchema>;
|
|
134
|
+
export type Range = z.infer<typeof RangeSchema>;
|
|
135
|
+
export type Conversation = z.infer<typeof ConversationSchema>;
|
|
136
|
+
export type File = z.infer<typeof FileSchema>;
|
|
137
|
+
export type RelatedResource = z.infer<typeof RelatedResourceSchema>;
|
|
138
|
+
export type TraceRecord = z.infer<typeof TraceRecordSchema>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
appendTrace,
|
|
7
|
+
computeRangePositions,
|
|
8
|
+
createTrace,
|
|
9
|
+
getWorkspaceRoot,
|
|
10
|
+
tryReadFile,
|
|
11
|
+
} from "./trace-store";
|
|
12
|
+
import type {
|
|
13
|
+
Extension,
|
|
14
|
+
HookInput,
|
|
15
|
+
ProviderAdapter,
|
|
16
|
+
TraceEvent,
|
|
17
|
+
} from "./types";
|
|
18
|
+
|
|
19
|
+
export type { HookInput } from "./types";
|
|
20
|
+
|
|
21
|
+
const providerRegistry = new Map<string, ProviderAdapter>();
|
|
22
|
+
|
|
23
|
+
export function registerProvider(name: string, adapter: ProviderAdapter): void {
|
|
24
|
+
providerRegistry.set(name, adapter);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const extensionRegistry = new Map<string, Extension>();
|
|
28
|
+
|
|
29
|
+
export function registerExtension(ext: Extension): void {
|
|
30
|
+
extensionRegistry.set(ext.name, ext);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseProvider(argv: string[]): string | undefined {
|
|
34
|
+
for (let i = 0; i < argv.length; i++) {
|
|
35
|
+
const arg = argv[i];
|
|
36
|
+
if (arg === "--provider") {
|
|
37
|
+
return argv[i + 1] ?? undefined;
|
|
38
|
+
}
|
|
39
|
+
if (arg?.startsWith("--provider=")) {
|
|
40
|
+
return arg.slice("--provider=".length) || undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadExtensionConfig(root: string): string[] | null {
|
|
47
|
+
const configPath = join(root, ".agent-trace", "config.json");
|
|
48
|
+
let raw: string;
|
|
49
|
+
try {
|
|
50
|
+
raw = readFileSync(configPath, "utf-8");
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
if (
|
|
57
|
+
parsed &&
|
|
58
|
+
typeof parsed === "object" &&
|
|
59
|
+
Array.isArray(parsed.extensions)
|
|
60
|
+
) {
|
|
61
|
+
return parsed.extensions.filter(
|
|
62
|
+
(e: unknown): e is string => typeof e === "string",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
console.error(
|
|
66
|
+
"agent-trace: config.json missing 'extensions' array, running all extensions",
|
|
67
|
+
);
|
|
68
|
+
return null;
|
|
69
|
+
} catch {
|
|
70
|
+
console.error("agent-trace: malformed config.json, running all extensions");
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function activeExtensions(root: string): Extension[] {
|
|
76
|
+
const allowlist = loadExtensionConfig(root);
|
|
77
|
+
if (allowlist === null) return [...extensionRegistry.values()];
|
|
78
|
+
|
|
79
|
+
const active: Extension[] = [];
|
|
80
|
+
for (const name of allowlist) {
|
|
81
|
+
const ext = extensionRegistry.get(name);
|
|
82
|
+
if (ext) {
|
|
83
|
+
active.push(ext);
|
|
84
|
+
} else {
|
|
85
|
+
console.error(`agent-trace: unknown extension "${name}", skipping`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return active;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeTrace(event: TraceEvent): void {
|
|
92
|
+
const appendIfTrace = (trace: ReturnType<typeof createTrace>): void => {
|
|
93
|
+
if (trace) appendTrace(trace);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
switch (event.kind) {
|
|
97
|
+
case "file_edit": {
|
|
98
|
+
const edits = event.edits;
|
|
99
|
+
const fileContent = event.readContent
|
|
100
|
+
? tryReadFile(event.filePath)
|
|
101
|
+
: undefined;
|
|
102
|
+
const rangePositions = edits.length
|
|
103
|
+
? computeRangePositions(edits, fileContent)
|
|
104
|
+
: undefined;
|
|
105
|
+
|
|
106
|
+
appendIfTrace(
|
|
107
|
+
createTrace("ai", event.filePath, {
|
|
108
|
+
model: event.model,
|
|
109
|
+
rangePositions,
|
|
110
|
+
transcript: event.transcript,
|
|
111
|
+
tool: event.tool,
|
|
112
|
+
metadata: event.meta,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "shell": {
|
|
118
|
+
appendIfTrace(
|
|
119
|
+
createTrace("ai", ".shell-history", {
|
|
120
|
+
model: event.model,
|
|
121
|
+
transcript: event.transcript,
|
|
122
|
+
tool: event.tool,
|
|
123
|
+
metadata: event.meta,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case "session_start": {
|
|
129
|
+
appendIfTrace(
|
|
130
|
+
createTrace("ai", ".sessions", {
|
|
131
|
+
model: event.model,
|
|
132
|
+
tool: event.tool,
|
|
133
|
+
metadata: { event: "session_start", ...event.meta },
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case "session_end": {
|
|
139
|
+
appendIfTrace(
|
|
140
|
+
createTrace("ai", ".sessions", {
|
|
141
|
+
model: event.model,
|
|
142
|
+
tool: event.tool,
|
|
143
|
+
metadata: { event: "session_end", ...event.meta },
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case "message":
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function runHook() {
|
|
154
|
+
if (providerRegistry.size === 0) {
|
|
155
|
+
console.error(
|
|
156
|
+
'No providers registered. Import provider registrations before calling runHook() (e.g. import "./providers").',
|
|
157
|
+
);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const chunks: Buffer[] = [];
|
|
162
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
163
|
+
chunks.push(Buffer.from(chunk));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const json = Buffer.concat(chunks).toString("utf-8").trim();
|
|
167
|
+
if (!json) process.exit(0);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const providerName = parseProvider(process.argv.slice(2));
|
|
171
|
+
if (!providerName) {
|
|
172
|
+
const registered = [...providerRegistry.keys()].join(", ");
|
|
173
|
+
console.error(
|
|
174
|
+
`Missing --provider flag. Registered providers: ${registered}`,
|
|
175
|
+
);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const adapter = providerRegistry.get(providerName);
|
|
180
|
+
if (!adapter) {
|
|
181
|
+
const registered = [...providerRegistry.keys()].join(", ");
|
|
182
|
+
console.error(
|
|
183
|
+
`Unknown provider "${providerName}". Registered providers: ${registered}`,
|
|
184
|
+
);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const input = JSON.parse(json) as HookInput;
|
|
189
|
+
|
|
190
|
+
process.env.AGENT_TRACE_PROVIDER = providerName;
|
|
191
|
+
|
|
192
|
+
const sessionId = adapter.sessionIdFor(input);
|
|
193
|
+
const extensions = activeExtensions(getWorkspaceRoot());
|
|
194
|
+
|
|
195
|
+
for (const ext of extensions) {
|
|
196
|
+
if (ext.onRawInput) {
|
|
197
|
+
try {
|
|
198
|
+
ext.onRawInput(providerName, sessionId, input);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
console.error(`Extension error (${ext.name}/onRawInput):`, e);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const toolInfo = adapter.toolInfo?.();
|
|
206
|
+
|
|
207
|
+
const result = adapter.adapt(input);
|
|
208
|
+
if (result) {
|
|
209
|
+
const events = Array.isArray(result) ? result : [result];
|
|
210
|
+
for (const raw of events) {
|
|
211
|
+
const event = toolInfo ? { ...raw, tool: toolInfo } : raw;
|
|
212
|
+
writeTrace(event);
|
|
213
|
+
for (const ext of extensions) {
|
|
214
|
+
if (ext.onTraceEvent) {
|
|
215
|
+
try {
|
|
216
|
+
ext.onTraceEvent(event);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.error(`Extension error (${ext.name}/onTraceEvent):`, e);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
console.error("Hook error:", e);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (import.meta.main) void runHook();
|