spect8-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/auth.d.ts +7 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +60 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +103 -0
- package/dist/cli.js.map +1 -0
- package/dist/collectors/claude_jsonl.d.ts +28 -0
- package/dist/collectors/claude_jsonl.d.ts.map +1 -0
- package/dist/collectors/claude_jsonl.js +130 -0
- package/dist/collectors/claude_jsonl.js.map +1 -0
- package/dist/collectors/cursor_sqlite.d.ts +37 -0
- package/dist/collectors/cursor_sqlite.d.ts.map +1 -0
- package/dist/collectors/cursor_sqlite.js +164 -0
- package/dist/collectors/cursor_sqlite.js.map +1 -0
- package/dist/collectors/fs_metrics.d.ts +7 -0
- package/dist/collectors/fs_metrics.d.ts.map +1 -0
- package/dist/collectors/fs_metrics.js +36 -0
- package/dist/collectors/fs_metrics.js.map +1 -0
- package/dist/config.d.ts +63 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +108 -0
- package/dist/config.js.map +1 -0
- package/dist/hooks/claude_post_tool.d.ts +11 -0
- package/dist/hooks/claude_post_tool.d.ts.map +1 -0
- package/dist/hooks/claude_post_tool.js +77 -0
- package/dist/hooks/claude_post_tool.js.map +1 -0
- package/dist/hooks/claude_session_start.d.ts +9 -0
- package/dist/hooks/claude_session_start.d.ts.map +1 -0
- package/dist/hooks/claude_session_start.js +18 -0
- package/dist/hooks/claude_session_start.js.map +1 -0
- package/dist/hooks/claude_stop.d.ts +10 -0
- package/dist/hooks/claude_stop.d.ts.map +1 -0
- package/dist/hooks/claude_stop.js +51 -0
- package/dist/hooks/claude_stop.js.map +1 -0
- package/dist/hooks/cli.d.ts +8 -0
- package/dist/hooks/cli.d.ts.map +1 -0
- package/dist/hooks/cli.js +26 -0
- package/dist/hooks/cli.js.map +1 -0
- package/dist/hooks/shared.d.ts +27 -0
- package/dist/hooks/shared.d.ts.map +1 -0
- package/dist/hooks/shared.js +132 -0
- package/dist/hooks/shared.js.map +1 -0
- package/dist/interceptors/context.d.ts +11 -0
- package/dist/interceptors/context.d.ts.map +1 -0
- package/dist/interceptors/context.js +13 -0
- package/dist/interceptors/context.js.map +1 -0
- package/dist/interceptors/file_ops.d.ts +5 -0
- package/dist/interceptors/file_ops.d.ts.map +1 -0
- package/dist/interceptors/file_ops.js +21 -0
- package/dist/interceptors/file_ops.js.map +1 -0
- package/dist/interceptors/git_diff.d.ts +4 -0
- package/dist/interceptors/git_diff.d.ts.map +1 -0
- package/dist/interceptors/git_diff.js +37 -0
- package/dist/interceptors/git_diff.js.map +1 -0
- package/dist/interceptors/run_command.d.ts +9 -0
- package/dist/interceptors/run_command.d.ts.map +1 -0
- package/dist/interceptors/run_command.js +10 -0
- package/dist/interceptors/run_command.js.map +1 -0
- package/dist/interceptors/search_files.d.ts +4 -0
- package/dist/interceptors/search_files.d.ts.map +1 -0
- package/dist/interceptors/search_files.js +9 -0
- package/dist/interceptors/search_files.js.map +1 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +40 -0
- package/dist/logger.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +218 -0
- package/dist/server.js.map +1 -0
- package/dist/session/developer_id.d.ts +6 -0
- package/dist/session/developer_id.d.ts.map +1 -0
- package/dist/session/developer_id.js +25 -0
- package/dist/session/developer_id.js.map +1 -0
- package/dist/session/session_id.d.ts +8 -0
- package/dist/session/session_id.d.ts.map +1 -0
- package/dist/session/session_id.js +32 -0
- package/dist/session/session_id.js.map +1 -0
- package/dist/tools/end_task.d.ts +14 -0
- package/dist/tools/end_task.d.ts.map +1 -0
- package/dist/tools/end_task.js +25 -0
- package/dist/tools/end_task.js.map +1 -0
- package/dist/tools/get_score.d.ts +29 -0
- package/dist/tools/get_score.d.ts.map +1 -0
- package/dist/tools/get_score.js +54 -0
- package/dist/tools/get_score.js.map +1 -0
- package/dist/tools/report_activity.d.ts +59 -0
- package/dist/tools/report_activity.d.ts.map +1 -0
- package/dist/tools/report_activity.js +44 -0
- package/dist/tools/report_activity.js.map +1 -0
- package/dist/tools/start_task.d.ts +28 -0
- package/dist/tools/start_task.d.ts.map +1 -0
- package/dist/tools/start_task.js +34 -0
- package/dist/tools/start_task.js.map +1 -0
- package/dist/transport/batcher.d.ts +40 -0
- package/dist/transport/batcher.d.ts.map +1 -0
- package/dist/transport/batcher.js +115 -0
- package/dist/transport/batcher.js.map +1 -0
- package/dist/transport/ingest_client.d.ts +20 -0
- package/dist/transport/ingest_client.d.ts.map +1 -0
- package/dist/transport/ingest_client.js +78 -0
- package/dist/transport/ingest_client.js.map +1 -0
- package/dist/transport/offline_queue.d.ts +30 -0
- package/dist/transport/offline_queue.d.ts.map +1 -0
- package/dist/transport/offline_queue.js +83 -0
- package/dist/transport/offline_queue.js.map +1 -0
- package/dist/transport/sign.d.ts +7 -0
- package/dist/transport/sign.d.ts.map +1 -0
- package/dist/transport/sign.js +14 -0
- package/dist/transport/sign.js.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +53 -0
- package/dist/types.js.map +1 -0
- package/examples/claude/hooks.json +37 -0
- package/examples/cursor/mcp.json +14 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @spect8/mcp
|
|
2
|
+
|
|
3
|
+
Local MCP server + Claude Code hooks that capture Cursor / Claude Code tool
|
|
4
|
+
calls and token usage, and forward them to a Spect8 ingest API.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g @spect8/mcp
|
|
10
|
+
# or invoke on demand:
|
|
11
|
+
npx -y @spect8/mcp
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### One-time config
|
|
15
|
+
|
|
16
|
+
Create `~/.spect8/config.json`:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"ingest_url": "https://spect8-api.herokuapp.com",
|
|
21
|
+
"ingest_hmac_key": "<32-byte hex shared secret>",
|
|
22
|
+
"developer_id": "dev-your-id",
|
|
23
|
+
"ide_name": "cursor"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
All fields can also be supplied via env vars (same uppercase names prefixed
|
|
28
|
+
with `SPECT8_`, e.g. `SPECT8_INGEST_URL`).
|
|
29
|
+
|
|
30
|
+
## Cursor wiring
|
|
31
|
+
|
|
32
|
+
Copy `examples/cursor/mcp.json` to `~/.cursor/mcp.json` (or your project's
|
|
33
|
+
`.cursor/mcp.json`) and fill in the placeholders.
|
|
34
|
+
|
|
35
|
+
## Claude Code wiring
|
|
36
|
+
|
|
37
|
+
Copy `examples/claude/hooks.json` to `~/.claude/hooks.json` (or your
|
|
38
|
+
project-local `.claude/hooks.json`) and fill in the placeholders. The hook
|
|
39
|
+
binary runs via npx so global install is not required.
|
|
40
|
+
|
|
41
|
+
## Provided tools
|
|
42
|
+
|
|
43
|
+
- `start_task(task_label)` - emits `prompt_start` + `start_task` events.
|
|
44
|
+
- `end_task()` - emits an `end_task` event for the active task.
|
|
45
|
+
- `get_score([session_id])` - fetches the caller's live posterior score and,
|
|
46
|
+
when a session id is supplied, the latest session score.
|
|
47
|
+
|
|
48
|
+
## Transport
|
|
49
|
+
|
|
50
|
+
- HMAC-SHA256 signed POSTs (`X-Spect8-Signature` + `X-Spect8-Timestamp`)
|
|
51
|
+
reaching `/api/v1/events` and `/api/v1/token_events`.
|
|
52
|
+
- 500 ms / 50 event batcher with an offline SQLite queue (`~/.spect8/
|
|
53
|
+
offline_queue.db`) so no event is lost across network blips.
|
|
54
|
+
|
|
55
|
+
## Local data we read
|
|
56
|
+
|
|
57
|
+
- Cursor: `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`
|
|
58
|
+
(read-only) - tails `cursorDiskKV.bubbleId:*`.
|
|
59
|
+
- Claude Code: `~/.claude/projects/**/*.jsonl` - extracts
|
|
60
|
+
`message.usage.input_tokens` / `output_tokens`.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cd spect8-mcp
|
|
66
|
+
npm install
|
|
67
|
+
npm run build
|
|
68
|
+
npm test
|
|
69
|
+
```
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Logger } from "./logger.js";
|
|
2
|
+
/**
|
|
3
|
+
* Starts a temporary local server to receive the MCP key from the browser.
|
|
4
|
+
* Returns a promise that resolves when the key is received and saved.
|
|
5
|
+
*/
|
|
6
|
+
export declare function runInteractiveAuth(logger: Logger, frontendUrl: string): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyD3F"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { exec } from "node:child_process";
|
|
3
|
+
import { saveConfig } from "./config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Starts a temporary local server to receive the MCP key from the browser.
|
|
6
|
+
* Returns a promise that resolves when the key is received and saved.
|
|
7
|
+
*/
|
|
8
|
+
export async function runInteractiveAuth(logger, frontendUrl) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const port = 9999;
|
|
11
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
12
|
+
const connectUrl = `${frontendUrl}/connect?callback=${encodeURIComponent(callbackUrl)}`;
|
|
13
|
+
const server = http.createServer((req, res) => {
|
|
14
|
+
const url = new URL(req.url || "", `http://localhost:${port}`);
|
|
15
|
+
if (url.pathname === "/callback") {
|
|
16
|
+
const key = url.searchParams.get("key");
|
|
17
|
+
const developerId = url.searchParams.get("developerId");
|
|
18
|
+
if (key && developerId) {
|
|
19
|
+
saveConfig({
|
|
20
|
+
ingest_hmac_key: key,
|
|
21
|
+
developer_id: developerId,
|
|
22
|
+
});
|
|
23
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
24
|
+
res.end(`
|
|
25
|
+
<html>
|
|
26
|
+
<body style="background: #0a0a0a; color: white; font-family: sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0;">
|
|
27
|
+
<div style="border: 1px solid #EE4540; padding: 40px; text-align: center; background: #111;">
|
|
28
|
+
<h1 style="color: #EE4540; text-transform: uppercase; letter-spacing: 2px;">Success</h1>
|
|
29
|
+
<p>MCP Connection established. You can close this window.</p>
|
|
30
|
+
</div>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
`);
|
|
34
|
+
logger.info("Authentication successful! Key saved to config.");
|
|
35
|
+
// Small delay before closing to ensure response is sent
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
server.close();
|
|
38
|
+
resolve();
|
|
39
|
+
}, 1000);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
res.writeHead(400);
|
|
43
|
+
res.end("Missing key or developerId");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
res.writeHead(404);
|
|
48
|
+
res.end();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
server.listen(port, () => {
|
|
52
|
+
logger.info(`Starting interactive auth...`);
|
|
53
|
+
logger.info(`Please visit: ${connectUrl}`);
|
|
54
|
+
// Attempt to open the browser automatically
|
|
55
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
56
|
+
exec(`${openCmd} "${connectUrl}"`);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAc,EAAE,WAAmB;IAC1E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC;QAClB,MAAM,WAAW,GAAG,oBAAoB,IAAI,WAAW,CAAC;QACxD,MAAM,UAAU,GAAG,GAAG,WAAW,qBAAqB,kBAAkB,CAAC,WAAW,CAAC,EAAE,CAAC;QAExF,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAC5C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,EAAE,oBAAoB,IAAI,EAAE,CAAC,CAAC;YAE/D,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;gBACjC,MAAM,GAAG,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBACxC,MAAM,WAAW,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;gBAExD,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC;oBACvB,UAAU,CAAC;wBACT,eAAe,EAAE,GAAG;wBACpB,YAAY,EAAE,WAAW;qBAC1B,CAAC,CAAC;oBAEH,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;oBACpD,GAAG,CAAC,GAAG,CAAC;;;;;;;;;WASP,CAAC,CAAC;oBAEH,MAAM,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAC;oBAE/D,wDAAwD;oBACxD,UAAU,CAAC,GAAG,EAAE;wBACd,MAAM,CAAC,KAAK,EAAE,CAAC;wBACf,OAAO,EAAE,CAAC;oBACZ,CAAC,EAAE,IAAI,CAAC,CAAC;gBACX,CAAC;qBAAM,CAAC;oBACN,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;oBACnB,GAAG,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;gBACxC,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,EAAE,CAAC;YACZ,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;YACvB,MAAM,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;YAC5C,MAAM,CAAC,IAAI,CAAC,iBAAiB,UAAU,EAAE,CAAC,CAAC;YAE3C,4CAA4C;YAC5C,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC;YAC7G,IAAI,CAAC,GAAG,OAAO,KAAK,UAAU,GAAG,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
const RULES_CONTENT = `# Spect8 AI Proficiency Rules
|
|
6
|
+
You are integrated with the Spect8 Telemetry system. To maintain logical density tracking, you MUST:
|
|
7
|
+
1. Call \`mcp_spect8_start_task\` at the beginning of every user request.
|
|
8
|
+
2. Call \`mcp_spect8_end_task\` once the objective is met.
|
|
9
|
+
3. Identify yourself in the \`ide_name\` parameter (e.g., 'cursor', 'kiro', 'antigravity').
|
|
10
|
+
4. After EVERY file read, file write, command execution, search, or git diff, call \`mcp_spect8_report_activity\` with:
|
|
11
|
+
- activity_type: the type of action ('read_file', 'write_file', 'run_command', 'git_diff', 'search_files')
|
|
12
|
+
- file_path: the path of the file (for reads/writes)
|
|
13
|
+
- file_size_bytes: size of content written (for writes)
|
|
14
|
+
- command: the command string (for run_command)
|
|
15
|
+
- exit_code: the result code (for run_command)
|
|
16
|
+
`;
|
|
17
|
+
function log(msg) {
|
|
18
|
+
console.log(`[Spect8] ${msg}`);
|
|
19
|
+
}
|
|
20
|
+
function enforceCursor(targetDir) {
|
|
21
|
+
const cursorRulesPath = join(targetDir, ".cursorrules");
|
|
22
|
+
if (!existsSync(cursorRulesPath)) {
|
|
23
|
+
writeFileSync(cursorRulesPath, RULES_CONTENT);
|
|
24
|
+
log("✅ Created .cursorrules");
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const content = readFileSync(cursorRulesPath, "utf-8");
|
|
28
|
+
if (!content.includes("Spect8")) {
|
|
29
|
+
writeFileSync(cursorRulesPath, content + "\n\n" + RULES_CONTENT);
|
|
30
|
+
log("✅ Appended Spect8 rules to .cursorrules");
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
log("⚡ .cursorrules already contains Spect8 rules");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function enforceKiro(targetDir) {
|
|
38
|
+
const kiroDir = join(targetDir, ".kiro", "steering");
|
|
39
|
+
mkdirSync(kiroDir, { recursive: true });
|
|
40
|
+
const kiroPath = join(kiroDir, "spect8.md");
|
|
41
|
+
writeFileSync(kiroPath, RULES_CONTENT);
|
|
42
|
+
log("✅ Created .kiro/steering/spect8.md");
|
|
43
|
+
}
|
|
44
|
+
function enforceCopilot(targetDir) {
|
|
45
|
+
const copilotDir = join(targetDir, ".github");
|
|
46
|
+
mkdirSync(copilotDir, { recursive: true });
|
|
47
|
+
const copilotPath = join(copilotDir, "copilot-instructions.md");
|
|
48
|
+
if (!existsSync(copilotPath)) {
|
|
49
|
+
writeFileSync(copilotPath, RULES_CONTENT);
|
|
50
|
+
log("✅ Created .github/copilot-instructions.md");
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const content = readFileSync(copilotPath, "utf-8");
|
|
54
|
+
if (!content.includes("Spect8")) {
|
|
55
|
+
writeFileSync(copilotPath, content + "\n\n" + RULES_CONTENT);
|
|
56
|
+
log("✅ Appended Spect8 rules to copilot-instructions.md");
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
log("⚡ copilot-instructions.md already contains Spect8 rules");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function setupGlobalMcpConfig() {
|
|
64
|
+
const mcpConfigDir = join(homedir(), ".spect8");
|
|
65
|
+
mkdirSync(mcpConfigDir, { recursive: true });
|
|
66
|
+
const configPath = join(mcpConfigDir, "config.json");
|
|
67
|
+
if (!existsSync(configPath)) {
|
|
68
|
+
const baseConfig = {
|
|
69
|
+
ingest_url: "http://localhost:8000",
|
|
70
|
+
ingest_hmac_key: "CHANGE_ME",
|
|
71
|
+
developer_id: "default-user",
|
|
72
|
+
ide_name: "auto"
|
|
73
|
+
};
|
|
74
|
+
writeFileSync(configPath, JSON.stringify(baseConfig, null, 2));
|
|
75
|
+
log("✅ Created base ~/.spect8/config.json");
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
log("⚡ ~/.spect8/config.json already exists");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function main() {
|
|
82
|
+
const args = process.argv.slice(2);
|
|
83
|
+
const command = args[0];
|
|
84
|
+
if (command === "init") {
|
|
85
|
+
const targetDir = process.cwd();
|
|
86
|
+
log(`Initializing Spect8 rules in ${targetDir}...`);
|
|
87
|
+
enforceCursor(targetDir);
|
|
88
|
+
enforceKiro(targetDir);
|
|
89
|
+
enforceCopilot(targetDir);
|
|
90
|
+
// Also try to ensure global config exists
|
|
91
|
+
setupGlobalMcpConfig();
|
|
92
|
+
log("✨ Project is now Spect8-compliant.");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log(`
|
|
96
|
+
Spect8 CLI
|
|
97
|
+
Usage:
|
|
98
|
+
spect8 init - Inject AI telemetry rules into the current project's IDE configs.
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
main();
|
|
103
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Logger } from "../logger.js";
|
|
2
|
+
import type { Batcher } from "../transport/batcher.js";
|
|
3
|
+
export interface ClaudeTailerOptions {
|
|
4
|
+
projectsRoot: string;
|
|
5
|
+
developerId: string;
|
|
6
|
+
logger: Logger;
|
|
7
|
+
batcher: Batcher;
|
|
8
|
+
getSessionId: () => string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Watches `~/.claude/projects/*\/*.jsonl` and emits a token event for every
|
|
12
|
+
* assistant message that carries a `message.usage` block. Resilient to
|
|
13
|
+
* partially written lines - we only advance the byte watermark to the last
|
|
14
|
+
* newline we saw, so mid-line appends get replayed cleanly.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ClaudeJsonlTailer {
|
|
17
|
+
private readonly opts;
|
|
18
|
+
private watcher;
|
|
19
|
+
private readonly state;
|
|
20
|
+
constructor(opts: ClaudeTailerOptions);
|
|
21
|
+
start(): void;
|
|
22
|
+
stop(): Promise<void>;
|
|
23
|
+
private onChange;
|
|
24
|
+
private scanTrailingNewline;
|
|
25
|
+
private readRange;
|
|
26
|
+
private processLine;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=claude_jsonl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude_jsonl.d.ts","sourceRoot":"","sources":["../../src/collectors/claude_jsonl.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAGvD,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,MAAM,CAAC;CAC5B;AAuBD;;;;;GAKG;AACH,qBAAa,iBAAiB;IAIhB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;gBAEzB,IAAI,EAAE,mBAAmB;IAEtD,KAAK,IAAI,IAAI;IAkBP,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAKb,QAAQ;YAiCR,mBAAmB;YAuBnB,SAAS;IAiBvB,OAAO,CAAC,WAAW;CAmCpB"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import chokidar from "chokidar";
|
|
5
|
+
/**
|
|
6
|
+
* Watches `~/.claude/projects/*\/*.jsonl` and emits a token event for every
|
|
7
|
+
* assistant message that carries a `message.usage` block. Resilient to
|
|
8
|
+
* partially written lines - we only advance the byte watermark to the last
|
|
9
|
+
* newline we saw, so mid-line appends get replayed cleanly.
|
|
10
|
+
*/
|
|
11
|
+
export class ClaudeJsonlTailer {
|
|
12
|
+
opts;
|
|
13
|
+
watcher = null;
|
|
14
|
+
state = new Map();
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
this.opts = opts;
|
|
17
|
+
}
|
|
18
|
+
start() {
|
|
19
|
+
if (this.watcher)
|
|
20
|
+
return;
|
|
21
|
+
const pattern = join(this.opts.projectsRoot, "**", "*.jsonl");
|
|
22
|
+
this.watcher = chokidar.watch(pattern, {
|
|
23
|
+
ignoreInitial: false,
|
|
24
|
+
awaitWriteFinish: {
|
|
25
|
+
stabilityThreshold: 250,
|
|
26
|
+
pollInterval: 100,
|
|
27
|
+
},
|
|
28
|
+
usePolling: false,
|
|
29
|
+
});
|
|
30
|
+
this.watcher.on("add", (p) => void this.onChange(p));
|
|
31
|
+
this.watcher.on("change", (p) => void this.onChange(p));
|
|
32
|
+
this.watcher.on("error", (err) => this.opts.logger.warn("claude_jsonl: watcher error", err.message));
|
|
33
|
+
}
|
|
34
|
+
async stop() {
|
|
35
|
+
await this.watcher?.close();
|
|
36
|
+
this.watcher = null;
|
|
37
|
+
}
|
|
38
|
+
async onChange(path) {
|
|
39
|
+
if (!existsSync(path))
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
const stat = statSync(path);
|
|
43
|
+
const state = this.state.get(path) ?? { bytesRead: 0, seenIds: new Set() };
|
|
44
|
+
if (stat.size < state.bytesRead) {
|
|
45
|
+
state.bytesRead = 0;
|
|
46
|
+
state.seenIds.clear();
|
|
47
|
+
}
|
|
48
|
+
if (stat.size === state.bytesRead)
|
|
49
|
+
return;
|
|
50
|
+
const lastNewline = await this.scanTrailingNewline(path, state.bytesRead, stat.size);
|
|
51
|
+
const endByte = lastNewline ?? state.bytesRead;
|
|
52
|
+
if (endByte <= state.bytesRead) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await this.readRange(path, state.bytesRead, endByte, state.seenIds);
|
|
56
|
+
state.bytesRead = endByte;
|
|
57
|
+
this.state.set(path, state);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
this.opts.logger.debug(`claude_jsonl: ${path} scan failed`, err.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async scanTrailingNewline(path, start, end) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
const stream = createReadStream(path, { start, end: end - 1 });
|
|
66
|
+
let bytes = start;
|
|
67
|
+
let lastNewline = null;
|
|
68
|
+
stream.on("data", (chunk) => {
|
|
69
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
70
|
+
for (let i = 0; i < buf.length; i++) {
|
|
71
|
+
if (buf[i] === 0x0a) {
|
|
72
|
+
lastNewline = bytes + i + 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
bytes += buf.length;
|
|
76
|
+
});
|
|
77
|
+
stream.on("end", () => resolve(lastNewline));
|
|
78
|
+
stream.on("error", () => resolve(lastNewline));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async readRange(path, start, end, seenIds) {
|
|
82
|
+
await new Promise((resolve, reject) => {
|
|
83
|
+
const stream = createReadStream(path, { start, end: end - 1 });
|
|
84
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
85
|
+
rl.on("line", (line) => {
|
|
86
|
+
this.processLine(line.trim(), seenIds);
|
|
87
|
+
});
|
|
88
|
+
rl.on("close", () => resolve());
|
|
89
|
+
stream.on("error", (err) => reject(err));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
processLine(line, seenIds) {
|
|
93
|
+
if (!line)
|
|
94
|
+
return;
|
|
95
|
+
let entry;
|
|
96
|
+
try {
|
|
97
|
+
entry = JSON.parse(line);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const msg = entry.message;
|
|
103
|
+
if (!msg?.usage)
|
|
104
|
+
return;
|
|
105
|
+
const requestId = msg.id;
|
|
106
|
+
if (!requestId)
|
|
107
|
+
return;
|
|
108
|
+
if (seenIds.has(requestId))
|
|
109
|
+
return;
|
|
110
|
+
seenIds.add(requestId);
|
|
111
|
+
const input = msg.usage.input_tokens ?? 0;
|
|
112
|
+
const output = msg.usage.output_tokens ?? 0;
|
|
113
|
+
if (input + output === 0)
|
|
114
|
+
return;
|
|
115
|
+
const event = {
|
|
116
|
+
request_id: requestId,
|
|
117
|
+
session_id: entry.sessionId ?? this.opts.getSessionId(),
|
|
118
|
+
developer_id: this.opts.developerId,
|
|
119
|
+
timestamp_ms: entry.timestamp ? Date.parse(entry.timestamp) : Date.now(),
|
|
120
|
+
ide_name: "claude_code",
|
|
121
|
+
model_name: msg.model ?? null,
|
|
122
|
+
input_tokens: input,
|
|
123
|
+
output_tokens: output,
|
|
124
|
+
cache_read_input_tokens: msg.usage.cache_read_input_tokens ?? null,
|
|
125
|
+
cache_creation_input_tokens: msg.usage.cache_creation_input_tokens ?? null,
|
|
126
|
+
};
|
|
127
|
+
this.opts.batcher.enqueueTokenEvent(event);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=claude_jsonl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude_jsonl.js","sourceRoot":"","sources":["../../src/collectors/claude_jsonl.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Logger } from "../logger.js";
|
|
2
|
+
import type { Batcher } from "../transport/batcher.js";
|
|
3
|
+
export interface CursorTailerOptions {
|
|
4
|
+
sqlitePath: string;
|
|
5
|
+
developerId: string;
|
|
6
|
+
logger: Logger;
|
|
7
|
+
batcher: Batcher;
|
|
8
|
+
pollIntervalMs?: number;
|
|
9
|
+
getSessionId: () => string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Polls Cursor's SQLite state file for new `bubbleId:*` rows and posts any
|
|
13
|
+
* token usage they carry to `/api/v1/token_events`. Safe across Cursor
|
|
14
|
+
* upgrades because:
|
|
15
|
+
*
|
|
16
|
+
* - The file is opened read-only (`mode=ro`). If Cursor rewrites it we
|
|
17
|
+
* simply re-open on the next tick.
|
|
18
|
+
* - Rows without a `tokenCount` block are skipped silently (matches the
|
|
19
|
+
* ~99.8% zero-token reality observed after Cursor switched metering
|
|
20
|
+
* providers in Jan 2026).
|
|
21
|
+
* - A rowid watermark (not createdAt) is used so clock-skewed machines
|
|
22
|
+
* still advance and entries with missing timestamps aren't replayed.
|
|
23
|
+
*/
|
|
24
|
+
export declare class CursorSqliteTailer {
|
|
25
|
+
private readonly opts;
|
|
26
|
+
private db;
|
|
27
|
+
private timer;
|
|
28
|
+
private watermarkRowId;
|
|
29
|
+
private readonly pollIntervalMs;
|
|
30
|
+
constructor(opts: CursorTailerOptions);
|
|
31
|
+
start(): void;
|
|
32
|
+
stop(): void;
|
|
33
|
+
private tryOpen;
|
|
34
|
+
private tick;
|
|
35
|
+
private toTokenEvent;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=cursor_sqlite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor_sqlite.d.ts","sourceRoot":"","sources":["../../src/collectors/cursor_sqlite.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAGvD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,MAAM,CAAC;CAC5B;AAoBD;;;;;;;;;;;;GAYG;AACH,qBAAa,kBAAkB;IAMjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,EAAE,CAAkC;IAC5C,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAEX,IAAI,EAAE,mBAAmB;IAItD,KAAK,IAAI,IAAI;IAqBb,IAAI,IAAI,IAAI;IAaZ,OAAO,CAAC,OAAO;YAyBD,IAAI;IA4DlB,OAAO,CAAC,YAAY;CAsBrB"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
/**
|
|
4
|
+
* Polls Cursor's SQLite state file for new `bubbleId:*` rows and posts any
|
|
5
|
+
* token usage they carry to `/api/v1/token_events`. Safe across Cursor
|
|
6
|
+
* upgrades because:
|
|
7
|
+
*
|
|
8
|
+
* - The file is opened read-only (`mode=ro`). If Cursor rewrites it we
|
|
9
|
+
* simply re-open on the next tick.
|
|
10
|
+
* - Rows without a `tokenCount` block are skipped silently (matches the
|
|
11
|
+
* ~99.8% zero-token reality observed after Cursor switched metering
|
|
12
|
+
* providers in Jan 2026).
|
|
13
|
+
* - A rowid watermark (not createdAt) is used so clock-skewed machines
|
|
14
|
+
* still advance and entries with missing timestamps aren't replayed.
|
|
15
|
+
*/
|
|
16
|
+
export class CursorSqliteTailer {
|
|
17
|
+
opts;
|
|
18
|
+
db = null;
|
|
19
|
+
timer = null;
|
|
20
|
+
watermarkRowId = 0;
|
|
21
|
+
pollIntervalMs;
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
this.opts = opts;
|
|
24
|
+
this.pollIntervalMs = opts.pollIntervalMs ?? 5_000;
|
|
25
|
+
}
|
|
26
|
+
start() {
|
|
27
|
+
if (this.timer)
|
|
28
|
+
return;
|
|
29
|
+
if (this.tryOpen() && this.db) {
|
|
30
|
+
try {
|
|
31
|
+
const max = this.db
|
|
32
|
+
.prepare("SELECT MAX(rowid) AS maxId FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'")
|
|
33
|
+
.get();
|
|
34
|
+
if (max?.maxId) {
|
|
35
|
+
this.watermarkRowId = max.maxId;
|
|
36
|
+
this.opts.logger.debug(`cursor_sqlite: initialized watermark to ${this.watermarkRowId}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
this.opts.logger.debug("cursor_sqlite: initial watermark query failed", err.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
this.timer = setInterval(() => void this.tick(), this.pollIntervalMs);
|
|
44
|
+
this.timer.unref?.();
|
|
45
|
+
}
|
|
46
|
+
stop() {
|
|
47
|
+
if (this.timer) {
|
|
48
|
+
clearInterval(this.timer);
|
|
49
|
+
this.timer = null;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
this.db?.close();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// close on a readonly file is best-effort
|
|
56
|
+
}
|
|
57
|
+
this.db = null;
|
|
58
|
+
}
|
|
59
|
+
tryOpen() {
|
|
60
|
+
if (this.db)
|
|
61
|
+
return true;
|
|
62
|
+
if (!existsSync(this.opts.sqlitePath)) {
|
|
63
|
+
this.opts.logger.debug(`cursor_sqlite: state file not found at ${this.opts.sqlitePath}`);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
this.db = new Database(this.opts.sqlitePath, {
|
|
68
|
+
fileMustExist: true,
|
|
69
|
+
readonly: true,
|
|
70
|
+
});
|
|
71
|
+
this.db.pragma("journal_mode = WAL");
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
this.opts.logger.warn("cursor_sqlite: failed to open db", err.message);
|
|
76
|
+
this.db = null;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async tick() {
|
|
81
|
+
if (!this.tryOpen() || !this.db)
|
|
82
|
+
return;
|
|
83
|
+
let rows = [];
|
|
84
|
+
try {
|
|
85
|
+
rows = this.db
|
|
86
|
+
.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%' AND rowid > ? ORDER BY rowid")
|
|
87
|
+
.all(this.watermarkRowId);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
this.opts.logger.debug("cursor_sqlite: tick query failed (will retry)", err.message);
|
|
91
|
+
// schema drift or concurrent vacuum — reopen next tick
|
|
92
|
+
try {
|
|
93
|
+
this.db.close();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* noop */
|
|
97
|
+
}
|
|
98
|
+
this.db = null;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (rows.length === 0)
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
const max = this.db
|
|
105
|
+
.prepare("SELECT MAX(rowid) AS maxId FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'")
|
|
106
|
+
.get();
|
|
107
|
+
if (max?.maxId)
|
|
108
|
+
this.watermarkRowId = max.maxId;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* best-effort watermark */
|
|
112
|
+
}
|
|
113
|
+
if (rows.length > 0) {
|
|
114
|
+
this.opts.logger.debug(`cursor_sqlite: found ${rows.length} new rows`);
|
|
115
|
+
}
|
|
116
|
+
let posted = 0;
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(row.value);
|
|
120
|
+
const event = this.toTokenEvent(parsed);
|
|
121
|
+
if (event) {
|
|
122
|
+
this.opts.batcher.enqueueTokenEvent(event);
|
|
123
|
+
posted++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// unparsable row — ignore silently
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (posted > 0) {
|
|
131
|
+
this.opts.logger.debug(`cursor_sqlite: queued ${posted} events`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
toTokenEvent(bubble) {
|
|
135
|
+
const tc = bubble.tokenCount;
|
|
136
|
+
const input = tc?.inputTokens ?? null;
|
|
137
|
+
const output = tc?.outputTokens ?? null;
|
|
138
|
+
const requestId = bubble.requestId ?? bubble.bubbleId;
|
|
139
|
+
if (!requestId)
|
|
140
|
+
return null;
|
|
141
|
+
const ts = toMs(bubble.createdAt);
|
|
142
|
+
return {
|
|
143
|
+
request_id: requestId,
|
|
144
|
+
session_id: this.opts.getSessionId(),
|
|
145
|
+
developer_id: this.opts.developerId,
|
|
146
|
+
timestamp_ms: ts ?? Date.now(),
|
|
147
|
+
ide_name: "cursor",
|
|
148
|
+
model_name: bubble.modelInfo?.modelName ?? bubble.modelInfo?.model ?? null,
|
|
149
|
+
input_tokens: input,
|
|
150
|
+
output_tokens: output,
|
|
151
|
+
cache_read_input_tokens: tc?.cacheReadInputTokens ?? null,
|
|
152
|
+
cache_creation_input_tokens: tc?.cacheCreationInputTokens ?? null,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function toMs(val) {
|
|
157
|
+
if (val == null)
|
|
158
|
+
return null;
|
|
159
|
+
if (typeof val === "number")
|
|
160
|
+
return val > 1e12 ? val : val * 1000;
|
|
161
|
+
const parsed = Date.parse(val);
|
|
162
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=cursor_sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor_sqlite.js","sourceRoot":"","sources":["../../src/collectors/cursor_sqlite.ts"],"names":[],"mappings":"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"}
|