@hasna/skills 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +136 -18
  2. package/bin/index.js +33877 -33924
  3. package/bin/mcp.js +196 -95
  4. package/dist/cli/commands/completion.d.ts +5 -0
  5. package/dist/cli/commands/create-sync-config.d.ts +5 -0
  6. package/dist/cli/commands/diagnostic.d.ts +5 -0
  7. package/dist/cli/commands/init.d.ts +5 -0
  8. package/dist/cli/commands/install.d.ts +5 -0
  9. package/dist/cli/commands/introspect.d.ts +5 -0
  10. package/dist/cli/commands/list.d.ts +5 -0
  11. package/dist/cli/commands/runtime.d.ts +5 -0
  12. package/dist/cli/commands/schedule.d.ts +5 -0
  13. package/dist/index.js +197 -97
  14. package/dist/lib/config.d.ts +1 -1
  15. package/dist/lib/registry.d.ts +1 -11
  16. package/dist/lib/scheduler.d.ts +1 -1
  17. package/dist/lib/scheduler.test.d.ts +4 -0
  18. package/dist/lib/search.d.ts +17 -0
  19. package/package.json +1 -1
  20. package/skills/skill-commitpush/SKILL.md +57 -0
  21. package/skills/skill-commitpush/package.json +34 -0
  22. package/skills/skill-commitpush/src/index.ts +34 -0
  23. package/skills/skill-commitpush/tsconfig.json +17 -0
  24. package/skills/skill-commitpushpr/SKILL.md +55 -0
  25. package/skills/skill-commitpushpr/package.json +34 -0
  26. package/skills/skill-commitpushpr/src/index.ts +34 -0
  27. package/skills/skill-commitpushpr/tsconfig.json +17 -0
  28. package/skills/skill-monitor/SKILL.md +69 -0
  29. package/skills/skill-monitor/package.json +34 -0
  30. package/skills/skill-monitor/src/index.ts +34 -0
  31. package/skills/skill-monitor/tsconfig.json +17 -0
  32. package/skills/skill-read-csv/SKILL.md +62 -0
  33. package/skills/skill-read-csv/package.json +38 -0
  34. package/skills/skill-read-csv/src/index.ts +331 -0
  35. package/skills/skill-read-csv/tsconfig.json +17 -0
  36. package/skills/skill-read-excel/SKILL.md +64 -0
  37. package/skills/skill-read-excel/package.json +37 -0
  38. package/skills/skill-read-excel/src/index.ts +253 -0
  39. package/skills/skill-read-excel/tsconfig.json +17 -0
  40. package/skills/skill-read-image/SKILL.md +47 -0
  41. package/skills/skill-read-image/package.json +34 -0
  42. package/skills/skill-read-image/src/index.ts +264 -0
  43. package/skills/skill-read-image/tsconfig.json +17 -0
  44. package/skills/skill-read-pdf/SKILL.md +52 -0
  45. package/skills/skill-read-pdf/package.json +37 -0
  46. package/skills/skill-read-pdf/src/index.ts +376 -0
  47. package/skills/skill-read-pdf/tsconfig.json +17 -0
  48. package/skills/skill-tmux-session/SKILL.md +109 -0
  49. package/skills/skill-tmux-session/package.json +34 -0
  50. package/skills/skill-tmux-session/src/index.ts +34 -0
  51. package/skills/skill-tmux-session/tsconfig.json +17 -0
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bun
2
+
3
+ const VERSION = "0.1.0";
4
+
5
+ function printHelp(): void {
6
+ console.log(`skill-commitpushpr - Instruction-set skill
7
+
8
+ DESCRIPTION:
9
+ Guides an agent through:
10
+ - scanning repo changes
11
+ - grouping them into logical commits
12
+ - pushing to a feature branch
13
+ - creating a pull request with gh
14
+
15
+ USAGE:
16
+ skills docs commitpushpr
17
+ skills install commitpushpr --for claude
18
+ `);
19
+ }
20
+
21
+ const args = process.argv.slice(2);
22
+
23
+ if (args.includes("--version") || args.includes("-v")) {
24
+ console.log(VERSION);
25
+ process.exit(0);
26
+ }
27
+
28
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
29
+ printHelp();
30
+ process.exit(0);
31
+ }
32
+
33
+ console.log("This is an instruction-set skill. Install it for an agent with:");
34
+ console.log(" skills install commitpushpr --for claude");
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "resolveJsonModule": true,
8
+ "noEmit": true
9
+ },
10
+ "include": [
11
+ "src/**/*"
12
+ ],
13
+ "exclude": [
14
+ "node_modules",
15
+ "dist"
16
+ ]
17
+ }
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: skill-monitor
3
+ description: Interact with the open-monitor MCP — check machine health, list processes, kill memory hogs, run doctor, manage cron jobs, trigger cache cleanup
4
+ user_invocable: true
5
+ ---
6
+
7
+ # skill-monitor
8
+
9
+ Use the `monitor_*` MCP tools from `@hasna/monitor` to help the user manage their machines.
10
+
11
+ ## Prerequisites check
12
+
13
+ If any `monitor_*` tool call fails with "tool not found" or "MCP not connected", tell the user:
14
+
15
+ ```text
16
+ The open-monitor MCP is not installed. Add it with:
17
+ claude mcp add monitor -- monitor-mcp
18
+ Then restart Claude Code and try again.
19
+ ```
20
+
21
+ ## Use cases
22
+
23
+ ### Check machine health
24
+ **Trigger**: "how is the machine doing?", "machine health", "system status", "is everything ok?"
25
+
26
+ 1. Call `monitor_health` (no args, or `machine_id: "local"`)
27
+ 2. Call `monitor_snapshot` to get current CPU/memory/disk
28
+ 3. Summarize: overall status, any alerts, top resource users
29
+
30
+ ### List memory hogs
31
+ **Trigger**: "what's eating memory?", "high memory processes", "memory usage", "what's using RAM?"
32
+
33
+ 1. Call `monitor_processes`
34
+ 2. Show a table: PID, name, memory (MB), CPU%
35
+ 3. Offer to kill any if the user wants
36
+
37
+ ### Kill a process
38
+ **Trigger**: "kill pid 1234", "kill process X", "stop process Y"
39
+
40
+ 1. Confirm the process name/PID with the user if ambiguous
41
+ 2. Call `monitor_kill` with `pid: <pid>` and `signal: "SIGTERM"`
42
+ 3. Report the result; if it failed, offer `SIGKILL`
43
+
44
+ ### Run doctor
45
+ **Trigger**: "run doctor", "doctor check", "health check", "diagnose"
46
+
47
+ 1. Call `monitor_doctor` (optionally with `machine_id`)
48
+ 2. Show the full report: status per check, recommended actions
49
+ 3. If any checks failed, summarize what to fix
50
+
51
+ ### Trigger cache cleanup
52
+ **Trigger**: "clean caches", "clear dev caches", "free up disk", "cleanup npm/bun/pip cache"
53
+
54
+ 1. Find the `cache-cleaner` cron job: call `monitor_cron_jobs` with `action: "list"`
55
+ 2. Run it with `monitor_cron_jobs`
56
+ 3. Report what changed
57
+
58
+ ### List cron jobs
59
+ **Trigger**: "show monitor jobs", "what cron jobs are set up?", "monitor schedule"
60
+
61
+ 1. Call `monitor_cron_jobs` with `action: "list"`
62
+ 2. Show a table: name, schedule, action type, enabled, last run
63
+ 3. Offer to enable/disable or run any job
64
+
65
+ ## Output style
66
+
67
+ - Use tables for process lists and cron job lists
68
+ - Lead with the summary and keep details below it
69
+ - Always show the machine ID being queried (default: local)
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@hasnaxyz/skill-monitor",
3
+ "version": "0.1.0",
4
+ "description": "Instruction-set skill for operating the open-monitor MCP",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "bin": {
8
+ "skill-monitor": "src/index.ts"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "SKILL.md",
13
+ "tsconfig.json"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "restricted"
17
+ },
18
+ "keywords": [
19
+ "monitor",
20
+ "mcp",
21
+ "machine",
22
+ "ops",
23
+ "skill"
24
+ ],
25
+ "license": "Apache-2.0",
26
+ "scripts": {
27
+ "start": "bun run src/index.ts",
28
+ "typecheck": "tsc --noEmit"
29
+ },
30
+ "devDependencies": {
31
+ "@types/bun": "latest",
32
+ "typescript": "^5.7.0"
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bun
2
+
3
+ const VERSION = "0.1.0";
4
+
5
+ function printHelp(): void {
6
+ console.log(`skill-monitor - Instruction-set skill
7
+
8
+ DESCRIPTION:
9
+ Guides an agent through common open-monitor MCP workflows:
10
+ - machine health and doctor checks
11
+ - process inspection and safe termination
12
+ - cron job inspection and execution
13
+ - cache cleanup and operational summaries
14
+
15
+ USAGE:
16
+ skills docs monitor
17
+ skills install monitor --for claude
18
+ `);
19
+ }
20
+
21
+ const args = process.argv.slice(2);
22
+
23
+ if (args.includes("--version") || args.includes("-v")) {
24
+ console.log(VERSION);
25
+ process.exit(0);
26
+ }
27
+
28
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
29
+ printHelp();
30
+ process.exit(0);
31
+ }
32
+
33
+ console.log("This is an instruction-set skill. Install it for an agent with:");
34
+ console.log(" skills install monitor --for claude");
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "resolveJsonModule": true,
8
+ "noEmit": true
9
+ },
10
+ "include": [
11
+ "src/**/*"
12
+ ],
13
+ "exclude": [
14
+ "node_modules",
15
+ "dist"
16
+ ]
17
+ }
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: skill-read-csv
3
+ description: Parse CSV files into structured JSON with delimiter auto-detection, header handling, common encoding support, and streaming reads for large files.
4
+ ---
5
+
6
+ # Read CSV
7
+
8
+ Parse CSV files from disk and return structured JSON that is easy to inspect, transform, or hand to another tool.
9
+
10
+ ## Features
11
+
12
+ - Delimiter auto-detection for comma, tab, semicolon, and pipe
13
+ - Header detection with `auto`, `true`, and `false` modes
14
+ - Common encoding support (`utf8`, `utf16le`, `utf16be`, `latin1`, `win1252`)
15
+ - Streaming parser for large files
16
+ - Optional row limits and file output
17
+
18
+ ## Usage
19
+
20
+ ```bash
21
+ # Parse a CSV with auto-detected headers and delimiter
22
+ skill-read-csv --input ./customers.csv
23
+
24
+ # Force tab-delimited parsing and save output
25
+ skill-read-csv --input ./report.tsv --delimiter tab --output ./report.json
26
+
27
+ # Parse with explicit header handling
28
+ skill-read-csv --input ./raw.csv --headers false
29
+
30
+ # Preview the first 100 rows only
31
+ skill-read-csv --input ./large.csv --limit 100
32
+ ```
33
+
34
+ ## Options
35
+
36
+ | Option | Description | Default |
37
+ |--------|-------------|---------|
38
+ | `-i, --input <path>` | CSV file to parse | required |
39
+ | `-d, --delimiter <value>` | Delimiter (`comma`, `tab`, `semicolon`, `pipe`, or literal character) | auto |
40
+ | `-e, --encoding <value>` | Input encoding (`auto`, `utf8`, `utf16le`, `utf16be`, `latin1`, `win1252`) | auto |
41
+ | `--headers <mode>` | Header mode: `auto`, `true`, `false` | auto |
42
+ | `-l, --limit <n>` | Stop after parsing `n` rows | unlimited |
43
+ | `-o, --output <path>` | Save JSON result to a file | stdout |
44
+ | `--help` | Show usage | |
45
+ | `--version` | Show version | |
46
+
47
+ ## Output Shape
48
+
49
+ ```json
50
+ {
51
+ "input": "/absolute/path/to/file.csv",
52
+ "encoding": "utf8",
53
+ "delimiter": ",",
54
+ "hasHeader": true,
55
+ "columns": ["id", "email", "plan"],
56
+ "rowCount": 2,
57
+ "truncated": false,
58
+ "rows": [
59
+ { "id": "1", "email": "a@example.com", "plan": "pro" }
60
+ ]
61
+ }
62
+ ```
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@hasnaxyz/skill-read-csv",
3
+ "version": "0.1.0",
4
+ "description": "Parse CSV files into structured JSON with delimiter and encoding detection",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "bin": {
8
+ "skill-read-csv": "src/index.ts"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "SKILL.md",
13
+ "tsconfig.json"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "restricted"
17
+ },
18
+ "keywords": [
19
+ "csv",
20
+ "parser",
21
+ "data",
22
+ "cli",
23
+ "skill"
24
+ ],
25
+ "license": "Apache-2.0",
26
+ "scripts": {
27
+ "start": "bun run src/index.ts",
28
+ "typecheck": "tsc --noEmit"
29
+ },
30
+ "dependencies": {
31
+ "csv-parse": "^5.5.6",
32
+ "iconv-lite": "^0.6.3"
33
+ },
34
+ "devDependencies": {
35
+ "@types/bun": "latest",
36
+ "typescript": "^5.7.0"
37
+ }
38
+ }
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { createReadStream } from "fs";
4
+ import { mkdir, open, readFile, writeFile } from "fs/promises";
5
+ import { dirname, resolve } from "path";
6
+ import { parse as createParser } from "csv-parse";
7
+ import { parse as parseCsvSync } from "csv-parse/sync";
8
+ import iconv from "iconv-lite";
9
+
10
+ const VERSION = "0.1.0";
11
+ const SAMPLE_BYTES = 128 * 1024;
12
+
13
+ type HeaderMode = "auto" | "true" | "false";
14
+
15
+ interface CliOptions {
16
+ input?: string;
17
+ delimiter?: string;
18
+ encoding: string;
19
+ headers: HeaderMode;
20
+ limit?: number;
21
+ output?: string;
22
+ }
23
+
24
+ interface CsvResult {
25
+ input: string;
26
+ encoding: string;
27
+ delimiter: string;
28
+ hasHeader: boolean;
29
+ columns: string[];
30
+ rowCount: number;
31
+ truncated: boolean;
32
+ rows: Array<Record<string, string | null>>;
33
+ }
34
+
35
+ function printHelp(): void {
36
+ console.log(`skill-read-csv v${VERSION}
37
+
38
+ USAGE:
39
+ skill-read-csv --input <path> [options]
40
+
41
+ OPTIONS:
42
+ -i, --input <path> CSV file to parse
43
+ -d, --delimiter <value> comma | tab | semicolon | pipe | literal character
44
+ -e, --encoding <value> auto | utf8 | utf16le | utf16be | latin1 | win1252
45
+ --headers <mode> auto | true | false
46
+ -l, --limit <n> Stop after parsing n rows
47
+ -o, --output <path> Save JSON result to a file
48
+ --help Show this help message
49
+ --version Show the current version
50
+ `);
51
+ }
52
+
53
+ function parseArgs(argv: string[]): CliOptions {
54
+ const options: CliOptions = {
55
+ encoding: "auto",
56
+ headers: "auto",
57
+ };
58
+
59
+ for (let i = 0; i < argv.length; i += 1) {
60
+ const arg = argv[i];
61
+
62
+ switch (arg) {
63
+ case "--help":
64
+ case "-h":
65
+ printHelp();
66
+ process.exit(0);
67
+ case "--version":
68
+ case "-v":
69
+ console.log(VERSION);
70
+ process.exit(0);
71
+ case "--input":
72
+ case "-i":
73
+ options.input = argv[++i];
74
+ break;
75
+ case "--delimiter":
76
+ case "-d":
77
+ options.delimiter = argv[++i];
78
+ break;
79
+ case "--encoding":
80
+ case "-e":
81
+ options.encoding = argv[++i] ?? "auto";
82
+ break;
83
+ case "--headers": {
84
+ const value = (argv[++i] ?? "auto").toLowerCase();
85
+ if (value !== "auto" && value !== "true" && value !== "false") {
86
+ throw new Error(`Invalid --headers value: ${value}`);
87
+ }
88
+ options.headers = value;
89
+ break;
90
+ }
91
+ case "--limit":
92
+ case "-l": {
93
+ const value = Number.parseInt(argv[++i] ?? "", 10);
94
+ if (!Number.isFinite(value) || value <= 0) {
95
+ throw new Error(`Invalid --limit value: ${argv[i]}`);
96
+ }
97
+ options.limit = value;
98
+ break;
99
+ }
100
+ case "--output":
101
+ case "-o":
102
+ options.output = argv[++i];
103
+ break;
104
+ default:
105
+ if (arg.startsWith("-")) {
106
+ throw new Error(`Unknown option: ${arg}`);
107
+ }
108
+ if (!options.input) {
109
+ options.input = arg;
110
+ break;
111
+ }
112
+ throw new Error(`Unexpected argument: ${arg}`);
113
+ }
114
+ }
115
+
116
+ if (!options.input) {
117
+ throw new Error("Missing required --input <path> argument");
118
+ }
119
+
120
+ return options;
121
+ }
122
+
123
+ function normalizeDelimiter(input?: string): string | undefined {
124
+ if (!input) return undefined;
125
+ const value = input.toLowerCase();
126
+ switch (value) {
127
+ case "comma":
128
+ return ",";
129
+ case "tab":
130
+ return "\t";
131
+ case "semicolon":
132
+ return ";";
133
+ case "pipe":
134
+ return "|";
135
+ default:
136
+ return input;
137
+ }
138
+ }
139
+
140
+ async function readSample(path: string, byteCount = SAMPLE_BYTES): Promise<Buffer> {
141
+ const handle = await open(path, "r");
142
+ try {
143
+ const buffer = Buffer.alloc(byteCount);
144
+ const { bytesRead } = await handle.read(buffer, 0, byteCount, 0);
145
+ return buffer.subarray(0, bytesRead);
146
+ } finally {
147
+ await handle.close();
148
+ }
149
+ }
150
+
151
+ function detectEncoding(sample: Buffer, requested: string): { encoding: string; bomBytes: number } {
152
+ if (requested !== "auto") {
153
+ return { encoding: requested, bomBytes: 0 };
154
+ }
155
+
156
+ if (sample.length >= 3 && sample[0] === 0xef && sample[1] === 0xbb && sample[2] === 0xbf) {
157
+ return { encoding: "utf8", bomBytes: 3 };
158
+ }
159
+ if (sample.length >= 2 && sample[0] === 0xff && sample[1] === 0xfe) {
160
+ return { encoding: "utf16le", bomBytes: 2 };
161
+ }
162
+ if (sample.length >= 2 && sample[0] === 0xfe && sample[1] === 0xff) {
163
+ return { encoding: "utf16be", bomBytes: 2 };
164
+ }
165
+
166
+ return { encoding: "utf8", bomBytes: 0 };
167
+ }
168
+
169
+ function detectDelimiter(sampleText: string): string {
170
+ const candidates = [",", "\t", ";", "|"];
171
+ const lines = sampleText
172
+ .split(/\r?\n/)
173
+ .map((line) => line.trim())
174
+ .filter(Boolean)
175
+ .slice(0, 8);
176
+
177
+ if (lines.length === 0) {
178
+ return ",";
179
+ }
180
+
181
+ let bestDelimiter = ",";
182
+ let bestScore = Number.NEGATIVE_INFINITY;
183
+
184
+ for (const delimiter of candidates) {
185
+ const counts = lines.map((line) => line.split(delimiter).length - 1);
186
+ const total = counts.reduce((sum, count) => sum + count, 0);
187
+ const variance = counts.reduce((sum, count) => sum + Math.abs(count - counts[0]), 0);
188
+ const score = total * 10 - variance;
189
+ if (score > bestScore) {
190
+ bestScore = score;
191
+ bestDelimiter = delimiter;
192
+ }
193
+ }
194
+
195
+ return bestDelimiter;
196
+ }
197
+
198
+ function parseSampleRows(sampleText: string, delimiter: string): string[][] {
199
+ const records = parseCsvSync(sampleText, {
200
+ delimiter,
201
+ relax_column_count: true,
202
+ skip_empty_lines: true,
203
+ to_line: 5,
204
+ }) as string[][];
205
+ return records;
206
+ }
207
+
208
+ function looksNumeric(value: string): boolean {
209
+ return value.trim() !== "" && !Number.isNaN(Number(value));
210
+ }
211
+
212
+ function normalizeColumnName(value: string, index: number, seen: Set<string>): string {
213
+ const base = value
214
+ .trim()
215
+ .toLowerCase()
216
+ .replace(/[^a-z0-9]+/g, "_")
217
+ .replace(/^_+|_+$/g, "") || `column_${index + 1}`;
218
+
219
+ let next = base;
220
+ let suffix = 2;
221
+ while (seen.has(next)) {
222
+ next = `${base}_${suffix}`;
223
+ suffix += 1;
224
+ }
225
+ seen.add(next);
226
+ return next;
227
+ }
228
+
229
+ function inferHeader(sampleRows: string[][]): boolean {
230
+ if (sampleRows.length < 2) {
231
+ return false;
232
+ }
233
+
234
+ const first = sampleRows[0];
235
+ const second = sampleRows[1];
236
+ const unique = new Set(first.map((value) => value.trim().toLowerCase()).filter(Boolean));
237
+ const mostlyText = first.filter((value) => !looksNumeric(value)).length >= Math.ceil(first.length / 2);
238
+ const secondHasNumeric = second.some((value) => looksNumeric(value));
239
+
240
+ return unique.size === first.length && mostlyText && secondHasNumeric;
241
+ }
242
+
243
+ function buildColumns(sampleRows: string[][], headerMode: HeaderMode): { hasHeader: boolean; columns: string[] } {
244
+ const firstRow = sampleRows[0] ?? [];
245
+ const seen = new Set<string>();
246
+
247
+ if (headerMode === "true" || (headerMode === "auto" && inferHeader(sampleRows))) {
248
+ return {
249
+ hasHeader: true,
250
+ columns: firstRow.map((value, index) => normalizeColumnName(String(value), index, seen)),
251
+ };
252
+ }
253
+
254
+ return {
255
+ hasHeader: false,
256
+ columns: firstRow.map((_, index) => normalizeColumnName(`column_${index + 1}`, index, seen)),
257
+ };
258
+ }
259
+
260
+ async function parseCsvFile(
261
+ path: string,
262
+ options: CliOptions,
263
+ encoding: string,
264
+ bomBytes: number,
265
+ delimiter: string,
266
+ columns: string[],
267
+ hasHeader: boolean,
268
+ ): Promise<{ rows: Array<Record<string, string | null>>; truncated: boolean }> {
269
+ const rows: Array<Record<string, string | null>> = [];
270
+ let truncated = false;
271
+ const stream = createReadStream(path, bomBytes > 0 ? { start: bomBytes } : undefined)
272
+ .pipe(iconv.decodeStream(encoding))
273
+ .pipe(createParser({
274
+ delimiter,
275
+ columns: hasHeader ? true : columns,
276
+ relax_column_count: true,
277
+ skip_empty_lines: true,
278
+ trim: false,
279
+ }));
280
+
281
+ for await (const record of stream) {
282
+ rows.push(record as Record<string, string | null>);
283
+ if (options.limit && rows.length >= options.limit) {
284
+ truncated = true;
285
+ break;
286
+ }
287
+ }
288
+
289
+ stream.destroy();
290
+ return { rows, truncated };
291
+ }
292
+
293
+ async function writeJson(path: string, payload: CsvResult): Promise<void> {
294
+ await mkdir(dirname(path), { recursive: true });
295
+ await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
296
+ }
297
+
298
+ async function main(): Promise<void> {
299
+ const options = parseArgs(process.argv.slice(2));
300
+ const inputPath = resolve(options.input!);
301
+ const sample = await readSample(inputPath);
302
+ const { encoding, bomBytes } = detectEncoding(sample, options.encoding.toLowerCase());
303
+ const sampleText = iconv.decode(sample.subarray(bomBytes), encoding);
304
+ const delimiter = normalizeDelimiter(options.delimiter) ?? detectDelimiter(sampleText);
305
+ const sampleRows = parseSampleRows(sampleText, delimiter);
306
+ const { hasHeader, columns } = buildColumns(sampleRows, options.headers);
307
+ const { rows, truncated } = await parseCsvFile(inputPath, options, encoding, bomBytes, delimiter, columns, hasHeader);
308
+
309
+ const result: CsvResult = {
310
+ input: inputPath,
311
+ encoding,
312
+ delimiter,
313
+ hasHeader,
314
+ columns,
315
+ rowCount: rows.length,
316
+ truncated,
317
+ rows,
318
+ };
319
+
320
+ if (options.output) {
321
+ await writeJson(resolve(options.output), result);
322
+ } else {
323
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
324
+ }
325
+ }
326
+
327
+ main().catch((error) => {
328
+ const message = error instanceof Error ? error.message : String(error);
329
+ process.stderr.write(`skill-read-csv: ${message}\n`);
330
+ process.exit(1);
331
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "resolveJsonModule": true,
8
+ "noEmit": true
9
+ },
10
+ "include": [
11
+ "src/**/*"
12
+ ],
13
+ "exclude": [
14
+ "node_modules",
15
+ "dist"
16
+ ]
17
+ }