@metamask-previews/tooling-insight 1.0.1-preview-898fae5
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 +38 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/daily-anonymizer.cjs +8 -0
- package/dist/daily-anonymizer.cjs.map +1 -0
- package/dist/daily-anonymizer.d.cts +3 -0
- package/dist/daily-anonymizer.d.cts.map +1 -0
- package/dist/daily-anonymizer.d.mts +3 -0
- package/dist/daily-anonymizer.d.mts.map +1 -0
- package/dist/daily-anonymizer.mjs +6 -0
- package/dist/daily-anonymizer.mjs.map +1 -0
- package/dist/index.cjs +6 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/allowlist.cjs +159 -0
- package/dist/lib/allowlist.cjs.map +1 -0
- package/dist/lib/allowlist.d.cts +31 -0
- package/dist/lib/allowlist.d.cts.map +1 -0
- package/dist/lib/allowlist.d.mts +31 -0
- package/dist/lib/allowlist.d.mts.map +1 -0
- package/dist/lib/allowlist.mjs +155 -0
- package/dist/lib/allowlist.mjs.map +1 -0
- package/dist/lib/csv.cjs +152 -0
- package/dist/lib/csv.cjs.map +1 -0
- package/dist/lib/csv.d.cts +16 -0
- package/dist/lib/csv.d.cts.map +1 -0
- package/dist/lib/csv.d.mts +16 -0
- package/dist/lib/csv.d.mts.map +1 -0
- package/dist/lib/csv.mjs +149 -0
- package/dist/lib/csv.mjs.map +1 -0
- package/dist/lib/exposition.cjs +102 -0
- package/dist/lib/exposition.cjs.map +1 -0
- package/dist/lib/exposition.d.cts +9 -0
- package/dist/lib/exposition.d.cts.map +1 -0
- package/dist/lib/exposition.d.mts +9 -0
- package/dist/lib/exposition.d.mts.map +1 -0
- package/dist/lib/exposition.mjs +99 -0
- package/dist/lib/exposition.mjs.map +1 -0
- package/dist/lib/fold.cjs +294 -0
- package/dist/lib/fold.cjs.map +1 -0
- package/dist/lib/fold.d.cts +32 -0
- package/dist/lib/fold.d.cts.map +1 -0
- package/dist/lib/fold.d.mts +32 -0
- package/dist/lib/fold.d.mts.map +1 -0
- package/dist/lib/fold.mjs +288 -0
- package/dist/lib/fold.mjs.map +1 -0
- package/dist/lib/log.cjs +116 -0
- package/dist/lib/log.cjs.map +1 -0
- package/dist/lib/log.d.cts +32 -0
- package/dist/lib/log.d.cts.map +1 -0
- package/dist/lib/log.d.mts +32 -0
- package/dist/lib/log.d.mts.map +1 -0
- package/dist/lib/log.mjs +113 -0
- package/dist/lib/log.mjs.map +1 -0
- package/dist/lib/paths.cjs +91 -0
- package/dist/lib/paths.cjs.map +1 -0
- package/dist/lib/paths.d.cts +45 -0
- package/dist/lib/paths.d.cts.map +1 -0
- package/dist/lib/paths.d.mts +45 -0
- package/dist/lib/paths.d.mts.map +1 -0
- package/dist/lib/paths.mjs +82 -0
- package/dist/lib/paths.mjs.map +1 -0
- package/dist/lib/push.cjs +122 -0
- package/dist/lib/push.cjs.map +1 -0
- package/dist/lib/push.d.cts +58 -0
- package/dist/lib/push.d.cts.map +1 -0
- package/dist/lib/push.d.mts +58 -0
- package/dist/lib/push.d.mts.map +1 -0
- package/dist/lib/push.mjs +116 -0
- package/dist/lib/push.mjs.map +1 -0
- package/dist/lib/remoteWrite.cjs +177 -0
- package/dist/lib/remoteWrite.cjs.map +1 -0
- package/dist/lib/remoteWrite.d.cts +24 -0
- package/dist/lib/remoteWrite.d.cts.map +1 -0
- package/dist/lib/remoteWrite.d.mts +24 -0
- package/dist/lib/remoteWrite.d.mts.map +1 -0
- package/dist/lib/remoteWrite.mjs +172 -0
- package/dist/lib/remoteWrite.mjs.map +1 -0
- package/dist/lib/state.cjs +100 -0
- package/dist/lib/state.cjs.map +1 -0
- package/dist/lib/state.d.cts +28 -0
- package/dist/lib/state.d.cts.map +1 -0
- package/dist/lib/state.d.mts +28 -0
- package/dist/lib/state.d.mts.map +1 -0
- package/dist/lib/state.mjs +95 -0
- package/dist/lib/state.mjs.map +1 -0
- package/dist/lib/types.cjs +3 -0
- package/dist/lib/types.cjs.map +1 -0
- package/dist/lib/types.d.cts +82 -0
- package/dist/lib/types.d.cts.map +1 -0
- package/dist/lib/types.d.mts +82 -0
- package/dist/lib/types.d.mts.map +1 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/lib/types.mjs.map +1 -0
- package/dist/run.cjs +137 -0
- package/dist/run.cjs.map +1 -0
- package/dist/run.d.cts +20 -0
- package/dist/run.d.cts.map +1 -0
- package/dist/run.d.mts +20 -0
- package/dist/run.d.mts.map +1 -0
- package/dist/run.mjs +134 -0
- package/dist/run.mjs.map +1 -0
- package/package.json +100 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const ENV_METAMASK_SKILLS_DIR = 'METAMASK_SKILLS_DIR';
|
|
5
|
+
const ENV_CONSENSYS_SKILLS_DIR = 'CONSENSYS_SKILLS_DIR';
|
|
6
|
+
/**
|
|
7
|
+
* Thin wrapper around `execFileSync` used as the default git executor.
|
|
8
|
+
*
|
|
9
|
+
* @param file - The git executable path.
|
|
10
|
+
* @param args - Arguments to pass to git.
|
|
11
|
+
* @param options - Execution options including cwd and encoding.
|
|
12
|
+
* @param options.cwd - Optional working directory for the git command.
|
|
13
|
+
* @param options.encoding - Output encoding; must be `'utf8'`.
|
|
14
|
+
* @returns The stdout of the git command as a UTF-8 string.
|
|
15
|
+
*/
|
|
16
|
+
function defaultExecGit(file, args, options) {
|
|
17
|
+
return execFileSync(file, args, options);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Walk up from `startDir` until a directory containing `package.json` is found.
|
|
21
|
+
*
|
|
22
|
+
* @param startDir - Directory to start the search from.
|
|
23
|
+
* @returns The repo root directory, or `startDir` if no `package.json` is found.
|
|
24
|
+
*/
|
|
25
|
+
export function findRepoRoot(startDir) {
|
|
26
|
+
let current = startDir;
|
|
27
|
+
while (true) {
|
|
28
|
+
if (existsSync(join(current, 'package.json'))) {
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
const parent = join(current, '..');
|
|
32
|
+
if (parent === current) {
|
|
33
|
+
return startDir;
|
|
34
|
+
}
|
|
35
|
+
current = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse the `.skills.local` key-value override file in the repo root.
|
|
40
|
+
*
|
|
41
|
+
* @param repoRoot - Absolute path to the repo root containing `.skills.local`.
|
|
42
|
+
* @returns A map of key-value pairs from the local skills override file.
|
|
43
|
+
*/
|
|
44
|
+
function parseSkillsLocal(repoRoot) {
|
|
45
|
+
const path = join(repoRoot, '.skills.local');
|
|
46
|
+
if (!existsSync(path)) {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
const entries = {};
|
|
50
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const eq = trimmed.indexOf('=');
|
|
56
|
+
if (eq === -1) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
entries[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
|
|
60
|
+
}
|
|
61
|
+
return entries;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the absolute path for a skills directory from multiple sources in priority order:
|
|
65
|
+
* environment variable → `.skills.local` key → cached directory.
|
|
66
|
+
*
|
|
67
|
+
* @param envKey - Environment variable name to check first.
|
|
68
|
+
* @param skillsLocalKey - Key to look up in `.skills.local`.
|
|
69
|
+
* @param repoRoot - Repo root for `.skills.local` and fallback resolution.
|
|
70
|
+
* @param defaultRelative - Optional relative path to a cached skills directory under the repo root.
|
|
71
|
+
* @returns Absolute path to the skills directory, or `null` if unresolvable.
|
|
72
|
+
*/
|
|
73
|
+
function resolveSkillsDir(envKey, skillsLocalKey, repoRoot, defaultRelative) {
|
|
74
|
+
if (process.env[envKey]) {
|
|
75
|
+
return process.env[envKey];
|
|
76
|
+
}
|
|
77
|
+
const local = parseSkillsLocal(repoRoot)[skillsLocalKey];
|
|
78
|
+
if (local) {
|
|
79
|
+
return local;
|
|
80
|
+
}
|
|
81
|
+
if (defaultRelative) {
|
|
82
|
+
const cached = join(repoRoot, defaultRelative);
|
|
83
|
+
if (existsSync(cached)) {
|
|
84
|
+
return cached;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Extract skill names from `git ls-tree` output by matching the `skill.md` path pattern.
|
|
91
|
+
*
|
|
92
|
+
* @param gitOutput - Raw `ls-tree` output from git.
|
|
93
|
+
* @returns An array of skill names prefixed with `mms-`.
|
|
94
|
+
*/
|
|
95
|
+
function extractSkillNames(gitOutput) {
|
|
96
|
+
const names = [];
|
|
97
|
+
for (const line of gitOutput.split('\n')) {
|
|
98
|
+
const match = line.match(/^domains\/[^/]+\/skills\/([^/]+)\/skill\.md$/u);
|
|
99
|
+
if (match) {
|
|
100
|
+
names.push(`mms-${match[1]}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return names;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* List skill names by running `git ls-tree` against the skills git repository.
|
|
107
|
+
*
|
|
108
|
+
* @param dir - Absolute path to the skills git repository.
|
|
109
|
+
* @param execGit - Git executor function.
|
|
110
|
+
* @returns An array of skill names found in the skills repository.
|
|
111
|
+
*/
|
|
112
|
+
function readSkillsFromDir(dir, execGit) {
|
|
113
|
+
const output = execGit('git', ['-C', dir, 'ls-tree', '-r', '--name-only', 'main', '--', 'domains'], { encoding: 'utf8' });
|
|
114
|
+
return extractSkillNames(output);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Read yarn script names from the consuming repo's `main` branch `package.json`.
|
|
118
|
+
*
|
|
119
|
+
* @param repoRoot - Absolute path to the consuming repo root.
|
|
120
|
+
* @param execGit - Git executor function.
|
|
121
|
+
* @returns An array of yarn script names from the consuming repo's main branch.
|
|
122
|
+
*/
|
|
123
|
+
function readYarnScripts(repoRoot, execGit) {
|
|
124
|
+
const output = execGit('git', ['show', 'main:package.json'], {
|
|
125
|
+
cwd: repoRoot,
|
|
126
|
+
encoding: 'utf8',
|
|
127
|
+
});
|
|
128
|
+
const parsed = JSON.parse(output);
|
|
129
|
+
return Object.keys(parsed.scripts ?? {});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Read skill and yarn-script allowlists from local git refs.
|
|
133
|
+
* Private skills repo is optional; public skills + package.json scripts are required.
|
|
134
|
+
*
|
|
135
|
+
* @param options - Optional overrides for repo root and git executor.
|
|
136
|
+
* @returns The combined allowlist of skill names and yarn script names.
|
|
137
|
+
*/
|
|
138
|
+
export function readAllowlist(options = {}) {
|
|
139
|
+
const repoRoot = options.repoRoot ?? findRepoRoot(process.cwd());
|
|
140
|
+
const execGit = options.execGit ?? defaultExecGit;
|
|
141
|
+
const publicDir = resolveSkillsDir(ENV_METAMASK_SKILLS_DIR, 'METAMASK_SKILLS_DIR', repoRoot, '.skills-cache/metamask-skills');
|
|
142
|
+
if (!publicDir) {
|
|
143
|
+
throw new Error('MetaMask skills source directory could not be resolved');
|
|
144
|
+
}
|
|
145
|
+
const privateDir = resolveSkillsDir(ENV_CONSENSYS_SKILLS_DIR, 'CONSENSYS_SKILLS_DIR', repoRoot);
|
|
146
|
+
const skills = new Set(readSkillsFromDir(publicDir, execGit));
|
|
147
|
+
if (privateDir) {
|
|
148
|
+
for (const name of readSkillsFromDir(privateDir, execGit)) {
|
|
149
|
+
skills.add(name);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const scripts = new Set(readYarnScripts(repoRoot, execGit));
|
|
153
|
+
return { skills, scripts };
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=allowlist.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowlist.mjs","sourceRoot":"","sources":["../../src/lib/allowlist.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFileSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport type { Allowlist } from './types';\n\nconst ENV_METAMASK_SKILLS_DIR = 'METAMASK_SKILLS_DIR';\nconst ENV_CONSENSYS_SKILLS_DIR = 'CONSENSYS_SKILLS_DIR';\n\n/** Signature of the git executor passed to allowlist readers, matching `execFileSync`. */\nexport type ExecGit = (\n file: string,\n args: readonly string[],\n options: {\n /** Optional working directory for the git command. */\n cwd?: string;\n /** Output encoding; must be `'utf8'`. */\n encoding: 'utf8';\n },\n) => string;\n\n/** Options for `readAllowlist`. */\nexport type AllowlistOptions = {\n /** Absolute path to the repo root; defaults to `findRepoRoot(process.cwd())`. */\n repoRoot?: string;\n /** Git executor; defaults to the real `execFileSync` wrapper. */\n execGit?: ExecGit;\n};\n\n/**\n * Thin wrapper around `execFileSync` used as the default git executor.\n *\n * @param file - The git executable path.\n * @param args - Arguments to pass to git.\n * @param options - Execution options including cwd and encoding.\n * @param options.cwd - Optional working directory for the git command.\n * @param options.encoding - Output encoding; must be `'utf8'`.\n * @returns The stdout of the git command as a UTF-8 string.\n */\nfunction defaultExecGit(\n file: string,\n args: readonly string[],\n options: {\n /** Optional working directory for the git command. */\n cwd?: string;\n /** Output encoding; must be `'utf8'`. */\n encoding: 'utf8';\n },\n): string {\n return execFileSync(file, args, options);\n}\n\n/**\n * Walk up from `startDir` until a directory containing `package.json` is found.\n *\n * @param startDir - Directory to start the search from.\n * @returns The repo root directory, or `startDir` if no `package.json` is found.\n */\nexport function findRepoRoot(startDir: string): string {\n let current = startDir;\n while (true) {\n if (existsSync(join(current, 'package.json'))) {\n return current;\n }\n const parent = join(current, '..');\n if (parent === current) {\n return startDir;\n }\n current = parent;\n }\n}\n\n/**\n * Parse the `.skills.local` key-value override file in the repo root.\n *\n * @param repoRoot - Absolute path to the repo root containing `.skills.local`.\n * @returns A map of key-value pairs from the local skills override file.\n */\nfunction parseSkillsLocal(repoRoot: string): Record<string, string> {\n const path = join(repoRoot, '.skills.local');\n if (!existsSync(path)) {\n return {};\n }\n const entries: Record<string, string> = {};\n for (const line of readFileSync(path, 'utf8').split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) {\n continue;\n }\n const eq = trimmed.indexOf('=');\n if (eq === -1) {\n continue;\n }\n entries[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();\n }\n return entries;\n}\n\n/**\n * Resolve the absolute path for a skills directory from multiple sources in priority order:\n * environment variable → `.skills.local` key → cached directory.\n *\n * @param envKey - Environment variable name to check first.\n * @param skillsLocalKey - Key to look up in `.skills.local`.\n * @param repoRoot - Repo root for `.skills.local` and fallback resolution.\n * @param defaultRelative - Optional relative path to a cached skills directory under the repo root.\n * @returns Absolute path to the skills directory, or `null` if unresolvable.\n */\nfunction resolveSkillsDir(\n envKey: string,\n skillsLocalKey: string,\n repoRoot: string,\n defaultRelative?: string,\n): string | null {\n if (process.env[envKey]) {\n return process.env[envKey];\n }\n const local = parseSkillsLocal(repoRoot)[skillsLocalKey];\n if (local) {\n return local;\n }\n if (defaultRelative) {\n const cached = join(repoRoot, defaultRelative);\n if (existsSync(cached)) {\n return cached;\n }\n }\n return null;\n}\n\n/**\n * Extract skill names from `git ls-tree` output by matching the `skill.md` path pattern.\n *\n * @param gitOutput - Raw `ls-tree` output from git.\n * @returns An array of skill names prefixed with `mms-`.\n */\nfunction extractSkillNames(gitOutput: string): string[] {\n const names: string[] = [];\n for (const line of gitOutput.split('\\n')) {\n const match = line.match(/^domains\\/[^/]+\\/skills\\/([^/]+)\\/skill\\.md$/u);\n if (match) {\n names.push(`mms-${match[1]}`);\n }\n }\n return names;\n}\n\n/**\n * List skill names by running `git ls-tree` against the skills git repository.\n *\n * @param dir - Absolute path to the skills git repository.\n * @param execGit - Git executor function.\n * @returns An array of skill names found in the skills repository.\n */\nfunction readSkillsFromDir(dir: string, execGit: ExecGit): string[] {\n const output = execGit(\n 'git',\n ['-C', dir, 'ls-tree', '-r', '--name-only', 'main', '--', 'domains'],\n { encoding: 'utf8' },\n );\n return extractSkillNames(output);\n}\n\n/**\n * Read yarn script names from the consuming repo's `main` branch `package.json`.\n *\n * @param repoRoot - Absolute path to the consuming repo root.\n * @param execGit - Git executor function.\n * @returns An array of yarn script names from the consuming repo's main branch.\n */\nfunction readYarnScripts(repoRoot: string, execGit: ExecGit): string[] {\n const output = execGit('git', ['show', 'main:package.json'], {\n cwd: repoRoot,\n encoding: 'utf8',\n });\n const parsed = JSON.parse(output) as {\n /** Map of yarn script names to their shell commands. */\n scripts?: Record<string, string>;\n };\n return Object.keys(parsed.scripts ?? {});\n}\n\n/**\n * Read skill and yarn-script allowlists from local git refs.\n * Private skills repo is optional; public skills + package.json scripts are required.\n *\n * @param options - Optional overrides for repo root and git executor.\n * @returns The combined allowlist of skill names and yarn script names.\n */\nexport function readAllowlist(options: AllowlistOptions = {}): Allowlist {\n const repoRoot = options.repoRoot ?? findRepoRoot(process.cwd());\n const execGit = options.execGit ?? defaultExecGit;\n\n const publicDir = resolveSkillsDir(\n ENV_METAMASK_SKILLS_DIR,\n 'METAMASK_SKILLS_DIR',\n repoRoot,\n '.skills-cache/metamask-skills',\n );\n if (!publicDir) {\n throw new Error('MetaMask skills source directory could not be resolved');\n }\n\n const privateDir = resolveSkillsDir(\n ENV_CONSENSYS_SKILLS_DIR,\n 'CONSENSYS_SKILLS_DIR',\n repoRoot,\n );\n\n const skills = new Set<string>(readSkillsFromDir(publicDir, execGit));\n if (privateDir) {\n for (const name of readSkillsFromDir(privateDir, execGit)) {\n skills.add(name);\n }\n }\n\n const scripts = new Set<string>(readYarnScripts(repoRoot, execGit));\n\n return { skills, scripts };\n}\n"]}
|
package/dist/lib/csv.cjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readAllEvents = readAllEvents;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const paths_1 = require("./paths.cjs");
|
|
7
|
+
const CSV_HEADER = 'tool_name,tool_type,event_type,agent_vendor,session_id,success,duration_ms,created_at';
|
|
8
|
+
/**
|
|
9
|
+
* Strip the `-events.log` suffix from an event log filename to derive the repo name.
|
|
10
|
+
*
|
|
11
|
+
* @param filename - Event log filename (e.g. `metamask-mobile-events.log`).
|
|
12
|
+
* @returns The repo name portion (filename without the `-events.log` suffix).
|
|
13
|
+
*/
|
|
14
|
+
function repoFromFilename(filename) {
|
|
15
|
+
return filename.replace(/-events\.log$/u, '');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse the raw `success` CSV column into a boolean or null.
|
|
19
|
+
*
|
|
20
|
+
* @param raw - Raw CSV column value.
|
|
21
|
+
* @returns `true`, `false`, or `null` when the value is unrecognised or empty.
|
|
22
|
+
*/
|
|
23
|
+
function parseSuccess(raw) {
|
|
24
|
+
if (raw === '') {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (raw === 'true') {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (raw === 'false') {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse the raw `duration_ms` CSV column into a number or null.
|
|
37
|
+
*
|
|
38
|
+
* @param raw - Raw CSV column value.
|
|
39
|
+
* @returns A finite numeric duration, or `null` when the value is empty or non-numeric.
|
|
40
|
+
*/
|
|
41
|
+
function parseDuration(raw) {
|
|
42
|
+
if (raw === '') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const value = Number(raw);
|
|
46
|
+
return Number.isFinite(value) ? value : null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse the raw `tool_type` CSV column into a `ToolType` or null.
|
|
50
|
+
*
|
|
51
|
+
* @param raw - Raw CSV column value.
|
|
52
|
+
* @returns The parsed `ToolType`, or `null` for unrecognised values.
|
|
53
|
+
*/
|
|
54
|
+
function parseToolType(raw) {
|
|
55
|
+
if (raw === 'skill' || raw === 'yarn_script') {
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse the raw `event_type` CSV column into a `CsvEventType` or null.
|
|
62
|
+
*
|
|
63
|
+
* @param raw - Raw CSV column value.
|
|
64
|
+
* @returns The parsed `CsvEventType`, or `null` for unrecognised values.
|
|
65
|
+
*/
|
|
66
|
+
function parseEventType(raw) {
|
|
67
|
+
if (raw === 'start' || raw === 'end' || raw === 'interrupted') {
|
|
68
|
+
return raw;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse a single CSV row into a `ToolEvent`, or null for invalid rows.
|
|
74
|
+
*
|
|
75
|
+
* @param line - A raw CSV row string.
|
|
76
|
+
* @param repo - The repo name derived from the log filename.
|
|
77
|
+
* @param onMalformedRow - Optional callback invoked for rows that cannot be parsed.
|
|
78
|
+
* @returns The parsed `ToolEvent`, or `null` for header, malformed, or filtered rows.
|
|
79
|
+
*/
|
|
80
|
+
function parseRow(line, repo, onMalformedRow) {
|
|
81
|
+
// The collector writes a fixed, controlled schema with no quoted/escaped
|
|
82
|
+
// fields, so a naive comma split is sufficient (and intentional).
|
|
83
|
+
const rawParts = line.split(',');
|
|
84
|
+
if (rawParts.length < 8) {
|
|
85
|
+
onMalformedRow?.(line, 'expected 8 columns');
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
// Safe to assert: we checked length >= 8 above, but TypeScript can't narrow
|
|
89
|
+
// array destructuring based on a length guard.
|
|
90
|
+
const parts = rawParts;
|
|
91
|
+
const [tool_name, tool_typeRaw, event_typeRaw, agent_vendor, session_id, successRaw, durationRaw, created_at,] = parts;
|
|
92
|
+
const tool_type = parseToolType(tool_typeRaw);
|
|
93
|
+
const event_type = parseEventType(event_typeRaw);
|
|
94
|
+
if (!tool_name || !tool_type || !event_type || !created_at) {
|
|
95
|
+
onMalformedRow?.(line, 'missing required fields');
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
repo,
|
|
100
|
+
tool_name,
|
|
101
|
+
tool_type,
|
|
102
|
+
event_type,
|
|
103
|
+
agent_vendor,
|
|
104
|
+
session_id,
|
|
105
|
+
success: parseSuccess(successRaw),
|
|
106
|
+
duration_ms: parseDuration(durationRaw),
|
|
107
|
+
created_at,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Read and parse all events from a single `*-events.log` file.
|
|
112
|
+
*
|
|
113
|
+
* @param filePath - Absolute path to the events log file.
|
|
114
|
+
* @param repo - The repo name associated with this file.
|
|
115
|
+
* @param onMalformedRow - Optional callback invoked for rows that cannot be parsed.
|
|
116
|
+
* @returns An array of parsed `ToolEvent` objects from the file.
|
|
117
|
+
*/
|
|
118
|
+
function readEventsFile(filePath, repo, onMalformedRow) {
|
|
119
|
+
const content = (0, node_fs_1.readFileSync)(filePath, 'utf8');
|
|
120
|
+
const lines = content.split('\n').filter((line) => line.trim() !== '');
|
|
121
|
+
const events = [];
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
if (line === CSV_HEADER || line.startsWith('tool_name,')) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const event = parseRow(line, repo, onMalformedRow);
|
|
127
|
+
if (event) {
|
|
128
|
+
events.push(event);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return events;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Read all `*-events.log` files from the configured collection directory.
|
|
135
|
+
*
|
|
136
|
+
* @param options - Optional directory override and malformed-row callback.
|
|
137
|
+
* @returns All parsed `ToolEvent` objects from all log files in the directory.
|
|
138
|
+
*/
|
|
139
|
+
function readAllEvents(options = {}) {
|
|
140
|
+
const dir = options.logDir ?? (0, paths_1.getEventsLogDir)();
|
|
141
|
+
if (!(0, node_fs_1.existsSync)(dir)) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const files = (0, node_fs_1.readdirSync)(dir).filter((name) => name.endsWith('-events.log'));
|
|
145
|
+
const events = [];
|
|
146
|
+
for (const filename of files.sort()) {
|
|
147
|
+
const repo = repoFromFilename(filename);
|
|
148
|
+
events.push(...readEventsFile((0, node_path_1.join)(dir, filename), repo, options.onMalformedRow));
|
|
149
|
+
}
|
|
150
|
+
return events;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=csv.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.cjs","sourceRoot":"","sources":["../../src/lib/csv.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { getEventsLogDir } from './paths';\nimport type { CsvEventType, ToolEvent, ToolType } from './types';\n\nconst CSV_HEADER =\n 'tool_name,tool_type,event_type,agent_vendor,session_id,success,duration_ms,created_at';\n\n/** Options for `readAllEvents`. */\nexport type ReadAllEventsOptions = {\n /** Override for the directory to scan; defaults to `getEventsLogDir()`. */\n logDir?: string;\n /** Called for each row that cannot be parsed into a `ToolEvent`. */\n onMalformedRow?: (line: string, reason: string) => void;\n};\n\n/**\n * Strip the `-events.log` suffix from an event log filename to derive the repo name.\n *\n * @param filename - Event log filename (e.g. `metamask-mobile-events.log`).\n * @returns The repo name portion (filename without the `-events.log` suffix).\n */\nfunction repoFromFilename(filename: string): string {\n return filename.replace(/-events\\.log$/u, '');\n}\n\n/**\n * Parse the raw `success` CSV column into a boolean or null.\n *\n * @param raw - Raw CSV column value.\n * @returns `true`, `false`, or `null` when the value is unrecognised or empty.\n */\nfunction parseSuccess(raw: string): boolean | null {\n if (raw === '') {\n return null;\n }\n if (raw === 'true') {\n return true;\n }\n if (raw === 'false') {\n return false;\n }\n return null;\n}\n\n/**\n * Parse the raw `duration_ms` CSV column into a number or null.\n *\n * @param raw - Raw CSV column value.\n * @returns A finite numeric duration, or `null` when the value is empty or non-numeric.\n */\nfunction parseDuration(raw: string): number | null {\n if (raw === '') {\n return null;\n }\n const value = Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n\n/**\n * Parse the raw `tool_type` CSV column into a `ToolType` or null.\n *\n * @param raw - Raw CSV column value.\n * @returns The parsed `ToolType`, or `null` for unrecognised values.\n */\nfunction parseToolType(raw: string): ToolType | null {\n if (raw === 'skill' || raw === 'yarn_script') {\n return raw;\n }\n return null;\n}\n\n/**\n * Parse the raw `event_type` CSV column into a `CsvEventType` or null.\n *\n * @param raw - Raw CSV column value.\n * @returns The parsed `CsvEventType`, or `null` for unrecognised values.\n */\nfunction parseEventType(raw: string): CsvEventType | null {\n if (raw === 'start' || raw === 'end' || raw === 'interrupted') {\n return raw;\n }\n return null;\n}\n\n/**\n * Parse a single CSV row into a `ToolEvent`, or null for invalid rows.\n *\n * @param line - A raw CSV row string.\n * @param repo - The repo name derived from the log filename.\n * @param onMalformedRow - Optional callback invoked for rows that cannot be parsed.\n * @returns The parsed `ToolEvent`, or `null` for header, malformed, or filtered rows.\n */\nfunction parseRow(\n line: string,\n repo: string,\n onMalformedRow?: (line: string, reason: string) => void,\n): ToolEvent | null {\n // The collector writes a fixed, controlled schema with no quoted/escaped\n // fields, so a naive comma split is sufficient (and intentional).\n const rawParts = line.split(',');\n if (rawParts.length < 8) {\n onMalformedRow?.(line, 'expected 8 columns');\n return null;\n }\n // Safe to assert: we checked length >= 8 above, but TypeScript can't narrow\n // array destructuring based on a length guard.\n const parts = rawParts as [\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n ...string[],\n ];\n\n const [\n tool_name,\n tool_typeRaw,\n event_typeRaw,\n agent_vendor,\n session_id,\n successRaw,\n durationRaw,\n created_at,\n ] = parts;\n\n const tool_type = parseToolType(tool_typeRaw);\n const event_type = parseEventType(event_typeRaw);\n\n if (!tool_name || !tool_type || !event_type || !created_at) {\n onMalformedRow?.(line, 'missing required fields');\n return null;\n }\n\n return {\n repo,\n tool_name,\n tool_type,\n event_type,\n agent_vendor,\n session_id,\n success: parseSuccess(successRaw),\n duration_ms: parseDuration(durationRaw),\n created_at,\n };\n}\n\n/**\n * Read and parse all events from a single `*-events.log` file.\n *\n * @param filePath - Absolute path to the events log file.\n * @param repo - The repo name associated with this file.\n * @param onMalformedRow - Optional callback invoked for rows that cannot be parsed.\n * @returns An array of parsed `ToolEvent` objects from the file.\n */\nfunction readEventsFile(\n filePath: string,\n repo: string,\n onMalformedRow?: (line: string, reason: string) => void,\n): ToolEvent[] {\n const content = readFileSync(filePath, 'utf8');\n const lines = content.split('\\n').filter((line) => line.trim() !== '');\n const events: ToolEvent[] = [];\n\n for (const line of lines) {\n if (line === CSV_HEADER || line.startsWith('tool_name,')) {\n continue;\n }\n const event = parseRow(line, repo, onMalformedRow);\n if (event) {\n events.push(event);\n }\n }\n\n return events;\n}\n\n/**\n * Read all `*-events.log` files from the configured collection directory.\n *\n * @param options - Optional directory override and malformed-row callback.\n * @returns All parsed `ToolEvent` objects from all log files in the directory.\n */\nexport function readAllEvents(options: ReadAllEventsOptions = {}): ToolEvent[] {\n const dir = options.logDir ?? getEventsLogDir();\n if (!existsSync(dir)) {\n return [];\n }\n\n const files = readdirSync(dir).filter((name) => name.endsWith('-events.log'));\n const events: ToolEvent[] = [];\n\n for (const filename of files.sort()) {\n const repo = repoFromFilename(filename);\n events.push(\n ...readEventsFile(join(dir, filename), repo, options.onMalformedRow),\n );\n }\n\n return events;\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ToolEvent } from "./types.cjs";
|
|
2
|
+
/** Options for `readAllEvents`. */
|
|
3
|
+
export type ReadAllEventsOptions = {
|
|
4
|
+
/** Override for the directory to scan; defaults to `getEventsLogDir()`. */
|
|
5
|
+
logDir?: string;
|
|
6
|
+
/** Called for each row that cannot be parsed into a `ToolEvent`. */
|
|
7
|
+
onMalformedRow?: (line: string, reason: string) => void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Read all `*-events.log` files from the configured collection directory.
|
|
11
|
+
*
|
|
12
|
+
* @param options - Optional directory override and malformed-row callback.
|
|
13
|
+
* @returns All parsed `ToolEvent` objects from all log files in the directory.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readAllEvents(options?: ReadAllEventsOptions): ToolEvent[];
|
|
16
|
+
//# sourceMappingURL=csv.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.d.cts","sourceRoot":"","sources":["../../src/lib/csv.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAgB,SAAS,EAAY,oBAAgB;AAKjE,mCAAmC;AACnC,MAAM,MAAM,oBAAoB,GAAG;IACjC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACzD,CAAC;AAuKF;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,SAAS,EAAE,CAiB7E"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ToolEvent } from "./types.mjs";
|
|
2
|
+
/** Options for `readAllEvents`. */
|
|
3
|
+
export type ReadAllEventsOptions = {
|
|
4
|
+
/** Override for the directory to scan; defaults to `getEventsLogDir()`. */
|
|
5
|
+
logDir?: string;
|
|
6
|
+
/** Called for each row that cannot be parsed into a `ToolEvent`. */
|
|
7
|
+
onMalformedRow?: (line: string, reason: string) => void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Read all `*-events.log` files from the configured collection directory.
|
|
11
|
+
*
|
|
12
|
+
* @param options - Optional directory override and malformed-row callback.
|
|
13
|
+
* @returns All parsed `ToolEvent` objects from all log files in the directory.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readAllEvents(options?: ReadAllEventsOptions): ToolEvent[];
|
|
16
|
+
//# sourceMappingURL=csv.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.d.mts","sourceRoot":"","sources":["../../src/lib/csv.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAgB,SAAS,EAAY,oBAAgB;AAKjE,mCAAmC;AACnC,MAAM,MAAM,oBAAoB,GAAG;IACjC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACzD,CAAC;AAuKF;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,SAAS,EAAE,CAiB7E"}
|
package/dist/lib/csv.mjs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getEventsLogDir } from "./paths.mjs";
|
|
4
|
+
const CSV_HEADER = 'tool_name,tool_type,event_type,agent_vendor,session_id,success,duration_ms,created_at';
|
|
5
|
+
/**
|
|
6
|
+
* Strip the `-events.log` suffix from an event log filename to derive the repo name.
|
|
7
|
+
*
|
|
8
|
+
* @param filename - Event log filename (e.g. `metamask-mobile-events.log`).
|
|
9
|
+
* @returns The repo name portion (filename without the `-events.log` suffix).
|
|
10
|
+
*/
|
|
11
|
+
function repoFromFilename(filename) {
|
|
12
|
+
return filename.replace(/-events\.log$/u, '');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parse the raw `success` CSV column into a boolean or null.
|
|
16
|
+
*
|
|
17
|
+
* @param raw - Raw CSV column value.
|
|
18
|
+
* @returns `true`, `false`, or `null` when the value is unrecognised or empty.
|
|
19
|
+
*/
|
|
20
|
+
function parseSuccess(raw) {
|
|
21
|
+
if (raw === '') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (raw === 'true') {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (raw === 'false') {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse the raw `duration_ms` CSV column into a number or null.
|
|
34
|
+
*
|
|
35
|
+
* @param raw - Raw CSV column value.
|
|
36
|
+
* @returns A finite numeric duration, or `null` when the value is empty or non-numeric.
|
|
37
|
+
*/
|
|
38
|
+
function parseDuration(raw) {
|
|
39
|
+
if (raw === '') {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const value = Number(raw);
|
|
43
|
+
return Number.isFinite(value) ? value : null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse the raw `tool_type` CSV column into a `ToolType` or null.
|
|
47
|
+
*
|
|
48
|
+
* @param raw - Raw CSV column value.
|
|
49
|
+
* @returns The parsed `ToolType`, or `null` for unrecognised values.
|
|
50
|
+
*/
|
|
51
|
+
function parseToolType(raw) {
|
|
52
|
+
if (raw === 'skill' || raw === 'yarn_script') {
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse the raw `event_type` CSV column into a `CsvEventType` or null.
|
|
59
|
+
*
|
|
60
|
+
* @param raw - Raw CSV column value.
|
|
61
|
+
* @returns The parsed `CsvEventType`, or `null` for unrecognised values.
|
|
62
|
+
*/
|
|
63
|
+
function parseEventType(raw) {
|
|
64
|
+
if (raw === 'start' || raw === 'end' || raw === 'interrupted') {
|
|
65
|
+
return raw;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Parse a single CSV row into a `ToolEvent`, or null for invalid rows.
|
|
71
|
+
*
|
|
72
|
+
* @param line - A raw CSV row string.
|
|
73
|
+
* @param repo - The repo name derived from the log filename.
|
|
74
|
+
* @param onMalformedRow - Optional callback invoked for rows that cannot be parsed.
|
|
75
|
+
* @returns The parsed `ToolEvent`, or `null` for header, malformed, or filtered rows.
|
|
76
|
+
*/
|
|
77
|
+
function parseRow(line, repo, onMalformedRow) {
|
|
78
|
+
// The collector writes a fixed, controlled schema with no quoted/escaped
|
|
79
|
+
// fields, so a naive comma split is sufficient (and intentional).
|
|
80
|
+
const rawParts = line.split(',');
|
|
81
|
+
if (rawParts.length < 8) {
|
|
82
|
+
onMalformedRow?.(line, 'expected 8 columns');
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
// Safe to assert: we checked length >= 8 above, but TypeScript can't narrow
|
|
86
|
+
// array destructuring based on a length guard.
|
|
87
|
+
const parts = rawParts;
|
|
88
|
+
const [tool_name, tool_typeRaw, event_typeRaw, agent_vendor, session_id, successRaw, durationRaw, created_at,] = parts;
|
|
89
|
+
const tool_type = parseToolType(tool_typeRaw);
|
|
90
|
+
const event_type = parseEventType(event_typeRaw);
|
|
91
|
+
if (!tool_name || !tool_type || !event_type || !created_at) {
|
|
92
|
+
onMalformedRow?.(line, 'missing required fields');
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
repo,
|
|
97
|
+
tool_name,
|
|
98
|
+
tool_type,
|
|
99
|
+
event_type,
|
|
100
|
+
agent_vendor,
|
|
101
|
+
session_id,
|
|
102
|
+
success: parseSuccess(successRaw),
|
|
103
|
+
duration_ms: parseDuration(durationRaw),
|
|
104
|
+
created_at,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Read and parse all events from a single `*-events.log` file.
|
|
109
|
+
*
|
|
110
|
+
* @param filePath - Absolute path to the events log file.
|
|
111
|
+
* @param repo - The repo name associated with this file.
|
|
112
|
+
* @param onMalformedRow - Optional callback invoked for rows that cannot be parsed.
|
|
113
|
+
* @returns An array of parsed `ToolEvent` objects from the file.
|
|
114
|
+
*/
|
|
115
|
+
function readEventsFile(filePath, repo, onMalformedRow) {
|
|
116
|
+
const content = readFileSync(filePath, 'utf8');
|
|
117
|
+
const lines = content.split('\n').filter((line) => line.trim() !== '');
|
|
118
|
+
const events = [];
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
if (line === CSV_HEADER || line.startsWith('tool_name,')) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const event = parseRow(line, repo, onMalformedRow);
|
|
124
|
+
if (event) {
|
|
125
|
+
events.push(event);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return events;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Read all `*-events.log` files from the configured collection directory.
|
|
132
|
+
*
|
|
133
|
+
* @param options - Optional directory override and malformed-row callback.
|
|
134
|
+
* @returns All parsed `ToolEvent` objects from all log files in the directory.
|
|
135
|
+
*/
|
|
136
|
+
export function readAllEvents(options = {}) {
|
|
137
|
+
const dir = options.logDir ?? getEventsLogDir();
|
|
138
|
+
if (!existsSync(dir)) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
const files = readdirSync(dir).filter((name) => name.endsWith('-events.log'));
|
|
142
|
+
const events = [];
|
|
143
|
+
for (const filename of files.sort()) {
|
|
144
|
+
const repo = repoFromFilename(filename);
|
|
145
|
+
events.push(...readEventsFile(join(dir, filename), repo, options.onMalformedRow));
|
|
146
|
+
}
|
|
147
|
+
return events;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=csv.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.mjs","sourceRoot":"","sources":["../../src/lib/csv.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { getEventsLogDir } from './paths';\nimport type { CsvEventType, ToolEvent, ToolType } from './types';\n\nconst CSV_HEADER =\n 'tool_name,tool_type,event_type,agent_vendor,session_id,success,duration_ms,created_at';\n\n/** Options for `readAllEvents`. */\nexport type ReadAllEventsOptions = {\n /** Override for the directory to scan; defaults to `getEventsLogDir()`. */\n logDir?: string;\n /** Called for each row that cannot be parsed into a `ToolEvent`. */\n onMalformedRow?: (line: string, reason: string) => void;\n};\n\n/**\n * Strip the `-events.log` suffix from an event log filename to derive the repo name.\n *\n * @param filename - Event log filename (e.g. `metamask-mobile-events.log`).\n * @returns The repo name portion (filename without the `-events.log` suffix).\n */\nfunction repoFromFilename(filename: string): string {\n return filename.replace(/-events\\.log$/u, '');\n}\n\n/**\n * Parse the raw `success` CSV column into a boolean or null.\n *\n * @param raw - Raw CSV column value.\n * @returns `true`, `false`, or `null` when the value is unrecognised or empty.\n */\nfunction parseSuccess(raw: string): boolean | null {\n if (raw === '') {\n return null;\n }\n if (raw === 'true') {\n return true;\n }\n if (raw === 'false') {\n return false;\n }\n return null;\n}\n\n/**\n * Parse the raw `duration_ms` CSV column into a number or null.\n *\n * @param raw - Raw CSV column value.\n * @returns A finite numeric duration, or `null` when the value is empty or non-numeric.\n */\nfunction parseDuration(raw: string): number | null {\n if (raw === '') {\n return null;\n }\n const value = Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n\n/**\n * Parse the raw `tool_type` CSV column into a `ToolType` or null.\n *\n * @param raw - Raw CSV column value.\n * @returns The parsed `ToolType`, or `null` for unrecognised values.\n */\nfunction parseToolType(raw: string): ToolType | null {\n if (raw === 'skill' || raw === 'yarn_script') {\n return raw;\n }\n return null;\n}\n\n/**\n * Parse the raw `event_type` CSV column into a `CsvEventType` or null.\n *\n * @param raw - Raw CSV column value.\n * @returns The parsed `CsvEventType`, or `null` for unrecognised values.\n */\nfunction parseEventType(raw: string): CsvEventType | null {\n if (raw === 'start' || raw === 'end' || raw === 'interrupted') {\n return raw;\n }\n return null;\n}\n\n/**\n * Parse a single CSV row into a `ToolEvent`, or null for invalid rows.\n *\n * @param line - A raw CSV row string.\n * @param repo - The repo name derived from the log filename.\n * @param onMalformedRow - Optional callback invoked for rows that cannot be parsed.\n * @returns The parsed `ToolEvent`, or `null` for header, malformed, or filtered rows.\n */\nfunction parseRow(\n line: string,\n repo: string,\n onMalformedRow?: (line: string, reason: string) => void,\n): ToolEvent | null {\n // The collector writes a fixed, controlled schema with no quoted/escaped\n // fields, so a naive comma split is sufficient (and intentional).\n const rawParts = line.split(',');\n if (rawParts.length < 8) {\n onMalformedRow?.(line, 'expected 8 columns');\n return null;\n }\n // Safe to assert: we checked length >= 8 above, but TypeScript can't narrow\n // array destructuring based on a length guard.\n const parts = rawParts as [\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n ...string[],\n ];\n\n const [\n tool_name,\n tool_typeRaw,\n event_typeRaw,\n agent_vendor,\n session_id,\n successRaw,\n durationRaw,\n created_at,\n ] = parts;\n\n const tool_type = parseToolType(tool_typeRaw);\n const event_type = parseEventType(event_typeRaw);\n\n if (!tool_name || !tool_type || !event_type || !created_at) {\n onMalformedRow?.(line, 'missing required fields');\n return null;\n }\n\n return {\n repo,\n tool_name,\n tool_type,\n event_type,\n agent_vendor,\n session_id,\n success: parseSuccess(successRaw),\n duration_ms: parseDuration(durationRaw),\n created_at,\n };\n}\n\n/**\n * Read and parse all events from a single `*-events.log` file.\n *\n * @param filePath - Absolute path to the events log file.\n * @param repo - The repo name associated with this file.\n * @param onMalformedRow - Optional callback invoked for rows that cannot be parsed.\n * @returns An array of parsed `ToolEvent` objects from the file.\n */\nfunction readEventsFile(\n filePath: string,\n repo: string,\n onMalformedRow?: (line: string, reason: string) => void,\n): ToolEvent[] {\n const content = readFileSync(filePath, 'utf8');\n const lines = content.split('\\n').filter((line) => line.trim() !== '');\n const events: ToolEvent[] = [];\n\n for (const line of lines) {\n if (line === CSV_HEADER || line.startsWith('tool_name,')) {\n continue;\n }\n const event = parseRow(line, repo, onMalformedRow);\n if (event) {\n events.push(event);\n }\n }\n\n return events;\n}\n\n/**\n * Read all `*-events.log` files from the configured collection directory.\n *\n * @param options - Optional directory override and malformed-row callback.\n * @returns All parsed `ToolEvent` objects from all log files in the directory.\n */\nexport function readAllEvents(options: ReadAllEventsOptions = {}): ToolEvent[] {\n const dir = options.logDir ?? getEventsLogDir();\n if (!existsSync(dir)) {\n return [];\n }\n\n const files = readdirSync(dir).filter((name) => name.endsWith('-events.log'));\n const events: ToolEvent[] = [];\n\n for (const filename of files.sort()) {\n const repo = repoFromFilename(filename);\n events.push(\n ...readEventsFile(join(dir, filename), repo, options.onMalformedRow),\n );\n }\n\n return events;\n}\n"]}
|