@raquezha/notrace 0.0.4 → 0.0.6

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