@raquezha/notrace 0.0.5 → 0.0.7
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/CHANGELOG.md +16 -0
- package/README.md +122 -52
- package/assets/notrace-logo.svg +20 -0
- package/assets/notrace-mark.svg +18 -0
- package/assets/notrace-wordmark.svg +4 -0
- package/bin/notrace-compare.mjs +153 -0
- package/bin/notrace-review.mjs +123 -0
- package/dist/notrace/adapters.d.ts +32 -0
- package/dist/notrace/adapters.js +83 -0
- package/dist/notrace/index.d.ts +2 -0
- package/dist/notrace/index.js +335 -0
- package/dist/notrace/renderer.d.ts +4 -0
- package/dist/notrace/renderer.js +681 -0
- package/dist/notrace/types.d.ts +94 -0
- package/dist/notrace/types.js +1 -0
- package/extensions/notrace/adapters.ts +88 -0
- package/extensions/notrace/index.ts +393 -0
- package/extensions/notrace/renderer.ts +694 -0
- package/extensions/notrace/types.ts +109 -0
- package/package.json +10 -3
- package/templates/README.md +24 -0
- package/templates/dashboard.sample.html +399 -0
- package/templates/dashboard.sample.json +113 -0
- package/templates/notrace-logo.preview.png +0 -0
- package/templates/render-samples.mjs +44 -0
- package/templates/session.sample.html +499 -0
- package/templates/session.sample.json +127 -0
- package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.html +313 -0
- package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.json +129 -0
- package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.html +313 -0
- package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.json +129 -0
- package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html +494 -0
- package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.json +134 -0
- package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html +493 -0
- package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.json +133 -0
- package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html +494 -0
- package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.json +134 -0
- package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.json +130 -0
- package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.html +313 -0
- package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.json +129 -0
- package/tsconfig.json +1 -1
- package/dist/notrace.d.ts +0 -9
- package/dist/notrace.js +0 -914
- package/extensions/notrace.ts +0 -965
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
function appendWorkLogEntry(taskDir, message) {
|
|
4
|
+
const workMd = path.join(taskDir, "WORK.md");
|
|
5
|
+
if (!existsSync(workMd))
|
|
6
|
+
return;
|
|
7
|
+
try {
|
|
8
|
+
const text = readFileSync(workMd, "utf-8");
|
|
9
|
+
const entry = `- ${new Date().toISOString()}: ${message}`;
|
|
10
|
+
if (!/^(## )?\[LOG\]\s*$/m.test(text)) {
|
|
11
|
+
writeFileSync(workMd, `${text.trimEnd()}\n\n## [LOG]\n${entry}\n`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const lines = text.split("\n");
|
|
15
|
+
const logIndex = lines.findIndex(l => /^(## )?\[LOG\]\s*$/.test(l));
|
|
16
|
+
let nextSection = lines.length;
|
|
17
|
+
for (let i = logIndex + 1; i < lines.length; i++) {
|
|
18
|
+
if (/^(## )?\[[A-Z0-9_-]+\]\s*$/.test(lines[i])) {
|
|
19
|
+
nextSection = i;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const before = lines.slice(0, nextSection);
|
|
24
|
+
const after = lines.slice(nextSection);
|
|
25
|
+
before.push(entry);
|
|
26
|
+
writeFileSync(workMd, `${before.join("\n")}\n${after.join("\n")}`);
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
}
|
|
30
|
+
export class NorpivAdapter {
|
|
31
|
+
name = "norpiv";
|
|
32
|
+
detect(cwd) {
|
|
33
|
+
return existsSync(path.join(cwd, ".workflow", "active_task.json"));
|
|
34
|
+
}
|
|
35
|
+
getContext(cwd) {
|
|
36
|
+
try {
|
|
37
|
+
const workflowDir = path.join(cwd, ".workflow");
|
|
38
|
+
const content = JSON.parse(readFileSync(path.join(workflowDir, "active_task.json"), "utf-8"));
|
|
39
|
+
const taskPath = content.taskPath || (content.active_task ? path.join("tasks", content.active_task) : null);
|
|
40
|
+
return {
|
|
41
|
+
workflow: this.name,
|
|
42
|
+
taskId: content.active_task || "unknown",
|
|
43
|
+
taskPath,
|
|
44
|
+
taskDir: taskPath ? path.resolve(cwd, taskPath) : null
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
attach(context, artifacts) {
|
|
52
|
+
if (!context.taskDir)
|
|
53
|
+
return;
|
|
54
|
+
appendWorkLogEntry(context.taskDir, `notrace retrospective: ${artifacts.html}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export class ResearchAdapter {
|
|
58
|
+
name = "research";
|
|
59
|
+
detect(cwd) {
|
|
60
|
+
return existsSync(path.join(cwd, ".git")) && !existsSync(path.join(cwd, ".workflow", "active_task.json"));
|
|
61
|
+
}
|
|
62
|
+
getContext(cwd) {
|
|
63
|
+
try {
|
|
64
|
+
const head = readFileSync(path.join(cwd, ".git", "HEAD"), "utf-8");
|
|
65
|
+
const branch = head.split("refs/heads/")[1]?.trim() || "main";
|
|
66
|
+
return { workflow: this.name, taskId: `branch:${branch}`, taskPath: null, taskDir: null };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
attach() { }
|
|
73
|
+
}
|
|
74
|
+
export class GenericAdapter {
|
|
75
|
+
name = "generic";
|
|
76
|
+
detect() { return true; }
|
|
77
|
+
getContext() { return null; }
|
|
78
|
+
attach() { }
|
|
79
|
+
}
|
|
80
|
+
const ADAPTERS = [new NorpivAdapter(), new ResearchAdapter(), new GenericAdapter()];
|
|
81
|
+
export function getActiveAdapter(cwd) {
|
|
82
|
+
return ADAPTERS.find(a => a.detect(cwd)) || new GenericAdapter();
|
|
83
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, chmodSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { getActiveAdapter } from "./adapters.js";
|
|
5
|
+
import { generateHtmlReport, generateDashboardHtml } from "./renderer.js";
|
|
6
|
+
const REDACTED = "[REDACTED by notrace]";
|
|
7
|
+
const SENSITIVE_VALUE_RE = /(bearer\s+[a-z0-9._~+/=-]{12,}|sk-[a-z0-9_-]{16,}|gh[pousr]_[a-z0-9_]{16,}|AKIA[0-9A-Z]{16})/gi;
|
|
8
|
+
const TELEMETRY_CHANNEL = "notrace.telemetry.extension";
|
|
9
|
+
const SCHEMA_VERSION = 2;
|
|
10
|
+
let currentMode = "full";
|
|
11
|
+
function getInitialMode() {
|
|
12
|
+
const env = process.env.NOTRACE_CAPTURE?.toLowerCase();
|
|
13
|
+
if (env === "metadata" || env === "redacted")
|
|
14
|
+
return env;
|
|
15
|
+
return "full";
|
|
16
|
+
}
|
|
17
|
+
const SENSITIVE_KEY_RE = /(authorization|cookie|setcookie|password|passwd|pwd|secret|token|apikey|accesskey|accesskeyid|accessid|accesstoken|privatekey|session|credential|refreshtoken|idtoken)/i;
|
|
18
|
+
function isSensitiveKey(key) {
|
|
19
|
+
const normalized = key.replace(/[^a-z0-9]/gi, "");
|
|
20
|
+
if (/^(inputtokens|outputtokens|totaltokens|prompttokens|completiontokens|reasoningtokens|cachedtokens|cachecreationinputtokens|cachereadinputtokens|cost|total|input|output|prompt|completion|reasoning|read|write)$/i.test(normalized))
|
|
21
|
+
return false;
|
|
22
|
+
return SENSITIVE_KEY_RE.test(normalized);
|
|
23
|
+
}
|
|
24
|
+
function sanitizeTraceValue(value) {
|
|
25
|
+
if (currentMode === "metadata")
|
|
26
|
+
return { omitted: true, reason: "metadata-capture" };
|
|
27
|
+
if (currentMode === "full")
|
|
28
|
+
return value;
|
|
29
|
+
if (value == null || typeof value !== "object") {
|
|
30
|
+
return typeof value === "string" ? value.replace(SENSITIVE_VALUE_RE, REDACTED).slice(0, 10000) : value;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return value.slice(0, 100).map(sanitizeTraceValue);
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [k, v] of Object.entries(value).slice(0, 100))
|
|
36
|
+
out[k] = isSensitiveKey(k) ? REDACTED : sanitizeTraceValue(v);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function asNumber(value) {
|
|
40
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
41
|
+
}
|
|
42
|
+
function normalizeUsage(raw) {
|
|
43
|
+
const usage = (raw && typeof raw === "object" ? raw : {});
|
|
44
|
+
return {
|
|
45
|
+
inputTokens: asNumber(usage.inputTokens ?? usage.input),
|
|
46
|
+
outputTokens: asNumber(usage.outputTokens ?? usage.output),
|
|
47
|
+
cacheReadTokens: asNumber(usage.cacheReadTokens ?? usage.cacheRead),
|
|
48
|
+
cacheWriteTokens: asNumber(usage.cacheWriteTokens ?? usage.cacheWrite),
|
|
49
|
+
totalTokens: asNumber(usage.totalTokens),
|
|
50
|
+
totalCostUsd: asNumber(usage.cost?.total),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function readJsonFile(filePath, fallback) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function writePrivateFileAtomic(filePath, content) {
|
|
62
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
63
|
+
writeFileSync(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
64
|
+
chmodSync(tmpPath, 0o600);
|
|
65
|
+
renameSync(tmpPath, filePath);
|
|
66
|
+
}
|
|
67
|
+
function validateRunRecord(record) {
|
|
68
|
+
if (record.kind !== "notrace-run")
|
|
69
|
+
throw new Error("notrace record validation failed: invalid kind");
|
|
70
|
+
if (record.schemaVersion !== SCHEMA_VERSION)
|
|
71
|
+
throw new Error("notrace record validation failed: invalid schemaVersion");
|
|
72
|
+
if (!record.traceId || !record.session?.id)
|
|
73
|
+
throw new Error("notrace record validation failed: missing session id");
|
|
74
|
+
if (!record.repository?.cwd)
|
|
75
|
+
throw new Error("notrace record validation failed: missing repository cwd");
|
|
76
|
+
if (!record.activity?.totals)
|
|
77
|
+
throw new Error("notrace record validation failed: missing activity totals");
|
|
78
|
+
if (!Array.isArray(record.events))
|
|
79
|
+
throw new Error("notrace record validation failed: events must be an array");
|
|
80
|
+
}
|
|
81
|
+
function collectActivity(events, startedAt, endedAt) {
|
|
82
|
+
const activity = {
|
|
83
|
+
turnCount: 0,
|
|
84
|
+
llmCallCount: 0,
|
|
85
|
+
toolCallCount: 0,
|
|
86
|
+
toolErrorCount: 0,
|
|
87
|
+
durationMs: Math.max(0, endedAt - startedAt),
|
|
88
|
+
totals: {
|
|
89
|
+
inputTokens: 0,
|
|
90
|
+
outputTokens: 0,
|
|
91
|
+
cacheReadTokens: 0,
|
|
92
|
+
cacheWriteTokens: 0,
|
|
93
|
+
totalTokens: 0,
|
|
94
|
+
totalCostUsd: 0,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
for (const e of events) {
|
|
98
|
+
if (e.type === "turn_start")
|
|
99
|
+
activity.turnCount++;
|
|
100
|
+
if (e.type === "tool_start")
|
|
101
|
+
activity.toolCallCount++;
|
|
102
|
+
if (e.type === "tool_end" && e.isError)
|
|
103
|
+
activity.toolErrorCount++;
|
|
104
|
+
if (e.type === "llm_completion") {
|
|
105
|
+
activity.llmCallCount++;
|
|
106
|
+
const usage = normalizeUsage(e.usage);
|
|
107
|
+
activity.totals.inputTokens += usage.inputTokens;
|
|
108
|
+
activity.totals.outputTokens += usage.outputTokens;
|
|
109
|
+
activity.totals.cacheReadTokens += usage.cacheReadTokens;
|
|
110
|
+
activity.totals.cacheWriteTokens += usage.cacheWriteTokens;
|
|
111
|
+
activity.totals.totalTokens += usage.totalTokens;
|
|
112
|
+
activity.totals.totalCostUsd += usage.totalCostUsd;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return activity;
|
|
116
|
+
}
|
|
117
|
+
function buildConditions(events, telemetry) {
|
|
118
|
+
const models = new Set();
|
|
119
|
+
const providers = new Set();
|
|
120
|
+
for (const event of events) {
|
|
121
|
+
if (event.type !== "llm_completion")
|
|
122
|
+
continue;
|
|
123
|
+
if (typeof event.model === "string" && event.model)
|
|
124
|
+
models.add(event.model);
|
|
125
|
+
if (typeof event.provider === "string" && event.provider)
|
|
126
|
+
providers.add(event.provider);
|
|
127
|
+
}
|
|
128
|
+
const extensions = ["notrace", ...Object.keys(telemetry).sort()];
|
|
129
|
+
return {
|
|
130
|
+
harness: {
|
|
131
|
+
name: "pi",
|
|
132
|
+
adapter: "pi-session-hooks",
|
|
133
|
+
version: null,
|
|
134
|
+
},
|
|
135
|
+
models: [...models],
|
|
136
|
+
providers: [...providers],
|
|
137
|
+
extensions,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function toTaskInfo(context) {
|
|
141
|
+
if (!context)
|
|
142
|
+
return null;
|
|
143
|
+
return {
|
|
144
|
+
workflow: context.workflow,
|
|
145
|
+
id: context.taskId,
|
|
146
|
+
path: context.taskPath,
|
|
147
|
+
dir: context.taskDir,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function createIndexEntry(record, cwd, htmlPath, recordPath) {
|
|
151
|
+
return {
|
|
152
|
+
sessionId: record.traceId,
|
|
153
|
+
startedAt: record.session.startedAt,
|
|
154
|
+
endedAt: record.session.endedAt,
|
|
155
|
+
captureMode: record.captureMode,
|
|
156
|
+
task: record.task,
|
|
157
|
+
conditions: record.conditions,
|
|
158
|
+
activity: record.activity,
|
|
159
|
+
artifacts: {
|
|
160
|
+
html: path.relative(cwd, htmlPath),
|
|
161
|
+
record: path.relative(cwd, recordPath),
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function normalizeTelemetryPayload(raw) {
|
|
166
|
+
if (!raw || typeof raw !== "object")
|
|
167
|
+
return null;
|
|
168
|
+
const payload = raw;
|
|
169
|
+
if (typeof payload.extension !== "string" || !payload.extension.trim())
|
|
170
|
+
return null;
|
|
171
|
+
const status = payload.status === "absent" ||
|
|
172
|
+
payload.status === "loaded-disabled" ||
|
|
173
|
+
payload.status === "loaded-inactive" ||
|
|
174
|
+
payload.status === "active" ||
|
|
175
|
+
payload.status === "unknown"
|
|
176
|
+
? payload.status
|
|
177
|
+
: "unknown";
|
|
178
|
+
return {
|
|
179
|
+
extension: payload.extension,
|
|
180
|
+
telemetry: {
|
|
181
|
+
loaded: payload.loaded !== false,
|
|
182
|
+
enabled: typeof payload.enabled === "boolean" ? payload.enabled : null,
|
|
183
|
+
active: typeof payload.active === "boolean" ? payload.active : null,
|
|
184
|
+
status,
|
|
185
|
+
summary: typeof payload.summary === "string" ? payload.summary : null,
|
|
186
|
+
details: payload.details && typeof payload.details === "object" ? payload.details : {},
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export default function (pi) {
|
|
191
|
+
const events = [];
|
|
192
|
+
const startTime = Date.now();
|
|
193
|
+
let traceId = "";
|
|
194
|
+
let activeLlmPayload = null;
|
|
195
|
+
let shutdownReason = null;
|
|
196
|
+
const extensionTelemetry = new Map();
|
|
197
|
+
currentMode = getInitialMode();
|
|
198
|
+
if (typeof pi.events?.on === "function") {
|
|
199
|
+
pi.events.on(TELEMETRY_CHANNEL, (raw) => {
|
|
200
|
+
const normalized = normalizeTelemetryPayload(raw);
|
|
201
|
+
if (!normalized)
|
|
202
|
+
return;
|
|
203
|
+
extensionTelemetry.set(normalized.extension, normalized.telemetry);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
pi.registerCommand("notrace", {
|
|
207
|
+
description: "Change notrace capture mode (full | redacted | metadata)",
|
|
208
|
+
handler: async (args, ctx) => {
|
|
209
|
+
const mode = args?.trim().toLowerCase();
|
|
210
|
+
if (mode === "full" || mode === "redacted" || mode === "metadata") {
|
|
211
|
+
currentMode = mode;
|
|
212
|
+
ctx.ui.notify(`notrace capture mode set to: ${currentMode}`, "info");
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
ctx.ui.notify(`Current notrace mode: ${currentMode}. Usage: /notrace [full|redacted|metadata]`, "info");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
pi.on("session_start", async (_e, ctx) => {
|
|
220
|
+
traceId = ctx.sessionManager.getSessionId() || `s-${Date.now()}`;
|
|
221
|
+
events.push({ type: "session_start", timestamp: Date.now() });
|
|
222
|
+
});
|
|
223
|
+
pi.on("turn_start", async () => events.push({ type: "turn_start", timestamp: Date.now() }));
|
|
224
|
+
pi.on("tool_execution_start", async (e) => {
|
|
225
|
+
events.push({ type: "tool_start", toolName: e.toolName, args: sanitizeTraceValue(e.args), timestamp: Date.now() });
|
|
226
|
+
});
|
|
227
|
+
pi.on("tool_execution_end", async (e) => {
|
|
228
|
+
events.push({ type: "tool_end", toolName: e.toolName, result: sanitizeTraceValue(e.result), isError: e.isError, timestamp: Date.now() });
|
|
229
|
+
});
|
|
230
|
+
pi.on("before_provider_request", async (e) => {
|
|
231
|
+
activeLlmPayload = sanitizeTraceValue(e.payload);
|
|
232
|
+
});
|
|
233
|
+
pi.on("message_end", async (e) => {
|
|
234
|
+
if (e.message.role === "assistant") {
|
|
235
|
+
events.push({
|
|
236
|
+
type: "llm_completion",
|
|
237
|
+
model: e.message.model,
|
|
238
|
+
provider: e.message.provider,
|
|
239
|
+
inputPayload: activeLlmPayload,
|
|
240
|
+
outputContent: sanitizeTraceValue(e.message.content),
|
|
241
|
+
usage: e.message.usage,
|
|
242
|
+
stopReason: typeof e.message.stopReason === "string" ? e.message.stopReason : undefined,
|
|
243
|
+
errorMessage: typeof e.message.errorMessage === "string" ? sanitizeTraceValue(e.message.errorMessage) : undefined,
|
|
244
|
+
timestamp: Date.now(),
|
|
245
|
+
});
|
|
246
|
+
activeLlmPayload = null;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
pi.on("session_shutdown", async (e, ctx) => {
|
|
250
|
+
shutdownReason = typeof e?.reason === "string" ? e.reason : null;
|
|
251
|
+
const endedAt = Date.now();
|
|
252
|
+
const adapter = getActiveAdapter(ctx.cwd);
|
|
253
|
+
const context = adapter.getContext(ctx.cwd);
|
|
254
|
+
const notraceDir = path.resolve(ctx.cwd, ".notrace");
|
|
255
|
+
const finalTraceId = ctx.sessionManager?.getSessionId?.() || traceId;
|
|
256
|
+
const outputDir = path.join(notraceDir, "sessions", finalTraceId.replace(/[^a-z0-9]/gi, "-"));
|
|
257
|
+
const repositoryName = path.basename(ctx.cwd);
|
|
258
|
+
let branchName = null;
|
|
259
|
+
try {
|
|
260
|
+
branchName = execSync("git branch --show-current", { cwd: ctx.cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim() || null;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// not a git repo or no commits yet
|
|
264
|
+
}
|
|
265
|
+
const recordPath = path.join(outputDir, "notrace.json");
|
|
266
|
+
let mergedEvents = events;
|
|
267
|
+
let originalStartedAt = startTime;
|
|
268
|
+
let originalTask = null;
|
|
269
|
+
if (existsSync(recordPath)) {
|
|
270
|
+
try {
|
|
271
|
+
const oldRecord = readJsonFile(recordPath, null);
|
|
272
|
+
if (Array.isArray(oldRecord.events)) {
|
|
273
|
+
mergedEvents = [...oldRecord.events, ...events];
|
|
274
|
+
}
|
|
275
|
+
if (oldRecord.session?.startedAt) {
|
|
276
|
+
originalStartedAt = new Date(oldRecord.session.startedAt).getTime();
|
|
277
|
+
}
|
|
278
|
+
if (oldRecord.task) {
|
|
279
|
+
originalTask = oldRecord.task;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
// ignore parse errors
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const activity = collectActivity(mergedEvents, originalStartedAt, endedAt);
|
|
287
|
+
// Do not index purely empty ghost sessions
|
|
288
|
+
const isGhostSession = activity.llmCallCount === 0 && activity.toolCallCount === 0 && activity.totals.totalTokens === 0;
|
|
289
|
+
const telemetry = Object.fromEntries([...extensionTelemetry.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
290
|
+
const record = {
|
|
291
|
+
kind: "notrace-run",
|
|
292
|
+
schemaVersion: SCHEMA_VERSION,
|
|
293
|
+
traceId: finalTraceId,
|
|
294
|
+
repository: {
|
|
295
|
+
name: repositoryName,
|
|
296
|
+
cwd: ctx.cwd,
|
|
297
|
+
branch: branchName,
|
|
298
|
+
},
|
|
299
|
+
session: {
|
|
300
|
+
id: finalTraceId,
|
|
301
|
+
startedAt: new Date(originalStartedAt).toISOString(),
|
|
302
|
+
endedAt: new Date(endedAt).toISOString(),
|
|
303
|
+
durationMs: activity.durationMs,
|
|
304
|
+
shutdownReason,
|
|
305
|
+
},
|
|
306
|
+
task: toTaskInfo(context) || originalTask,
|
|
307
|
+
captureMode: currentMode,
|
|
308
|
+
conditions: buildConditions(mergedEvents, telemetry),
|
|
309
|
+
activity,
|
|
310
|
+
telemetry: { extensions: telemetry },
|
|
311
|
+
events: mergedEvents,
|
|
312
|
+
};
|
|
313
|
+
validateRunRecord(record);
|
|
314
|
+
const html = generateHtmlReport(record);
|
|
315
|
+
mkdirSync(outputDir, { recursive: true });
|
|
316
|
+
const htmlPath = path.join(outputDir, "notrace.html");
|
|
317
|
+
writePrivateFileAtomic(htmlPath, html);
|
|
318
|
+
writePrivateFileAtomic(recordPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
319
|
+
const indexPath = path.join(notraceDir, "index.json");
|
|
320
|
+
const existing = readJsonFile(indexPath, { repositoryName, sessions: [] });
|
|
321
|
+
let sessions = Array.isArray(existing.sessions) ? existing.sessions.filter((s) => s.sessionId !== finalTraceId) : [];
|
|
322
|
+
if (!isGhostSession) {
|
|
323
|
+
sessions.push(createIndexEntry(record, ctx.cwd, htmlPath, recordPath));
|
|
324
|
+
}
|
|
325
|
+
writePrivateFileAtomic(indexPath, `${JSON.stringify({ repositoryName, sessions }, null, 2)}\n`);
|
|
326
|
+
writePrivateFileAtomic(path.join(notraceDir, "index.html"), generateDashboardHtml(sessions, { repositoryName }));
|
|
327
|
+
if (context) {
|
|
328
|
+
adapter.attach(context, {
|
|
329
|
+
html: path.relative(ctx.cwd, htmlPath),
|
|
330
|
+
record: path.relative(ctx.cwd, recordPath)
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
console.log(`\n\x1b[1m\x1b[38;5;208m[notrace] Session Retrospective: file://${htmlPath}\x1b[0m\n`);
|
|
334
|
+
});
|
|
335
|
+
}
|