@pi-unipi/utility 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -21
- package/package.json +16 -7
- package/skills/utility/SKILL.md +70 -0
- package/src/analytics/collector.ts +293 -0
- package/src/cache/ttl-cache.ts +311 -0
- package/src/commands.ts +186 -0
- package/src/diagnostics/engine.ts +298 -0
- package/src/display/capabilities.ts +200 -0
- package/src/display/width.ts +226 -0
- package/src/index.ts +172 -0
- package/src/info-screen.ts +80 -0
- package/src/lifecycle/cleanup.ts +332 -0
- package/src/lifecycle/process.ts +162 -0
- package/src/tools/batch.ts +229 -0
- package/src/tools/env.ts +134 -0
- package/src/tui/settings-inspector.ts +303 -0
- package/src/types.ts +257 -0
- package/commands.ts +0 -38
- package/index.ts +0 -34
package/README.md
CHANGED
|
@@ -1,51 +1,151 @@
|
|
|
1
1
|
# @pi-unipi/utility
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Comprehensive utility suite for the Pi coding agent — part of the Unipi extension suite.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### Commands
|
|
8
|
+
|
|
9
|
+
| Command | Description |
|
|
10
|
+
|---------|-------------|
|
|
11
|
+
| `/unipi:continue` | Continue agent without polluting context |
|
|
12
|
+
| `/unipi:reload` | Explain how to reload extensions |
|
|
13
|
+
| `/unipi:status` | Show status of all unipi modules |
|
|
14
|
+
| `/unipi:cleanup` | Clean stale DBs, temp files, old sessions |
|
|
15
|
+
| `/unipi:env` | Show environment info (Node, Pi, OS, paths) |
|
|
16
|
+
| `/unipi:doctor` | Run diagnostics across all modules |
|
|
17
|
+
|
|
18
|
+
### Tools
|
|
19
|
+
|
|
20
|
+
| Tool | Description |
|
|
21
|
+
|------|-------------|
|
|
22
|
+
| `ctx_batch` | Atomic batch execution with rollback support |
|
|
23
|
+
| `ctx_env` | Environment inspection for debugging |
|
|
24
|
+
|
|
25
|
+
### Modules (Programmatic API)
|
|
26
|
+
|
|
27
|
+
| Module | Path | Description |
|
|
28
|
+
|--------|------|-------------|
|
|
29
|
+
| **ProcessLifecycle** | `lifecycle/process` | Parent PID polling, orphan detection, signal handlers, cleanup registry |
|
|
30
|
+
| **cleanupStale** | `lifecycle/cleanup` | Stale DB/temp/session/cache cleanup with dry-run support |
|
|
31
|
+
| **TTLCache** | `cache/ttl-cache` | Memory or SQLite-backed TTL cache with auto-expiration |
|
|
32
|
+
| **AnalyticsCollector** | `analytics/collector` | Privacy-respecting event collection with daily rollup |
|
|
33
|
+
| **runDiagnostics** | `diagnostics/engine` | Cross-module health checks with plugin architecture |
|
|
34
|
+
| **detectCapabilities** | `display/capabilities` | Terminal feature detection (color, Nerd Font, unicode) |
|
|
35
|
+
| **Width Utilities** | `display/width` | ANSI-aware clamp, wrap, collapse, pad, center |
|
|
36
|
+
| **SettingsInspector** | `tui/settings-inspector` | Reusable settings overlay data model |
|
|
8
37
|
|
|
9
|
-
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pi install npm:@pi-unipi/utility
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or install the full Unipi suite:
|
|
10
45
|
|
|
46
|
+
```bash
|
|
47
|
+
pi install npm:@pi-unipi/unipi
|
|
11
48
|
```
|
|
12
|
-
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Commands
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
/unipi:continue # Resume agent cleanly
|
|
56
|
+
/unipi:cleanup # Clean stale files
|
|
57
|
+
/unipi:cleanup --dry-run # Preview what would be cleaned
|
|
58
|
+
/unipi:env # Show environment
|
|
59
|
+
/unipi:doctor # Run diagnostics
|
|
13
60
|
```
|
|
14
61
|
|
|
15
|
-
|
|
62
|
+
### Batch Execution (Code)
|
|
16
63
|
|
|
17
|
-
|
|
64
|
+
```typescript
|
|
65
|
+
import { BatchBuilder } from "@pi-unipi/utility/tools/batch";
|
|
18
66
|
|
|
19
|
-
|
|
67
|
+
const report = await new BatchBuilder()
|
|
68
|
+
.addCommand("search", { query: "refactor" })
|
|
69
|
+
.addTool("memory_search", { query: "patterns" })
|
|
70
|
+
.withOptions({ failFast: true, commandTimeoutMs: 30000 })
|
|
71
|
+
.execute(myExecutor);
|
|
20
72
|
|
|
73
|
+
if (!report.success) {
|
|
74
|
+
console.log("Failed:", report.results.find(r => !r.success)?.error);
|
|
75
|
+
}
|
|
21
76
|
```
|
|
22
|
-
|
|
77
|
+
|
|
78
|
+
### TTL Cache (Code)
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { TTLCache } from "@pi-unipi/utility/cache/ttl-cache";
|
|
82
|
+
|
|
83
|
+
const cache = new TTLCache({ defaultTtlMs: 60000 });
|
|
84
|
+
await cache.set("key", { data: "value" });
|
|
85
|
+
const value = await cache.get("key");
|
|
23
86
|
```
|
|
24
87
|
|
|
25
|
-
|
|
88
|
+
### Diagnostics (Code)
|
|
26
89
|
|
|
27
|
-
|
|
90
|
+
```typescript
|
|
91
|
+
import { runDiagnostics, formatDiagnosticsReport } from "@pi-unipi/utility/diagnostics/engine";
|
|
28
92
|
|
|
29
|
-
|
|
30
|
-
|
|
93
|
+
const report = await runDiagnostics();
|
|
94
|
+
console.log(formatDiagnosticsReport(report));
|
|
31
95
|
```
|
|
32
96
|
|
|
33
|
-
|
|
97
|
+
### Terminal Capabilities (Code)
|
|
34
98
|
|
|
35
|
-
```
|
|
36
|
-
|
|
99
|
+
```typescript
|
|
100
|
+
import { detectCapabilities, getIcon } from "@pi-unipi/utility/display/capabilities";
|
|
101
|
+
|
|
102
|
+
const caps = detectCapabilities();
|
|
103
|
+
console.log("Nerd Font:", caps.nerdFont);
|
|
104
|
+
console.log(getIcon("", "[OK]")); // Uses Nerd Font if available
|
|
37
105
|
```
|
|
38
106
|
|
|
39
|
-
##
|
|
107
|
+
## Architecture
|
|
40
108
|
|
|
41
|
-
|
|
109
|
+
```
|
|
110
|
+
packages/utility/src/
|
|
111
|
+
├── index.ts # Extension entry point
|
|
112
|
+
├── commands.ts # Command registration
|
|
113
|
+
├── types.ts # Shared types
|
|
114
|
+
├── info-screen.ts # Info-screen integration
|
|
115
|
+
├── lifecycle/
|
|
116
|
+
│ ├── process.ts # Process lifecycle manager
|
|
117
|
+
│ └── cleanup.ts # Stale cleanup utility
|
|
118
|
+
├── cache/
|
|
119
|
+
│ └── ttl-cache.ts # TTL cache (memory + SQLite)
|
|
120
|
+
├── analytics/
|
|
121
|
+
│ └── collector.ts # Event collection + rollup
|
|
122
|
+
├── diagnostics/
|
|
123
|
+
│ └── engine.ts # Health check engine
|
|
124
|
+
├── display/
|
|
125
|
+
│ ├── capabilities.ts # Terminal detection
|
|
126
|
+
│ └── width.ts # Width utilities
|
|
127
|
+
├── tui/
|
|
128
|
+
│ └── settings-inspector.ts # Settings overlay model
|
|
129
|
+
└── tools/
|
|
130
|
+
├── batch.ts # Batch execution
|
|
131
|
+
└── env.ts # Environment info
|
|
132
|
+
```
|
|
42
133
|
|
|
43
|
-
|
|
134
|
+
## Privacy
|
|
44
135
|
|
|
45
|
-
|
|
136
|
+
The analytics collector is **privacy-respecting** by design:
|
|
137
|
+
- No file contents are recorded
|
|
138
|
+
- No sensitive data (API keys, tokens, passwords) — redacted automatically
|
|
139
|
+
- Strings truncated to 500 characters
|
|
140
|
+
- All data stays local (in-memory by default, optional SQLite)
|
|
46
141
|
|
|
47
142
|
## Dependencies
|
|
48
143
|
|
|
49
|
-
- `@pi-unipi/core` — Shared constants
|
|
144
|
+
- `@pi-unipi/core` — Shared constants, events, utilities
|
|
50
145
|
- `@mariozechner/pi-coding-agent` — Pi extension API
|
|
51
|
-
- `@sinclair/typebox` — Schema validation
|
|
146
|
+
- `@sinclair/typebox` — Schema validation (peer dependency)
|
|
147
|
+
- `sqlite3` — Optional, for persistent cache/analytics
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/utility",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Utility commands and tools for Pi coding agent —
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Utility commands and tools for Pi coding agent — lifecycle, diagnostics, cache, analytics, display, batch execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Neuron Mr White",
|
|
@@ -16,16 +16,22 @@
|
|
|
16
16
|
"pi-coding-agent",
|
|
17
17
|
"unipi",
|
|
18
18
|
"utility",
|
|
19
|
-
"continue"
|
|
19
|
+
"continue",
|
|
20
|
+
"diagnostics",
|
|
21
|
+
"cache",
|
|
22
|
+
"analytics"
|
|
20
23
|
],
|
|
21
24
|
"files": [
|
|
22
|
-
"index.ts",
|
|
23
|
-
"commands.ts",
|
|
24
|
-
"
|
|
25
|
+
"src/index.ts",
|
|
26
|
+
"src/commands.ts",
|
|
27
|
+
"src/types.ts",
|
|
28
|
+
"src/**/*.ts",
|
|
29
|
+
"README.md",
|
|
30
|
+
"skills/**/*.md"
|
|
25
31
|
],
|
|
26
32
|
"pi": {
|
|
27
33
|
"extensions": [
|
|
28
|
-
"index.ts"
|
|
34
|
+
"src/index.ts"
|
|
29
35
|
]
|
|
30
36
|
},
|
|
31
37
|
"publishConfig": {
|
|
@@ -37,5 +43,8 @@
|
|
|
37
43
|
"peerDependencies": {
|
|
38
44
|
"@mariozechner/pi-coding-agent": "*",
|
|
39
45
|
"@sinclair/typebox": "*"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "npx tsx --test tests/**/*.test.ts"
|
|
40
49
|
}
|
|
41
50
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: utility
|
|
3
|
+
scope: agent
|
|
4
|
+
description: |
|
|
5
|
+
Guidance for using the @pi-unipi/utility extension commands and tools.
|
|
6
|
+
Provides lifecycle management, diagnostics, cleanup, batch execution,
|
|
7
|
+
environment inspection, and terminal utilities.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# @pi-unipi/utility — Agent Guidance
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
The `@pi-unipi/utility` extension provides general-purpose utilities for the Pi coding agent. It is **not** related to compaction — it provides helpers that any module or agent can use.
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
| Command | Purpose | When to Use |
|
|
19
|
+
|---------|---------|-------------|
|
|
20
|
+
| `/unipi:continue` | Continue agent without polluting context | After interruption, to resume cleanly |
|
|
21
|
+
| `/unipi:reload` | Explain how to reload extensions | When user asks about hot-reloading |
|
|
22
|
+
| `/unipi:status` | Request status from all modules | To check which modules are loaded |
|
|
23
|
+
| `/unipi:cleanup` | Clean stale files, DBs, sessions | When disk space is low or after crashes |
|
|
24
|
+
| `/unipi:env` | Show environment info | For debugging version/path issues |
|
|
25
|
+
| `/unipi:doctor` | Run diagnostics | When something seems broken |
|
|
26
|
+
|
|
27
|
+
### Cleanup Options
|
|
28
|
+
|
|
29
|
+
- `--dry-run` — Report what would be cleaned without removing anything
|
|
30
|
+
- Default max ages: DBs 14 days, temp 7 days, sessions 30 days
|
|
31
|
+
|
|
32
|
+
## Tools
|
|
33
|
+
|
|
34
|
+
| Tool | Purpose |
|
|
35
|
+
|------|---------|
|
|
36
|
+
| `ctx_batch` | Atomic batch execution of commands with rollback |
|
|
37
|
+
| `ctx_env` | Environment inspection |
|
|
38
|
+
|
|
39
|
+
### Batch Execution Pattern
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { BatchBuilder } from "@pi-unipi/utility/tools/batch";
|
|
43
|
+
|
|
44
|
+
const report = await new BatchBuilder()
|
|
45
|
+
.addCommand("search", { query: "foo" })
|
|
46
|
+
.addTool("memory_search", { query: "bar" })
|
|
47
|
+
.withOptions({ failFast: true, commandTimeoutMs: 30000 })
|
|
48
|
+
.execute(myExecutor);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Modules (for code use)
|
|
52
|
+
|
|
53
|
+
| Module | Import Path | Purpose |
|
|
54
|
+
|--------|-------------|---------|
|
|
55
|
+
| ProcessLifecycle | `@pi-unipi/utility/lifecycle/process` | Parent PID polling, orphan detection, cleanup registry |
|
|
56
|
+
| cleanupStale | `@pi-unipi/utility/lifecycle/cleanup` | Stale file/DB/session cleanup |
|
|
57
|
+
| TTLCache | `@pi-unipi/utility/cache/ttl-cache` | In-memory or SQLite-backed TTL cache |
|
|
58
|
+
| AnalyticsCollector | `@pi-unipi/utility/analytics/collector` | Privacy-respecting event collection |
|
|
59
|
+
| runDiagnostics | `@pi-unipi/utility/diagnostics/engine` | Cross-module health checks |
|
|
60
|
+
| detectCapabilities | `@pi-unipi/utility/display/capabilities` | Terminal feature detection |
|
|
61
|
+
| clampWidth, wrapLines | `@pi-unipi/utility/display/width` | ANSI-aware text formatting |
|
|
62
|
+
| SettingsInspector | `@pi-unipi/utility/tui/settings-inspector` | Reusable settings overlay model |
|
|
63
|
+
|
|
64
|
+
## Integration Notes
|
|
65
|
+
|
|
66
|
+
- Lifecycle manager is a singleton — use `getLifecycle()` to access
|
|
67
|
+
- Analytics collector is a singleton — use `getAnalyticsCollector()`
|
|
68
|
+
- Diagnostics engine supports custom plugins via `registerDiagnosticPlugin()`
|
|
69
|
+
- TTLCache defaults to memory backend; set `persistent: true` for SQLite
|
|
70
|
+
- All modules emit unipi events for cross-module integration
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — Analytics Collector
|
|
3
|
+
*
|
|
4
|
+
* Lightweight metrics collection for all unipi modules.
|
|
5
|
+
* Privacy-respecting: no file contents, no sensitive data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import type {
|
|
10
|
+
AnalyticsEvent,
|
|
11
|
+
AnalyticsEventType,
|
|
12
|
+
AnalyticsRollup,
|
|
13
|
+
AnalyticsOptions,
|
|
14
|
+
} from "../types.js";
|
|
15
|
+
|
|
16
|
+
/** Default options */
|
|
17
|
+
const DEFAULTS: Required<AnalyticsOptions> = {
|
|
18
|
+
dbPath: "~/.unipi/analytics/events.db",
|
|
19
|
+
maxEvents: 10000,
|
|
20
|
+
rollupEnabled: true,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** In-memory event buffer */
|
|
24
|
+
const eventBuffer: AnalyticsEvent[] = [];
|
|
25
|
+
let bufferFlushTimer: ReturnType<typeof setInterval> | null = null;
|
|
26
|
+
|
|
27
|
+
/** Generate a unique event ID */
|
|
28
|
+
function generateId(): string {
|
|
29
|
+
return randomUUID();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Get today's date as YYYY-MM-DD */
|
|
33
|
+
function today(): string {
|
|
34
|
+
return new Date().toISOString().slice(0, 10);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* AnalyticsCollector records events and provides rollup aggregation.
|
|
39
|
+
* Uses in-memory buffering with periodic flush to avoid I/O overhead.
|
|
40
|
+
*/
|
|
41
|
+
export class AnalyticsCollector {
|
|
42
|
+
private opts: Required<AnalyticsOptions>;
|
|
43
|
+
private enabled: boolean;
|
|
44
|
+
|
|
45
|
+
constructor(options: AnalyticsOptions = {}) {
|
|
46
|
+
this.opts = { ...DEFAULTS, ...options };
|
|
47
|
+
this.enabled = true;
|
|
48
|
+
this.startFlushTimer();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Disable collection */
|
|
52
|
+
disable(): void {
|
|
53
|
+
this.enabled = false;
|
|
54
|
+
this.stopFlushTimer();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Enable collection */
|
|
58
|
+
enable(): void {
|
|
59
|
+
this.enabled = true;
|
|
60
|
+
this.startFlushTimer();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Record an analytics event */
|
|
64
|
+
record(
|
|
65
|
+
type: AnalyticsEventType,
|
|
66
|
+
metadata?: Record<string, unknown>,
|
|
67
|
+
): AnalyticsEvent {
|
|
68
|
+
if (!this.enabled) {
|
|
69
|
+
return {
|
|
70
|
+
id: generateId(),
|
|
71
|
+
type,
|
|
72
|
+
timestamp: Date.now(),
|
|
73
|
+
metadata,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const event: AnalyticsEvent = {
|
|
78
|
+
id: generateId(),
|
|
79
|
+
type,
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
metadata: this.sanitizeMetadata(metadata),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
eventBuffer.push(event);
|
|
85
|
+
|
|
86
|
+
// Auto-flush if buffer is full
|
|
87
|
+
if (eventBuffer.length >= this.opts.maxEvents) {
|
|
88
|
+
this.flush().catch(() => {
|
|
89
|
+
// Best-effort flush
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return event;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Record a command execution */
|
|
97
|
+
recordCommand(
|
|
98
|
+
command: string,
|
|
99
|
+
module: string,
|
|
100
|
+
durationMs: number,
|
|
101
|
+
success: boolean,
|
|
102
|
+
): AnalyticsEvent {
|
|
103
|
+
return this.record("command_run", {
|
|
104
|
+
command,
|
|
105
|
+
module,
|
|
106
|
+
durationMs,
|
|
107
|
+
success,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Record a tool call */
|
|
112
|
+
recordTool(
|
|
113
|
+
tool: string,
|
|
114
|
+
module: string,
|
|
115
|
+
durationMs: number,
|
|
116
|
+
success: boolean,
|
|
117
|
+
): AnalyticsEvent {
|
|
118
|
+
return this.record("tool_call", {
|
|
119
|
+
tool,
|
|
120
|
+
module,
|
|
121
|
+
durationMs,
|
|
122
|
+
success,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Record an error */
|
|
127
|
+
recordError(
|
|
128
|
+
module: string,
|
|
129
|
+
errorType: string,
|
|
130
|
+
message?: string,
|
|
131
|
+
): AnalyticsEvent {
|
|
132
|
+
return this.record("error", {
|
|
133
|
+
module,
|
|
134
|
+
errorType,
|
|
135
|
+
message: message ? this.sanitizeString(message) : undefined,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Record a module load */
|
|
140
|
+
recordModuleLoad(module: string, version: string): AnalyticsEvent {
|
|
141
|
+
return this.record("module_load", { module, version });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Record a search operation */
|
|
145
|
+
recordSearch(
|
|
146
|
+
module: string,
|
|
147
|
+
query: string,
|
|
148
|
+
resultCount: number,
|
|
149
|
+
): AnalyticsEvent {
|
|
150
|
+
return this.record("search", {
|
|
151
|
+
module,
|
|
152
|
+
query: this.sanitizeString(query),
|
|
153
|
+
resultCount,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Get all buffered events */
|
|
158
|
+
getEvents(): readonly AnalyticsEvent[] {
|
|
159
|
+
return eventBuffer;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Get events filtered by type */
|
|
163
|
+
getEventsByType(type: AnalyticsEventType): AnalyticsEvent[] {
|
|
164
|
+
return eventBuffer.filter((e) => e.type === type);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Compute daily rollup from buffered events */
|
|
168
|
+
getRollup(date?: string): AnalyticsRollup {
|
|
169
|
+
const targetDate = date ?? today();
|
|
170
|
+
const dayStart = new Date(targetDate).getTime();
|
|
171
|
+
const dayEnd = dayStart + 24 * 60 * 60 * 1000;
|
|
172
|
+
|
|
173
|
+
const dayEvents = eventBuffer.filter(
|
|
174
|
+
(e) => e.timestamp >= dayStart && e.timestamp < dayEnd,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const events: Record<AnalyticsEventType, number> = {
|
|
178
|
+
module_load: 0,
|
|
179
|
+
command_run: 0,
|
|
180
|
+
tool_call: 0,
|
|
181
|
+
error: 0,
|
|
182
|
+
compaction: 0,
|
|
183
|
+
search: 0,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
let totalDurationMs = 0;
|
|
187
|
+
let errorCount = 0;
|
|
188
|
+
|
|
189
|
+
for (const e of dayEvents) {
|
|
190
|
+
events[e.type]++;
|
|
191
|
+
if (e.metadata?.durationMs) {
|
|
192
|
+
totalDurationMs += Number(e.metadata.durationMs);
|
|
193
|
+
}
|
|
194
|
+
if (e.type === "error" || e.metadata?.success === false) {
|
|
195
|
+
errorCount++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
date: targetDate,
|
|
201
|
+
events,
|
|
202
|
+
totalDurationMs,
|
|
203
|
+
errorCount,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Export all events to JSON */
|
|
208
|
+
exportToJSON(): string {
|
|
209
|
+
return JSON.stringify(eventBuffer, null, 2);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Flush buffered events to storage (placeholder for future SQLite persistence) */
|
|
213
|
+
async flush(): Promise<number> {
|
|
214
|
+
const count = eventBuffer.length;
|
|
215
|
+
// TODO: persist to SQLite when sqlite3 is available
|
|
216
|
+
// For now, keep in memory but trim if over max
|
|
217
|
+
if (count > this.opts.maxEvents) {
|
|
218
|
+
eventBuffer.splice(0, count - this.opts.maxEvents);
|
|
219
|
+
}
|
|
220
|
+
return count;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Clear all buffered events */
|
|
224
|
+
clear(): void {
|
|
225
|
+
eventBuffer.length = 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── Private ───────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
private startFlushTimer(): void {
|
|
231
|
+
if (bufferFlushTimer) return;
|
|
232
|
+
bufferFlushTimer = setInterval(() => {
|
|
233
|
+
this.flush().catch(() => {
|
|
234
|
+
// Best-effort
|
|
235
|
+
});
|
|
236
|
+
}, 60000); // Flush every minute
|
|
237
|
+
if (bufferFlushTimer.unref) {
|
|
238
|
+
bufferFlushTimer.unref();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private stopFlushTimer(): void {
|
|
243
|
+
if (bufferFlushTimer) {
|
|
244
|
+
clearInterval(bufferFlushTimer);
|
|
245
|
+
bufferFlushTimer = null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Sensitive key patterns to redact */
|
|
250
|
+
private static SENSITIVE_KEYS =
|
|
251
|
+
/api[_-]?key|apiKey|token|password|secret|auth|credential|private[_-]?key/i;
|
|
252
|
+
|
|
253
|
+
/** Remove sensitive data from metadata */
|
|
254
|
+
private sanitizeMetadata(
|
|
255
|
+
metadata?: Record<string, unknown>,
|
|
256
|
+
): Record<string, unknown> | undefined {
|
|
257
|
+
if (!metadata) return undefined;
|
|
258
|
+
const sanitized: Record<string, unknown> = {};
|
|
259
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
260
|
+
if (AnalyticsCollector.SENSITIVE_KEYS.test(key)) {
|
|
261
|
+
sanitized[key] = "[REDACTED]";
|
|
262
|
+
} else if (typeof value === "string") {
|
|
263
|
+
sanitized[key] = this.sanitizeString(value);
|
|
264
|
+
} else {
|
|
265
|
+
sanitized[key] = value;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return sanitized;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Truncate long strings, remove potential secrets */
|
|
272
|
+
private sanitizeString(str: string): string {
|
|
273
|
+
// Truncate to 500 chars
|
|
274
|
+
let result = str.length > 500 ? str.slice(0, 500) + "…" : str;
|
|
275
|
+
// Redact common secret patterns in strings
|
|
276
|
+
result = result.replace(
|
|
277
|
+
/(api[_-]?key|apiKey|token|password|secret|auth)\s*[:=]\s*\S+/gi,
|
|
278
|
+
"$1: [REDACTED]",
|
|
279
|
+
);
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Global singleton */
|
|
285
|
+
let globalCollector: AnalyticsCollector | null = null;
|
|
286
|
+
|
|
287
|
+
/** Get or create the global analytics collector */
|
|
288
|
+
export function getAnalyticsCollector(options?: AnalyticsOptions): AnalyticsCollector {
|
|
289
|
+
if (!globalCollector) {
|
|
290
|
+
globalCollector = new AnalyticsCollector(options);
|
|
291
|
+
}
|
|
292
|
+
return globalCollector;
|
|
293
|
+
}
|