@raquezha/notrace 0.0.2 → 0.0.4
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 +13 -0
- package/README.md +12 -2
- package/dist/{index.js → notrace.js} +140 -45
- package/{index.ts → extensions/notrace.ts} +141 -44
- package/package.json +4 -4
- package/tsconfig.json +2 -1
- /package/dist/{index.d.ts → notrace.d.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @raquezha/notrace
|
|
2
2
|
|
|
3
|
+
## 0.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f2959b5: Harden notrace reports with default redaction, metadata-only capture support, offline CSP-protected HTML, escaped rendering, private file permissions, and `.workflow`-confined report writes.
|
|
8
|
+
|
|
9
|
+
## 0.0.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 2ab1520: Fix skill conflicts by auto-expanding skill collections in shell integration.
|
|
14
|
+
Standardize extension structure by moving entrypoints to conventional extensions/ directories. This allows Pi to auto-discover them and display clean labels (e.g., "noagy") without file extensions in the UI.
|
|
15
|
+
|
|
3
16
|
## 0.0.2
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Phase 0 / POC local-first interactive HTML Trace Viewer for the Pi Coding Agent. It captures execution traces for workflow debugging — LLM calls, tool executions, token usage, costs — and writes an interactive HTML report to your active task workspace at session end.
|
|
4
4
|
|
|
5
|
-
> **
|
|
5
|
+
> **Security warning:** notrace is local-first and now redacts common secrets by default, escapes report rendering, blocks network access in generated reports, and writes private report files. Reports can still contain sensitive prompts, tool payloads, outputs, and local paths. Do not publish generated reports.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -10,7 +10,8 @@ Phase 0 / POC local-first interactive HTML Trace Viewer for the Pi Coding Agent.
|
|
|
10
10
|
- **Metrics dashboard**: Total tokens, input/output split, cache reads, cost (USD), duration
|
|
11
11
|
- **Clickable `file://` link**: Report path printed to console at session end for instant browser access
|
|
12
12
|
- **Active task aware**: Writes the report into `.workflow/tasks/<task>/notrace.html` when a task is active
|
|
13
|
-
- **HTML report**:
|
|
13
|
+
- **HTML report**: Self-contained/offline report with a restrictive CSP and no remote font/network loads
|
|
14
|
+
- **Safer defaults**: Secret-key/value redaction, bounded payload sizes, metadata-only mode, private file permissions, and `.workflow`-confined report writes
|
|
14
15
|
|
|
15
16
|
## Output
|
|
16
17
|
|
|
@@ -35,6 +36,15 @@ pi --dev
|
|
|
35
36
|
npm install -g @raquezha/notrace
|
|
36
37
|
```
|
|
37
38
|
|
|
39
|
+
## Capture controls
|
|
40
|
+
|
|
41
|
+
By default, notrace uses `NOTRACE_CAPTURE=redacted`: it captures useful payloads but redacts common secret keys/values and truncates very large values.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
NOTRACE_CAPTURE=metadata pi --dev # no prompt/tool payload bodies
|
|
45
|
+
NOTRACE_CAPTURE=full pi --dev # unsafe: raw payloads for local debugging only
|
|
46
|
+
```
|
|
47
|
+
|
|
38
48
|
## Build
|
|
39
49
|
|
|
40
50
|
```bash
|
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
const REDACTED = "[REDACTED by notrace]";
|
|
4
|
+
const MAX_STRING_LENGTH = 20_000;
|
|
5
|
+
const MAX_ARRAY_ITEMS = 200;
|
|
6
|
+
const MAX_OBJECT_KEYS = 200;
|
|
7
|
+
const MAX_DEPTH = 8;
|
|
8
|
+
const SENSITIVE_KEY_RE = /(authorization|cookie|setcookie|password|passwd|pwd|secret|token|apikey|accesskey|accesskeyid|accessid|accesstoken|privatekey|session|credential|refreshtoken|idtoken)/i;
|
|
9
|
+
const SENSITIVE_VALUE_RE = /(bearer\s+[a-z0-9._~+/=-]{12,}|sk-[a-z0-9_-]{16,}|gh[pousr]_[a-z0-9_]{16,}|xox[baprs]-[a-z0-9-]{16,}|AKIA[0-9A-Z]{16})/gi;
|
|
10
|
+
function getCaptureMode() {
|
|
11
|
+
const mode = process.env.NOTRACE_CAPTURE?.toLowerCase();
|
|
12
|
+
if (mode === "metadata" || mode === "full")
|
|
13
|
+
return mode;
|
|
14
|
+
return "redacted";
|
|
15
|
+
}
|
|
16
|
+
function isSensitiveKey(key) {
|
|
17
|
+
return SENSITIVE_KEY_RE.test(key.replace(/[^a-z0-9]/gi, ""));
|
|
18
|
+
}
|
|
19
|
+
function redactString(value) {
|
|
20
|
+
const redacted = value.replace(SENSITIVE_VALUE_RE, REDACTED);
|
|
21
|
+
if (redacted.length <= MAX_STRING_LENGTH)
|
|
22
|
+
return redacted;
|
|
23
|
+
return `${redacted.slice(0, MAX_STRING_LENGTH)}\n…[truncated ${redacted.length - MAX_STRING_LENGTH} chars by notrace]`;
|
|
24
|
+
}
|
|
25
|
+
function sanitizeTraceValue(value, depth = 0, seen = new WeakSet()) {
|
|
26
|
+
if (getCaptureMode() === "full")
|
|
27
|
+
return value;
|
|
28
|
+
if (value == null || typeof value === "number" || typeof value === "boolean")
|
|
29
|
+
return value;
|
|
30
|
+
if (typeof value === "string")
|
|
31
|
+
return redactString(value);
|
|
32
|
+
if (typeof value === "bigint")
|
|
33
|
+
return value.toString();
|
|
34
|
+
if (typeof value === "function" || typeof value === "symbol")
|
|
35
|
+
return `[${typeof value}]`;
|
|
36
|
+
if (depth >= MAX_DEPTH)
|
|
37
|
+
return "[Max depth reached by notrace]";
|
|
38
|
+
if (typeof value !== "object")
|
|
39
|
+
return String(value);
|
|
40
|
+
if (seen.has(value))
|
|
41
|
+
return "[Circular]";
|
|
42
|
+
seen.add(value);
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
const items = value.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitizeTraceValue(item, depth + 1, seen));
|
|
45
|
+
if (value.length > MAX_ARRAY_ITEMS)
|
|
46
|
+
items.push(`…[truncated ${value.length - MAX_ARRAY_ITEMS} items by notrace]`);
|
|
47
|
+
return items;
|
|
48
|
+
}
|
|
49
|
+
const output = {};
|
|
50
|
+
const entries = Object.entries(value);
|
|
51
|
+
for (const [key, item] of entries.slice(0, MAX_OBJECT_KEYS)) {
|
|
52
|
+
output[key] = isSensitiveKey(key) ? REDACTED : sanitizeTraceValue(item, depth + 1, seen);
|
|
53
|
+
}
|
|
54
|
+
if (entries.length > MAX_OBJECT_KEYS)
|
|
55
|
+
output.__notrace_truncated__ = `${entries.length - MAX_OBJECT_KEYS} keys`;
|
|
56
|
+
return output;
|
|
57
|
+
}
|
|
58
|
+
function safeResolveUnder(baseDir, candidate) {
|
|
59
|
+
const base = path.resolve(baseDir);
|
|
60
|
+
const resolved = path.resolve(base, candidate);
|
|
61
|
+
const relative = path.relative(base, resolved);
|
|
62
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ? resolved : null;
|
|
63
|
+
}
|
|
64
|
+
function escapeHtml(value) {
|
|
65
|
+
return String(value).replace(/[&<>'"]/g, (char) => {
|
|
66
|
+
switch (char) {
|
|
67
|
+
case "&": return "&";
|
|
68
|
+
case "<": return "<";
|
|
69
|
+
case ">": return ">";
|
|
70
|
+
case "'": return "'";
|
|
71
|
+
case '"': return """;
|
|
72
|
+
default: return char;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
3
76
|
/**
|
|
4
77
|
* html-observability extension
|
|
5
78
|
*
|
|
@@ -14,31 +87,34 @@ export default function (pi) {
|
|
|
14
87
|
let activeLlmPayload = null;
|
|
15
88
|
let llmStartTime = 0;
|
|
16
89
|
const activeToolTimes = {};
|
|
17
|
-
// Helper to extract active task path from .workflow/active_task.json
|
|
90
|
+
// Helper to extract active task path from .workflow/active_task.json.
|
|
91
|
+
// Reports are constrained to cwd/.workflow to avoid task metadata causing writes elsewhere.
|
|
18
92
|
function getActiveTaskDir(cwd) {
|
|
93
|
+
const workflowDir = path.resolve(cwd, ".workflow");
|
|
19
94
|
try {
|
|
20
|
-
const activeTaskJsonPath = path.join(
|
|
95
|
+
const activeTaskJsonPath = path.join(workflowDir, "active_task.json");
|
|
21
96
|
if (existsSync(activeTaskJsonPath)) {
|
|
22
97
|
const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
98
|
+
const candidate = typeof content.taskPath === "string"
|
|
99
|
+
? safeResolveUnder(cwd, content.taskPath)
|
|
100
|
+
: typeof content.active_task === "string"
|
|
101
|
+
? safeResolveUnder(workflowDir, path.join("tasks", content.active_task))
|
|
102
|
+
: null;
|
|
103
|
+
if (candidate && safeResolveUnder(workflowDir, path.relative(workflowDir, candidate))) {
|
|
104
|
+
return candidate;
|
|
28
105
|
}
|
|
29
106
|
}
|
|
30
107
|
}
|
|
31
108
|
catch {
|
|
32
109
|
// fallback
|
|
33
110
|
}
|
|
34
|
-
|
|
35
|
-
if (!existsSync(defaultDir)) {
|
|
111
|
+
if (!existsSync(workflowDir)) {
|
|
36
112
|
try {
|
|
37
|
-
mkdirSync(
|
|
113
|
+
mkdirSync(workflowDir, { recursive: true, mode: 0o700 });
|
|
38
114
|
}
|
|
39
115
|
catch { }
|
|
40
116
|
}
|
|
41
|
-
return
|
|
117
|
+
return workflowDir;
|
|
42
118
|
}
|
|
43
119
|
// 1. Session start
|
|
44
120
|
pi.on("session_start", async (event, ctx) => {
|
|
@@ -72,7 +148,7 @@ export default function (pi) {
|
|
|
72
148
|
type: "tool_start",
|
|
73
149
|
toolCallId,
|
|
74
150
|
toolName,
|
|
75
|
-
args,
|
|
151
|
+
args: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(args),
|
|
76
152
|
timestamp: Date.now()
|
|
77
153
|
});
|
|
78
154
|
});
|
|
@@ -85,7 +161,7 @@ export default function (pi) {
|
|
|
85
161
|
type: "tool_end",
|
|
86
162
|
toolCallId,
|
|
87
163
|
toolName,
|
|
88
|
-
result,
|
|
164
|
+
result: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(result),
|
|
89
165
|
isError,
|
|
90
166
|
durationMs,
|
|
91
167
|
timestamp: Date.now()
|
|
@@ -94,7 +170,7 @@ export default function (pi) {
|
|
|
94
170
|
});
|
|
95
171
|
// 6. LLM call start (capture payload)
|
|
96
172
|
pi.on("before_provider_request", async (event, ctx) => {
|
|
97
|
-
activeLlmPayload = event.payload;
|
|
173
|
+
activeLlmPayload = getCaptureMode() === "metadata" ? null : sanitizeTraceValue(event.payload);
|
|
98
174
|
llmStartTime = Date.now();
|
|
99
175
|
});
|
|
100
176
|
// 7. LLM call end (capture generation / usage)
|
|
@@ -108,8 +184,8 @@ export default function (pi) {
|
|
|
108
184
|
model: message.model || "unknown",
|
|
109
185
|
provider: message.provider || "unknown",
|
|
110
186
|
inputPayload: activeLlmPayload,
|
|
111
|
-
outputContent: message.content,
|
|
112
|
-
usage: message.usage,
|
|
187
|
+
outputContent: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(message.content),
|
|
188
|
+
usage: sanitizeTraceValue(message.usage),
|
|
113
189
|
durationMs,
|
|
114
190
|
timestamp: Date.now()
|
|
115
191
|
});
|
|
@@ -160,7 +236,8 @@ export default function (pi) {
|
|
|
160
236
|
events
|
|
161
237
|
});
|
|
162
238
|
try {
|
|
163
|
-
|
|
239
|
+
mkdirSync(taskDir, { recursive: true, mode: 0o700 });
|
|
240
|
+
writeFileSync(reportPath, htmlContent, { encoding: "utf-8", mode: 0o600 });
|
|
164
241
|
// Output a nice clickable file:// link to the console for the user
|
|
165
242
|
console.log(`\n📊 [notrace] Observability report generated:`);
|
|
166
243
|
console.log(`👉 \x1b[36mfile://${reportPath}\x1b[0m\n`);
|
|
@@ -185,15 +262,15 @@ function safeJsonForScript(value) {
|
|
|
185
262
|
// Returns a self-contained premium HTML template incorporating the design tokens
|
|
186
263
|
function generateHtmlReport(data) {
|
|
187
264
|
const serializedData = safeJsonForScript(data);
|
|
265
|
+
const escapedTraceId = escapeHtml(data.traceId);
|
|
188
266
|
return `<!DOCTYPE html>
|
|
189
267
|
<html lang="en">
|
|
190
268
|
<head>
|
|
191
269
|
<meta charset="UTF-8">
|
|
192
270
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
193
|
-
<
|
|
194
|
-
<
|
|
195
|
-
<
|
|
196
|
-
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Source+Code+Pro:wght@400;500&display=swap" rel="stylesheet">
|
|
271
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'; connect-src 'none'">
|
|
272
|
+
<meta name="referrer" content="no-referrer">
|
|
273
|
+
<title>notrace - ${escapedTraceId}</title>
|
|
197
274
|
<style>
|
|
198
275
|
:root {
|
|
199
276
|
--bg: #0b0b0e;
|
|
@@ -218,7 +295,7 @@ function generateHtmlReport(data) {
|
|
|
218
295
|
}
|
|
219
296
|
|
|
220
297
|
body {
|
|
221
|
-
font-family:
|
|
298
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
222
299
|
background-color: var(--bg);
|
|
223
300
|
color: var(--text);
|
|
224
301
|
line-height: 1.5;
|
|
@@ -424,7 +501,7 @@ function generateHtmlReport(data) {
|
|
|
424
501
|
}
|
|
425
502
|
|
|
426
503
|
.code-block {
|
|
427
|
-
font-family:
|
|
504
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
428
505
|
font-size: 0.875rem;
|
|
429
506
|
background: rgba(0, 0, 0, 0.35);
|
|
430
507
|
border: 1px solid var(--border);
|
|
@@ -526,18 +603,34 @@ function generateHtmlReport(data) {
|
|
|
526
603
|
<script>
|
|
527
604
|
const traceData = ${serializedData};
|
|
528
605
|
|
|
606
|
+
function escapeHtml(value) {
|
|
607
|
+
return String(value ?? "").replace(/[&<>'"]/g, (char) => ({
|
|
608
|
+
"&": "&",
|
|
609
|
+
"<": "<",
|
|
610
|
+
">": ">",
|
|
611
|
+
"'": "'",
|
|
612
|
+
'"': """
|
|
613
|
+
}[char]));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function safeClassName(value, fallback = "unknown") {
|
|
617
|
+
const normalized = String(value ?? fallback).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
618
|
+
return normalized || fallback;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function jsonText(value) {
|
|
622
|
+
return escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
623
|
+
}
|
|
624
|
+
|
|
529
625
|
// Render Metrics
|
|
530
626
|
document.getElementById("sess-id").textContent = traceData.traceId;
|
|
531
627
|
document.getElementById("sess-time").textContent = new Date(traceData.startTime).toLocaleString();
|
|
532
628
|
document.getElementById("val-duration").textContent = (traceData.durationMs / 1000).toFixed(2) + "s";
|
|
533
629
|
document.getElementById("val-tokens").textContent = traceData.metrics.totalTokens.toLocaleString();
|
|
534
|
-
document.getElementById("val-llms").textContent = traceData.metrics.
|
|
630
|
+
document.getElementById("val-llms").textContent = traceData.metrics.llmCallCount;
|
|
535
631
|
document.getElementById("val-tools").textContent = traceData.metrics.toolCallCount;
|
|
536
632
|
document.getElementById("val-cost").textContent = "$" + traceData.metrics.totalCost;
|
|
537
633
|
|
|
538
|
-
// Correct the labels mapping if names swapped
|
|
539
|
-
document.getElementById("val-llms").textContent = traceData.metrics.llmCallCount;
|
|
540
|
-
|
|
541
634
|
// Process & Render Timeline
|
|
542
635
|
const container = document.getElementById("timeline-container");
|
|
543
636
|
const events = traceData.events;
|
|
@@ -597,19 +690,20 @@ function generateHtmlReport(data) {
|
|
|
597
690
|
// Render cards
|
|
598
691
|
renderedEvents.forEach((ev, index) => {
|
|
599
692
|
const evDiv = document.createElement("div");
|
|
600
|
-
|
|
693
|
+
const eventType = safeClassName(ev.type);
|
|
694
|
+
evDiv.className = \`timeline-event \${eventType}-start \${ev.isError ? "error" : ""}\`;
|
|
601
695
|
|
|
602
696
|
let cardHtml = \`
|
|
603
697
|
<div class="timeline-dot"></div>
|
|
604
698
|
<div class="card" id="card-\${index}">
|
|
605
699
|
<div class="card-header" onclick="toggleCard(\${index})">
|
|
606
700
|
<div class="card-title">
|
|
607
|
-
<span class="card-badge badge-\${
|
|
608
|
-
<span>\${ev.title}</span>
|
|
609
|
-
\${ev.durationMs ? \`<span class="duration-pill">\${(ev.durationMs / 1000).toFixed(2)}s</span>\` : ""}
|
|
701
|
+
<span class="card-badge badge-\${eventType} \${ev.isError ? "error" : ""}">\${escapeHtml(eventType.toUpperCase())}</span>
|
|
702
|
+
<span>\${escapeHtml(ev.title)}</span>
|
|
703
|
+
\${ev.durationMs ? \`<span class="duration-pill">\${escapeHtml((ev.durationMs / 1000).toFixed(2))}s</span>\` : ""}
|
|
610
704
|
</div>
|
|
611
705
|
<div class="card-time">
|
|
612
|
-
<span>\${ev.time}</span>
|
|
706
|
+
<span>\${escapeHtml(ev.time)}</span>
|
|
613
707
|
<svg class="arrow-icon" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
|
614
708
|
</div>
|
|
615
709
|
</div>
|
|
@@ -617,13 +711,13 @@ function generateHtmlReport(data) {
|
|
|
617
711
|
\`;
|
|
618
712
|
|
|
619
713
|
if (ev.type === "session" || ev.type === "turn") {
|
|
620
|
-
cardHtml += \`<div class="code-block">\${ev.body}</div>\`;
|
|
714
|
+
cardHtml += \`<div class="code-block">\${escapeHtml(ev.body)}</div>\`;
|
|
621
715
|
} else if (ev.type === "tool") {
|
|
622
716
|
cardHtml += \`
|
|
623
717
|
<strong>Arguments:</strong>
|
|
624
|
-
<div class="code-block">\${
|
|
718
|
+
<div class="code-block">\${jsonText(ev.args)}</div>
|
|
625
719
|
<strong style="margin-top: 1rem; display: block;">Result (\${ev.isError ? "Error" : "Success"}):</strong>
|
|
626
|
-
<div class="code-block">\${
|
|
720
|
+
<div class="code-block">\${jsonText(ev.result)}</div>
|
|
627
721
|
\`;
|
|
628
722
|
} else if (ev.type === "llm") {
|
|
629
723
|
// Render system prompt and input messages if present
|
|
@@ -636,17 +730,18 @@ function generateHtmlReport(data) {
|
|
|
636
730
|
messagesHtml += \`
|
|
637
731
|
<div class="msg-row system">
|
|
638
732
|
<span class="msg-role">System Instruction</span>
|
|
639
|
-
<span class="msg-text">\${instr}</span>
|
|
733
|
+
<span class="msg-text">\${escapeHtml(instr)}</span>
|
|
640
734
|
</div>
|
|
641
735
|
\`;
|
|
642
736
|
}
|
|
643
737
|
if (ev.payload.messages && Array.isArray(ev.payload.messages)) {
|
|
644
738
|
ev.payload.messages.forEach(m => {
|
|
645
739
|
const contentText = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
|
|
740
|
+
const role = safeClassName(m.role, "message");
|
|
646
741
|
messagesHtml += \`
|
|
647
|
-
<div class="msg-row \${
|
|
648
|
-
<span class="msg-role">\${m.role}</span>
|
|
649
|
-
<span class="msg-text">\${contentText}</span>
|
|
742
|
+
<div class="msg-row \${role}">
|
|
743
|
+
<span class="msg-role">\${escapeHtml(m.role)}</span>
|
|
744
|
+
<span class="msg-text">\${escapeHtml(contentText)}</span>
|
|
650
745
|
</div>
|
|
651
746
|
\`;
|
|
652
747
|
});
|
|
@@ -658,13 +753,13 @@ function generateHtmlReport(data) {
|
|
|
658
753
|
<strong>Context Messages:</strong>
|
|
659
754
|
\${messagesHtml}
|
|
660
755
|
<strong style="margin-top: 1.25rem; display: block;">Generated Response:</strong>
|
|
661
|
-
<div class="code-block">\${
|
|
756
|
+
<div class="code-block">\${jsonText(ev.output)}</div>
|
|
662
757
|
\${ev.usage ? \`
|
|
663
758
|
<div style="margin-top: 1rem; font-size: 0.85rem; color: var(--text-muted); display: flex; gap: 1.5rem;">
|
|
664
|
-
<span>Input Tokens: <strong>\${ev.usage.input}</strong></span>
|
|
665
|
-
<span>Output Tokens: <strong>\${ev.usage.output}</strong></span>
|
|
666
|
-
<span>Total Tokens: <strong>\${ev.usage.totalTokens}</strong></span>
|
|
667
|
-
<span>Cost: <strong>\$\${ev.usage.cost?.total
|
|
759
|
+
<span>Input Tokens: <strong>\${escapeHtml(ev.usage.input ?? 0)}</strong></span>
|
|
760
|
+
<span>Output Tokens: <strong>\${escapeHtml(ev.usage.output ?? 0)}</strong></span>
|
|
761
|
+
<span>Total Tokens: <strong>\${escapeHtml(ev.usage.totalTokens ?? 0)}</strong></span>
|
|
762
|
+
<span>Cost: <strong>\$\${escapeHtml(ev.usage.cost?.total?.toFixed?.(5) || "0.00")}</strong></span>
|
|
668
763
|
</div>
|
|
669
764
|
\` : ""}
|
|
670
765
|
\`;
|
|
@@ -682,7 +777,7 @@ function generateHtmlReport(data) {
|
|
|
682
777
|
// Expand/Collapse controller
|
|
683
778
|
function toggleCard(index) {
|
|
684
779
|
const card = document.getElementById(\`card-\${index}\`);
|
|
685
|
-
card
|
|
780
|
+
card?.classList.toggle("expanded");
|
|
686
781
|
}
|
|
687
782
|
</script>
|
|
688
783
|
</body>
|
|
@@ -2,6 +2,79 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
+
const REDACTED = "[REDACTED by notrace]";
|
|
6
|
+
const MAX_STRING_LENGTH = 20_000;
|
|
7
|
+
const MAX_ARRAY_ITEMS = 200;
|
|
8
|
+
const MAX_OBJECT_KEYS = 200;
|
|
9
|
+
const MAX_DEPTH = 8;
|
|
10
|
+
|
|
11
|
+
const SENSITIVE_KEY_RE = /(authorization|cookie|setcookie|password|passwd|pwd|secret|token|apikey|accesskey|accesskeyid|accessid|accesstoken|privatekey|session|credential|refreshtoken|idtoken)/i;
|
|
12
|
+
const SENSITIVE_VALUE_RE = /(bearer\s+[a-z0-9._~+/=-]{12,}|sk-[a-z0-9_-]{16,}|gh[pousr]_[a-z0-9_]{16,}|xox[baprs]-[a-z0-9-]{16,}|AKIA[0-9A-Z]{16})/gi;
|
|
13
|
+
|
|
14
|
+
type CaptureMode = "metadata" | "redacted" | "full";
|
|
15
|
+
|
|
16
|
+
function getCaptureMode(): CaptureMode {
|
|
17
|
+
const mode = process.env.NOTRACE_CAPTURE?.toLowerCase();
|
|
18
|
+
if (mode === "metadata" || mode === "full") return mode;
|
|
19
|
+
return "redacted";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isSensitiveKey(key: string): boolean {
|
|
23
|
+
return SENSITIVE_KEY_RE.test(key.replace(/[^a-z0-9]/gi, ""));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function redactString(value: string): string {
|
|
27
|
+
const redacted = value.replace(SENSITIVE_VALUE_RE, REDACTED);
|
|
28
|
+
if (redacted.length <= MAX_STRING_LENGTH) return redacted;
|
|
29
|
+
return `${redacted.slice(0, MAX_STRING_LENGTH)}\n…[truncated ${redacted.length - MAX_STRING_LENGTH} chars by notrace]`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sanitizeTraceValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
|
|
33
|
+
if (getCaptureMode() === "full") return value;
|
|
34
|
+
if (value == null || typeof value === "number" || typeof value === "boolean") return value;
|
|
35
|
+
if (typeof value === "string") return redactString(value);
|
|
36
|
+
if (typeof value === "bigint") return value.toString();
|
|
37
|
+
if (typeof value === "function" || typeof value === "symbol") return `[${typeof value}]`;
|
|
38
|
+
if (depth >= MAX_DEPTH) return "[Max depth reached by notrace]";
|
|
39
|
+
if (typeof value !== "object") return String(value);
|
|
40
|
+
if (seen.has(value)) return "[Circular]";
|
|
41
|
+
seen.add(value);
|
|
42
|
+
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
const items = value.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitizeTraceValue(item, depth + 1, seen));
|
|
45
|
+
if (value.length > MAX_ARRAY_ITEMS) items.push(`…[truncated ${value.length - MAX_ARRAY_ITEMS} items by notrace]`);
|
|
46
|
+
return items;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const output: Record<string, unknown> = {};
|
|
50
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
51
|
+
for (const [key, item] of entries.slice(0, MAX_OBJECT_KEYS)) {
|
|
52
|
+
output[key] = isSensitiveKey(key) ? REDACTED : sanitizeTraceValue(item, depth + 1, seen);
|
|
53
|
+
}
|
|
54
|
+
if (entries.length > MAX_OBJECT_KEYS) output.__notrace_truncated__ = `${entries.length - MAX_OBJECT_KEYS} keys`;
|
|
55
|
+
return output;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function safeResolveUnder(baseDir: string, candidate: string): string | null {
|
|
59
|
+
const base = path.resolve(baseDir);
|
|
60
|
+
const resolved = path.resolve(base, candidate);
|
|
61
|
+
const relative = path.relative(base, resolved);
|
|
62
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ? resolved : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function escapeHtml(value: unknown): string {
|
|
66
|
+
return String(value).replace(/[&<>'"]/g, (char) => {
|
|
67
|
+
switch (char) {
|
|
68
|
+
case "&": return "&";
|
|
69
|
+
case "<": return "<";
|
|
70
|
+
case ">": return ">";
|
|
71
|
+
case "'": return "'";
|
|
72
|
+
case '"': return """;
|
|
73
|
+
default: return char;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
5
78
|
/**
|
|
6
79
|
* html-observability extension
|
|
7
80
|
*
|
|
@@ -17,26 +90,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
17
90
|
let llmStartTime = 0;
|
|
18
91
|
const activeToolTimes: Record<string, number> = {};
|
|
19
92
|
|
|
20
|
-
// Helper to extract active task path from .workflow/active_task.json
|
|
93
|
+
// Helper to extract active task path from .workflow/active_task.json.
|
|
94
|
+
// Reports are constrained to cwd/.workflow to avoid task metadata causing writes elsewhere.
|
|
21
95
|
function getActiveTaskDir(cwd: string): string {
|
|
96
|
+
const workflowDir = path.resolve(cwd, ".workflow");
|
|
22
97
|
try {
|
|
23
|
-
const activeTaskJsonPath = path.join(
|
|
98
|
+
const activeTaskJsonPath = path.join(workflowDir, "active_task.json");
|
|
24
99
|
if (existsSync(activeTaskJsonPath)) {
|
|
25
100
|
const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
101
|
+
const candidate = typeof content.taskPath === "string"
|
|
102
|
+
? safeResolveUnder(cwd, content.taskPath)
|
|
103
|
+
: typeof content.active_task === "string"
|
|
104
|
+
? safeResolveUnder(workflowDir, path.join("tasks", content.active_task))
|
|
105
|
+
: null;
|
|
106
|
+
|
|
107
|
+
if (candidate && safeResolveUnder(workflowDir, path.relative(workflowDir, candidate))) {
|
|
108
|
+
return candidate;
|
|
30
109
|
}
|
|
31
110
|
}
|
|
32
111
|
} catch {
|
|
33
112
|
// fallback
|
|
34
113
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
try { mkdirSync(defaultDir, { recursive: true }); } catch {}
|
|
114
|
+
if (!existsSync(workflowDir)) {
|
|
115
|
+
try { mkdirSync(workflowDir, { recursive: true, mode: 0o700 }); } catch {}
|
|
38
116
|
}
|
|
39
|
-
return
|
|
117
|
+
return workflowDir;
|
|
40
118
|
}
|
|
41
119
|
|
|
42
120
|
// 1. Session start
|
|
@@ -74,7 +152,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
74
152
|
type: "tool_start",
|
|
75
153
|
toolCallId,
|
|
76
154
|
toolName,
|
|
77
|
-
args,
|
|
155
|
+
args: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(args),
|
|
78
156
|
timestamp: Date.now()
|
|
79
157
|
});
|
|
80
158
|
});
|
|
@@ -89,7 +167,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
89
167
|
type: "tool_end",
|
|
90
168
|
toolCallId,
|
|
91
169
|
toolName,
|
|
92
|
-
result,
|
|
170
|
+
result: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(result),
|
|
93
171
|
isError,
|
|
94
172
|
durationMs,
|
|
95
173
|
timestamp: Date.now()
|
|
@@ -99,7 +177,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
99
177
|
|
|
100
178
|
// 6. LLM call start (capture payload)
|
|
101
179
|
pi.on("before_provider_request", async (event, ctx) => {
|
|
102
|
-
activeLlmPayload = event.payload;
|
|
180
|
+
activeLlmPayload = getCaptureMode() === "metadata" ? null : sanitizeTraceValue(event.payload);
|
|
103
181
|
llmStartTime = Date.now();
|
|
104
182
|
});
|
|
105
183
|
|
|
@@ -115,8 +193,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
115
193
|
model: message.model || "unknown",
|
|
116
194
|
provider: message.provider || "unknown",
|
|
117
195
|
inputPayload: activeLlmPayload,
|
|
118
|
-
outputContent: message.content,
|
|
119
|
-
usage: message.usage,
|
|
196
|
+
outputContent: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(message.content),
|
|
197
|
+
usage: sanitizeTraceValue(message.usage),
|
|
120
198
|
durationMs,
|
|
121
199
|
timestamp: Date.now()
|
|
122
200
|
});
|
|
@@ -173,7 +251,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
173
251
|
});
|
|
174
252
|
|
|
175
253
|
try {
|
|
176
|
-
|
|
254
|
+
mkdirSync(taskDir, { recursive: true, mode: 0o700 });
|
|
255
|
+
writeFileSync(reportPath, htmlContent, { encoding: "utf-8", mode: 0o600 });
|
|
177
256
|
// Output a nice clickable file:// link to the console for the user
|
|
178
257
|
console.log(`\n📊 [notrace] Observability report generated:`);
|
|
179
258
|
console.log(`👉 \x1b[36mfile://${reportPath}\x1b[0m\n`);
|
|
@@ -199,16 +278,16 @@ function safeJsonForScript(value: any): string {
|
|
|
199
278
|
// Returns a self-contained premium HTML template incorporating the design tokens
|
|
200
279
|
function generateHtmlReport(data: any): string {
|
|
201
280
|
const serializedData = safeJsonForScript(data);
|
|
281
|
+
const escapedTraceId = escapeHtml(data.traceId);
|
|
202
282
|
|
|
203
283
|
return `<!DOCTYPE html>
|
|
204
284
|
<html lang="en">
|
|
205
285
|
<head>
|
|
206
286
|
<meta charset="UTF-8">
|
|
207
287
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
208
|
-
<
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Source+Code+Pro:wght@400;500&display=swap" rel="stylesheet">
|
|
288
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'; connect-src 'none'">
|
|
289
|
+
<meta name="referrer" content="no-referrer">
|
|
290
|
+
<title>notrace - ${escapedTraceId}</title>
|
|
212
291
|
<style>
|
|
213
292
|
:root {
|
|
214
293
|
--bg: #0b0b0e;
|
|
@@ -233,7 +312,7 @@ function generateHtmlReport(data: any): string {
|
|
|
233
312
|
}
|
|
234
313
|
|
|
235
314
|
body {
|
|
236
|
-
font-family:
|
|
315
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
237
316
|
background-color: var(--bg);
|
|
238
317
|
color: var(--text);
|
|
239
318
|
line-height: 1.5;
|
|
@@ -439,7 +518,7 @@ function generateHtmlReport(data: any): string {
|
|
|
439
518
|
}
|
|
440
519
|
|
|
441
520
|
.code-block {
|
|
442
|
-
font-family:
|
|
521
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
443
522
|
font-size: 0.875rem;
|
|
444
523
|
background: rgba(0, 0, 0, 0.35);
|
|
445
524
|
border: 1px solid var(--border);
|
|
@@ -541,18 +620,34 @@ function generateHtmlReport(data: any): string {
|
|
|
541
620
|
<script>
|
|
542
621
|
const traceData = ${serializedData};
|
|
543
622
|
|
|
623
|
+
function escapeHtml(value) {
|
|
624
|
+
return String(value ?? "").replace(/[&<>'"]/g, (char) => ({
|
|
625
|
+
"&": "&",
|
|
626
|
+
"<": "<",
|
|
627
|
+
">": ">",
|
|
628
|
+
"'": "'",
|
|
629
|
+
'"': """
|
|
630
|
+
}[char]));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function safeClassName(value, fallback = "unknown") {
|
|
634
|
+
const normalized = String(value ?? fallback).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
635
|
+
return normalized || fallback;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function jsonText(value) {
|
|
639
|
+
return escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
640
|
+
}
|
|
641
|
+
|
|
544
642
|
// Render Metrics
|
|
545
643
|
document.getElementById("sess-id").textContent = traceData.traceId;
|
|
546
644
|
document.getElementById("sess-time").textContent = new Date(traceData.startTime).toLocaleString();
|
|
547
645
|
document.getElementById("val-duration").textContent = (traceData.durationMs / 1000).toFixed(2) + "s";
|
|
548
646
|
document.getElementById("val-tokens").textContent = traceData.metrics.totalTokens.toLocaleString();
|
|
549
|
-
document.getElementById("val-llms").textContent = traceData.metrics.
|
|
647
|
+
document.getElementById("val-llms").textContent = traceData.metrics.llmCallCount;
|
|
550
648
|
document.getElementById("val-tools").textContent = traceData.metrics.toolCallCount;
|
|
551
649
|
document.getElementById("val-cost").textContent = "$" + traceData.metrics.totalCost;
|
|
552
650
|
|
|
553
|
-
// Correct the labels mapping if names swapped
|
|
554
|
-
document.getElementById("val-llms").textContent = traceData.metrics.llmCallCount;
|
|
555
|
-
|
|
556
651
|
// Process & Render Timeline
|
|
557
652
|
const container = document.getElementById("timeline-container");
|
|
558
653
|
const events = traceData.events;
|
|
@@ -612,19 +707,20 @@ function generateHtmlReport(data: any): string {
|
|
|
612
707
|
// Render cards
|
|
613
708
|
renderedEvents.forEach((ev, index) => {
|
|
614
709
|
const evDiv = document.createElement("div");
|
|
615
|
-
|
|
710
|
+
const eventType = safeClassName(ev.type);
|
|
711
|
+
evDiv.className = \`timeline-event \${eventType}-start \${ev.isError ? "error" : ""}\`;
|
|
616
712
|
|
|
617
713
|
let cardHtml = \`
|
|
618
714
|
<div class="timeline-dot"></div>
|
|
619
715
|
<div class="card" id="card-\${index}">
|
|
620
716
|
<div class="card-header" onclick="toggleCard(\${index})">
|
|
621
717
|
<div class="card-title">
|
|
622
|
-
<span class="card-badge badge-\${
|
|
623
|
-
<span>\${ev.title}</span>
|
|
624
|
-
\${ev.durationMs ? \`<span class="duration-pill">\${(ev.durationMs / 1000).toFixed(2)}s</span>\` : ""}
|
|
718
|
+
<span class="card-badge badge-\${eventType} \${ev.isError ? "error" : ""}">\${escapeHtml(eventType.toUpperCase())}</span>
|
|
719
|
+
<span>\${escapeHtml(ev.title)}</span>
|
|
720
|
+
\${ev.durationMs ? \`<span class="duration-pill">\${escapeHtml((ev.durationMs / 1000).toFixed(2))}s</span>\` : ""}
|
|
625
721
|
</div>
|
|
626
722
|
<div class="card-time">
|
|
627
|
-
<span>\${ev.time}</span>
|
|
723
|
+
<span>\${escapeHtml(ev.time)}</span>
|
|
628
724
|
<svg class="arrow-icon" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
|
629
725
|
</div>
|
|
630
726
|
</div>
|
|
@@ -632,13 +728,13 @@ function generateHtmlReport(data: any): string {
|
|
|
632
728
|
\`;
|
|
633
729
|
|
|
634
730
|
if (ev.type === "session" || ev.type === "turn") {
|
|
635
|
-
cardHtml += \`<div class="code-block">\${ev.body}</div>\`;
|
|
731
|
+
cardHtml += \`<div class="code-block">\${escapeHtml(ev.body)}</div>\`;
|
|
636
732
|
} else if (ev.type === "tool") {
|
|
637
733
|
cardHtml += \`
|
|
638
734
|
<strong>Arguments:</strong>
|
|
639
|
-
<div class="code-block">\${
|
|
735
|
+
<div class="code-block">\${jsonText(ev.args)}</div>
|
|
640
736
|
<strong style="margin-top: 1rem; display: block;">Result (\${ev.isError ? "Error" : "Success"}):</strong>
|
|
641
|
-
<div class="code-block">\${
|
|
737
|
+
<div class="code-block">\${jsonText(ev.result)}</div>
|
|
642
738
|
\`;
|
|
643
739
|
} else if (ev.type === "llm") {
|
|
644
740
|
// Render system prompt and input messages if present
|
|
@@ -651,17 +747,18 @@ function generateHtmlReport(data: any): string {
|
|
|
651
747
|
messagesHtml += \`
|
|
652
748
|
<div class="msg-row system">
|
|
653
749
|
<span class="msg-role">System Instruction</span>
|
|
654
|
-
<span class="msg-text">\${instr}</span>
|
|
750
|
+
<span class="msg-text">\${escapeHtml(instr)}</span>
|
|
655
751
|
</div>
|
|
656
752
|
\`;
|
|
657
753
|
}
|
|
658
754
|
if (ev.payload.messages && Array.isArray(ev.payload.messages)) {
|
|
659
755
|
ev.payload.messages.forEach(m => {
|
|
660
756
|
const contentText = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
|
|
757
|
+
const role = safeClassName(m.role, "message");
|
|
661
758
|
messagesHtml += \`
|
|
662
|
-
<div class="msg-row \${
|
|
663
|
-
<span class="msg-role">\${m.role}</span>
|
|
664
|
-
<span class="msg-text">\${contentText}</span>
|
|
759
|
+
<div class="msg-row \${role}">
|
|
760
|
+
<span class="msg-role">\${escapeHtml(m.role)}</span>
|
|
761
|
+
<span class="msg-text">\${escapeHtml(contentText)}</span>
|
|
665
762
|
</div>
|
|
666
763
|
\`;
|
|
667
764
|
});
|
|
@@ -673,13 +770,13 @@ function generateHtmlReport(data: any): string {
|
|
|
673
770
|
<strong>Context Messages:</strong>
|
|
674
771
|
\${messagesHtml}
|
|
675
772
|
<strong style="margin-top: 1.25rem; display: block;">Generated Response:</strong>
|
|
676
|
-
<div class="code-block">\${
|
|
773
|
+
<div class="code-block">\${jsonText(ev.output)}</div>
|
|
677
774
|
\${ev.usage ? \`
|
|
678
775
|
<div style="margin-top: 1rem; font-size: 0.85rem; color: var(--text-muted); display: flex; gap: 1.5rem;">
|
|
679
|
-
<span>Input Tokens: <strong>\${ev.usage.input}</strong></span>
|
|
680
|
-
<span>Output Tokens: <strong>\${ev.usage.output}</strong></span>
|
|
681
|
-
<span>Total Tokens: <strong>\${ev.usage.totalTokens}</strong></span>
|
|
682
|
-
<span>Cost: <strong>\$\${ev.usage.cost?.total
|
|
776
|
+
<span>Input Tokens: <strong>\${escapeHtml(ev.usage.input ?? 0)}</strong></span>
|
|
777
|
+
<span>Output Tokens: <strong>\${escapeHtml(ev.usage.output ?? 0)}</strong></span>
|
|
778
|
+
<span>Total Tokens: <strong>\${escapeHtml(ev.usage.totalTokens ?? 0)}</strong></span>
|
|
779
|
+
<span>Cost: <strong>\$\${escapeHtml(ev.usage.cost?.total?.toFixed?.(5) || "0.00")}</strong></span>
|
|
683
780
|
</div>
|
|
684
781
|
\` : ""}
|
|
685
782
|
\`;
|
|
@@ -697,7 +794,7 @@ function generateHtmlReport(data: any): string {
|
|
|
697
794
|
// Expand/Collapse controller
|
|
698
795
|
function toggleCard(index) {
|
|
699
796
|
const card = document.getElementById(\`card-\${index}\`);
|
|
700
|
-
card
|
|
797
|
+
card?.classList.toggle("expanded");
|
|
701
798
|
}
|
|
702
799
|
</script>
|
|
703
800
|
</body>
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raquezha/notrace",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Zero-dependency, local-first interactive HTML Trace Viewer for the Pi Coding Agent",
|
|
5
|
-
"main": "dist/
|
|
6
|
-
"types": "dist/
|
|
5
|
+
"main": "dist/notrace.js",
|
|
6
|
+
"types": "dist/notrace.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "tsc",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"pi": {
|
|
30
30
|
"extensions": [
|
|
31
|
-
"
|
|
31
|
+
"extensions"
|
|
32
32
|
]
|
|
33
33
|
}
|
|
34
34
|
}
|
package/tsconfig.json
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
"moduleResolution": "NodeNext",
|
|
6
6
|
"declaration": true,
|
|
7
7
|
"outDir": "./dist",
|
|
8
|
+
"rootDir": "extensions",
|
|
8
9
|
"strict": true,
|
|
9
10
|
"esModuleInterop": true,
|
|
10
11
|
"skipLibCheck": true,
|
|
11
12
|
"forceConsistentCasingInFileNames": true
|
|
12
13
|
},
|
|
13
|
-
"include": ["
|
|
14
|
+
"include": ["extensions/notrace.ts"]
|
|
14
15
|
}
|
|
File without changes
|