@crafter/cli-tree 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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/archaeology/cache.d.ts +11 -0
  4. package/dist/archaeology/delegate.d.ts +43 -0
  5. package/dist/archaeology/index.d.ts +12 -0
  6. package/dist/archaeology/index.js +61 -0
  7. package/dist/archaeology/index.js.map +9 -0
  8. package/dist/archaeology/llm.d.ts +1 -0
  9. package/dist/archaeology/merge.d.ts +3 -0
  10. package/dist/archaeology/orchestrator.d.ts +25 -0
  11. package/dist/archaeology/prompts.d.ts +13 -0
  12. package/dist/archaeology/types.d.ts +101 -0
  13. package/dist/archaeology/validate.d.ts +18 -0
  14. package/dist/chunk-57gtsvhb.js +434 -0
  15. package/dist/chunk-57gtsvhb.js.map +16 -0
  16. package/dist/chunk-5aahbfr2.js +293 -0
  17. package/dist/chunk-5aahbfr2.js.map +10 -0
  18. package/dist/chunk-pkfpaae1.js +678 -0
  19. package/dist/chunk-pkfpaae1.js.map +15 -0
  20. package/dist/chunk-q4se2rwe.js +181 -0
  21. package/dist/chunk-q4se2rwe.js.map +14 -0
  22. package/dist/chunk-v5w3w6bd.js +168 -0
  23. package/dist/chunk-v5w3w6bd.js.map +11 -0
  24. package/dist/chunk-ykze151b.js +770 -0
  25. package/dist/chunk-ykze151b.js.map +16 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +433 -0
  28. package/dist/cli.js.map +10 -0
  29. package/dist/encoders/ansi.d.ts +2 -0
  30. package/dist/encoders/html.d.ts +10 -0
  31. package/dist/encoders/string.d.ts +2 -0
  32. package/dist/flow/encode.d.ts +5 -0
  33. package/dist/flow/index.d.ts +8 -0
  34. package/dist/flow/index.js +25 -0
  35. package/dist/flow/index.js.map +9 -0
  36. package/dist/flow/layout.d.ts +30 -0
  37. package/dist/flow/parse.d.ts +2 -0
  38. package/dist/flow/render.d.ts +3 -0
  39. package/dist/flow/types.d.ts +42 -0
  40. package/dist/flow/validate.d.ts +3 -0
  41. package/dist/flow/yaml.d.ts +4 -0
  42. package/dist/grid.d.ts +14 -0
  43. package/dist/index.d.ts +7 -0
  44. package/dist/index.js +21 -0
  45. package/dist/index.js.map +9 -0
  46. package/dist/miner/history.d.ts +6 -0
  47. package/dist/miner/index.d.ts +18 -0
  48. package/dist/miner/index.js +38 -0
  49. package/dist/miner/index.js.map +9 -0
  50. package/dist/miner/sessions.d.ts +3 -0
  51. package/dist/miner/stats.d.ts +2 -0
  52. package/dist/miner/suggest.d.ts +11 -0
  53. package/dist/miner/transitions.d.ts +6 -0
  54. package/dist/miner/types.d.ts +46 -0
  55. package/dist/miner/workflows.d.ts +11 -0
  56. package/dist/parse.d.ts +3 -0
  57. package/dist/render.d.ts +3 -0
  58. package/dist/types.d.ts +39 -0
  59. package/package.json +85 -0
  60. package/skill/SKILL.md +263 -0
  61. package/skill/evals/evals.json +26 -0
  62. package/skill/install.sh +38 -0
  63. package/skill/references/archaeology-guide.md +157 -0
  64. package/skill/references/skill-template.md +120 -0
  65. package/src/archaeology/cache.ts +107 -0
  66. package/src/archaeology/delegate.ts +113 -0
  67. package/src/archaeology/index.ts +48 -0
  68. package/src/archaeology/llm.ts +10 -0
  69. package/src/archaeology/merge.ts +155 -0
  70. package/src/archaeology/orchestrator.ts +185 -0
  71. package/src/archaeology/prompts.ts +178 -0
  72. package/src/archaeology/types.ts +139 -0
  73. package/src/archaeology/validate.ts +157 -0
  74. package/src/cli.ts +451 -0
  75. package/src/encoders/ansi.ts +32 -0
  76. package/src/encoders/html.ts +78 -0
  77. package/src/encoders/string.ts +20 -0
  78. package/src/flow/encode.ts +21 -0
  79. package/src/flow/index.ts +15 -0
  80. package/src/flow/layout.ts +150 -0
  81. package/src/flow/parse.ts +100 -0
  82. package/src/flow/render.ts +186 -0
  83. package/src/flow/types.ts +45 -0
  84. package/src/flow/validate.ts +111 -0
  85. package/src/flow/yaml.ts +235 -0
  86. package/src/grid.ts +59 -0
  87. package/src/index.ts +24 -0
  88. package/src/miner/history.ts +156 -0
  89. package/src/miner/index.ts +76 -0
  90. package/src/miner/sessions.ts +39 -0
  91. package/src/miner/stats.ts +43 -0
  92. package/src/miner/suggest.ts +101 -0
  93. package/src/miner/transitions.ts +62 -0
  94. package/src/miner/types.ts +45 -0
  95. package/src/miner/workflows.ts +96 -0
  96. package/src/parse.ts +321 -0
  97. package/src/render.ts +182 -0
  98. package/src/types.ts +62 -0
  99. package/workflows/docker-deploy.yml +42 -0
  100. package/workflows/docker-parallel.yml +36 -0
  101. package/workflows/gh-issue-to-pr.yml +48 -0
  102. package/workflows/git-pr-flow.yml +36 -0
  103. package/workflows/kubectl-rollout.yml +37 -0
  104. package/workflows/npm-publish.yml +42 -0
@@ -0,0 +1,235 @@
1
+ export type YamlValue = string | number | boolean | null | YamlValue[] | { [key: string]: YamlValue };
2
+
3
+ export function parseYaml(text: string): YamlValue {
4
+ const lines = text.split("\n");
5
+ const cleaned: { indent: number; content: string; lineNum: number }[] = [];
6
+
7
+ for (let i = 0; i < lines.length; i++) {
8
+ const raw = lines[i]!;
9
+ const commentIdx = findCommentStart(raw);
10
+ const withoutComment = commentIdx >= 0 ? raw.slice(0, commentIdx) : raw;
11
+ const trimmed = withoutComment.trimEnd();
12
+ if (!trimmed.trim()) continue;
13
+
14
+ const indent = trimmed.length - trimmed.trimStart().length;
15
+ cleaned.push({ indent, content: trimmed.trim(), lineNum: i + 1 });
16
+ }
17
+
18
+ const result = parseBlock(cleaned, 0, 0);
19
+ return result.value;
20
+ }
21
+
22
+ function findCommentStart(line: string): number {
23
+ let inString: string | null = null;
24
+ for (let i = 0; i < line.length; i++) {
25
+ const ch = line[i];
26
+ if (inString) {
27
+ if (ch === inString && line[i - 1] !== "\\") inString = null;
28
+ } else {
29
+ if (ch === '"' || ch === "'") inString = ch;
30
+ else if (ch === "#" && (i === 0 || line[i - 1] === " " || line[i - 1] === "\t")) {
31
+ return i;
32
+ }
33
+ }
34
+ }
35
+ return -1;
36
+ }
37
+
38
+ interface ParseState {
39
+ value: YamlValue;
40
+ nextIdx: number;
41
+ }
42
+
43
+ function parseBlock(
44
+ lines: { indent: number; content: string; lineNum: number }[],
45
+ startIdx: number,
46
+ baseIndent: number,
47
+ ): ParseState {
48
+ if (startIdx >= lines.length) return { value: null, nextIdx: startIdx };
49
+
50
+ const first = lines[startIdx]!;
51
+ if (first.indent < baseIndent) return { value: null, nextIdx: startIdx };
52
+
53
+ if (first.content.startsWith("- ") || first.content === "-") {
54
+ return parseList(lines, startIdx, first.indent);
55
+ }
56
+
57
+ if (first.content.includes(":")) {
58
+ return parseMap(lines, startIdx, first.indent);
59
+ }
60
+
61
+ return { value: parseScalar(first.content), nextIdx: startIdx + 1 };
62
+ }
63
+
64
+ function parseList(
65
+ lines: { indent: number; content: string; lineNum: number }[],
66
+ startIdx: number,
67
+ listIndent: number,
68
+ ): ParseState {
69
+ const items: YamlValue[] = [];
70
+ let i = startIdx;
71
+
72
+ while (i < lines.length) {
73
+ const line = lines[i]!;
74
+ if (line.indent < listIndent) break;
75
+ if (line.indent > listIndent) {
76
+ i++;
77
+ continue;
78
+ }
79
+ if (!line.content.startsWith("- ") && line.content !== "-") break;
80
+
81
+ const itemContent = line.content === "-" ? "" : line.content.slice(2);
82
+
83
+ if (!itemContent) {
84
+ const childIndent = listIndent + 2;
85
+ const next = parseBlock(lines, i + 1, childIndent);
86
+ items.push(next.value);
87
+ i = next.nextIdx;
88
+ } else if (itemContent.includes(":") && !isFlowScalar(itemContent)) {
89
+ const inlineIndent = listIndent + 2;
90
+ const synthetic = [
91
+ { indent: inlineIndent, content: itemContent, lineNum: line.lineNum },
92
+ ...lines.slice(i + 1),
93
+ ];
94
+ const mapResult = parseMap(synthetic, 0, inlineIndent);
95
+ items.push(mapResult.value);
96
+ i = i + 1 + (mapResult.nextIdx - 1);
97
+ } else {
98
+ items.push(parseScalar(itemContent));
99
+ i++;
100
+ }
101
+ }
102
+
103
+ return { value: items, nextIdx: i };
104
+ }
105
+
106
+ function parseMap(
107
+ lines: { indent: number; content: string; lineNum: number }[],
108
+ startIdx: number,
109
+ mapIndent: number,
110
+ ): ParseState {
111
+ const map: { [key: string]: YamlValue } = {};
112
+ let i = startIdx;
113
+
114
+ while (i < lines.length) {
115
+ const line = lines[i]!;
116
+ if (line.indent < mapIndent) break;
117
+ if (line.indent > mapIndent) {
118
+ i++;
119
+ continue;
120
+ }
121
+
122
+ const colonIdx = findUnquotedColon(line.content);
123
+ if (colonIdx < 0) break;
124
+
125
+ const key = line.content.slice(0, colonIdx).trim().replace(/^["']|["']$/g, "");
126
+ const rest = line.content.slice(colonIdx + 1).trim();
127
+
128
+ if (!rest) {
129
+ const next = parseBlock(lines, i + 1, mapIndent + 1);
130
+ map[key] = next.value;
131
+ i = next.nextIdx;
132
+ } else if (rest.startsWith("[") && rest.endsWith("]")) {
133
+ map[key] = parseInlineList(rest);
134
+ i++;
135
+ } else if (rest.startsWith("{") && rest.endsWith("}")) {
136
+ map[key] = parseInlineMap(rest);
137
+ i++;
138
+ } else {
139
+ map[key] = parseScalar(rest);
140
+ i++;
141
+ }
142
+ }
143
+
144
+ return { value: map, nextIdx: i };
145
+ }
146
+
147
+ function findUnquotedColon(text: string): number {
148
+ let inString: string | null = null;
149
+ let bracket = 0;
150
+ for (let i = 0; i < text.length; i++) {
151
+ const ch = text[i];
152
+ if (inString) {
153
+ if (ch === inString && text[i - 1] !== "\\") inString = null;
154
+ } else {
155
+ if (ch === '"' || ch === "'") inString = ch;
156
+ else if (ch === "[" || ch === "{") bracket++;
157
+ else if (ch === "]" || ch === "}") bracket--;
158
+ else if (ch === ":" && bracket === 0) return i;
159
+ }
160
+ }
161
+ return -1;
162
+ }
163
+
164
+ function parseScalar(text: string): YamlValue {
165
+ const trimmed = text.trim();
166
+ if (trimmed === "" || trimmed === "null" || trimmed === "~") return null;
167
+ if (trimmed === "true") return true;
168
+ if (trimmed === "false") return false;
169
+
170
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
171
+ return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'");
172
+ }
173
+
174
+ const num = Number(trimmed);
175
+ if (!Number.isNaN(num) && /^-?\d+(\.\d+)?$/.test(trimmed)) return num;
176
+
177
+ return trimmed;
178
+ }
179
+
180
+ function isFlowScalar(text: string): boolean {
181
+ return text.startsWith("[") || text.startsWith("{") || text.startsWith('"') || text.startsWith("'");
182
+ }
183
+
184
+ function parseInlineList(text: string): YamlValue[] {
185
+ const inner = text.slice(1, -1).trim();
186
+ if (!inner) return [];
187
+ return splitTopLevel(inner, ",").map(item => parseScalar(item.trim()));
188
+ }
189
+
190
+ function parseInlineMap(text: string): { [key: string]: YamlValue } {
191
+ const inner = text.slice(1, -1).trim();
192
+ if (!inner) return {};
193
+ const result: { [key: string]: YamlValue } = {};
194
+ for (const pair of splitTopLevel(inner, ",")) {
195
+ const colonIdx = findUnquotedColon(pair);
196
+ if (colonIdx < 0) continue;
197
+ const key = pair.slice(0, colonIdx).trim().replace(/^["']|["']$/g, "");
198
+ const val = pair.slice(colonIdx + 1).trim();
199
+ result[key] = parseScalar(val);
200
+ }
201
+ return result;
202
+ }
203
+
204
+ function splitTopLevel(text: string, sep: string): string[] {
205
+ const parts: string[] = [];
206
+ let depth = 0;
207
+ let inString: string | null = null;
208
+ let current = "";
209
+
210
+ for (let i = 0; i < text.length; i++) {
211
+ const ch = text[i]!;
212
+ if (inString) {
213
+ if (ch === inString && text[i - 1] !== "\\") inString = null;
214
+ current += ch;
215
+ } else {
216
+ if (ch === '"' || ch === "'") {
217
+ inString = ch;
218
+ current += ch;
219
+ } else if (ch === "[" || ch === "{") {
220
+ depth++;
221
+ current += ch;
222
+ } else if (ch === "]" || ch === "}") {
223
+ depth--;
224
+ current += ch;
225
+ } else if (ch === sep && depth === 0) {
226
+ parts.push(current);
227
+ current = "";
228
+ } else {
229
+ current += ch;
230
+ }
231
+ }
232
+ }
233
+ if (current) parts.push(current);
234
+ return parts;
235
+ }
package/src/grid.ts ADDED
@@ -0,0 +1,59 @@
1
+ export interface Cell {
2
+ char: string;
3
+ fg: string | null;
4
+ }
5
+
6
+ export interface Grid {
7
+ width: number;
8
+ height: number;
9
+ cells: Cell[][];
10
+ set(x: number, y: number, char: string, fg?: string | null): void;
11
+ get(x: number, y: number): Cell | undefined;
12
+ addRow(): void;
13
+ }
14
+
15
+ export function createGrid(width: number): Grid {
16
+ const cells: Cell[][] = [];
17
+
18
+ return {
19
+ get width() {
20
+ return width;
21
+ },
22
+ get height() {
23
+ return cells.length;
24
+ },
25
+ cells,
26
+ set(x: number, y: number, char: string, fg?: string | null) {
27
+ while (y >= cells.length) {
28
+ const row: Cell[] = [];
29
+ for (let i = 0; i < width; i++) {
30
+ row.push({ char: " ", fg: null });
31
+ }
32
+ cells.push(row);
33
+ }
34
+ if (x < 0 || x >= width) return;
35
+ const cell = cells[y]?.[x];
36
+ if (!cell) return;
37
+ cell.char = char;
38
+ if (fg !== undefined) cell.fg = fg ?? null;
39
+ },
40
+ get(x: number, y: number) {
41
+ return cells[y]?.[x];
42
+ },
43
+ addRow() {
44
+ const row: Cell[] = [];
45
+ for (let i = 0; i < width; i++) {
46
+ row.push({ char: " ", fg: null });
47
+ }
48
+ cells.push(row);
49
+ },
50
+ };
51
+ }
52
+
53
+ export function writeText(grid: Grid, x: number, y: number, text: string, fg?: string | null): number {
54
+ for (let i = 0; i < text.length; i++) {
55
+ if (x + i >= grid.width) break;
56
+ grid.set(x + i, y, text[i]!, fg);
57
+ }
58
+ return x + text.length;
59
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { renderTree } from "./render";
2
+ import { encodeAnsi } from "./encoders/ansi";
3
+ import { encodeString } from "./encoders/string";
4
+ import { encodeHtml } from "./encoders/html";
5
+ import type { CLINode, Flag, Arg, TreeOptions, AnsiColor } from "./types";
6
+
7
+ export type { CLINode, Flag, Arg, TreeOptions, AnsiColor };
8
+ export { parseHelp, parseHelpRecursive } from "./parse";
9
+
10
+ export function treeToAnsi(root: CLINode, opts?: TreeOptions): string {
11
+ return encodeAnsi(renderTree(root, opts));
12
+ }
13
+
14
+ export function treeToString(root: CLINode, opts?: TreeOptions): string {
15
+ return encodeString(renderTree(root, opts));
16
+ }
17
+
18
+ export function treeToHtml(root: CLINode, opts?: TreeOptions): string {
19
+ return encodeHtml(renderTree(root, opts));
20
+ }
21
+
22
+ export function printTree(root: CLINode, opts?: TreeOptions): void {
23
+ console.log(treeToAnsi(root, opts));
24
+ }
@@ -0,0 +1,156 @@
1
+ import type { HistoryEntry } from "./types";
2
+
3
+ export async function readHistoryFile(path: string): Promise<string> {
4
+ return await Bun.file(path).text();
5
+ }
6
+
7
+ export function detectHistoryFormat(text: string): "zsh-extended" | "bash" | "fish" | "unknown" {
8
+ const firstNonEmpty = text.split("\n").find(l => l.trim().length > 0);
9
+ if (!firstNonEmpty) return "unknown";
10
+ if (/^: \d+:\d+;/.test(firstNonEmpty)) return "zsh-extended";
11
+ if (/^- cmd: /.test(firstNonEmpty)) return "fish";
12
+ return "bash";
13
+ }
14
+
15
+ export function parseHistory(text: string): HistoryEntry[] {
16
+ const format = detectHistoryFormat(text);
17
+ switch (format) {
18
+ case "zsh-extended":
19
+ return parseZshExtended(text);
20
+ case "fish":
21
+ return parseFishHistory(text);
22
+ case "bash":
23
+ default:
24
+ return parseBashHistory(text);
25
+ }
26
+ }
27
+
28
+ function parseZshExtended(text: string): HistoryEntry[] {
29
+ const entries: HistoryEntry[] = [];
30
+ const lines = text.split("\n");
31
+
32
+ let pending: { timestamp: number; cmd: string } | null = null;
33
+
34
+ for (const rawLine of lines) {
35
+ const match = rawLine.match(/^: (\d+):\d+;(.*)$/);
36
+ if (match) {
37
+ if (pending) {
38
+ entries.push(buildEntry(pending.timestamp, pending.cmd));
39
+ }
40
+ pending = { timestamp: Number.parseInt(match[1]!, 10), cmd: match[2]! };
41
+ } else if (pending) {
42
+ pending.cmd += "\n" + rawLine;
43
+ }
44
+ }
45
+
46
+ if (pending) {
47
+ entries.push(buildEntry(pending.timestamp, pending.cmd));
48
+ }
49
+
50
+ return entries.filter(e => e.argv.length > 0);
51
+ }
52
+
53
+ function parseBashHistory(text: string): HistoryEntry[] {
54
+ const entries: HistoryEntry[] = [];
55
+ const lines = text.split("\n");
56
+ let currentTimestamp = 0;
57
+
58
+ for (const line of lines) {
59
+ const trimmed = line.trim();
60
+ if (!trimmed) continue;
61
+
62
+ if (/^#\d+$/.test(trimmed)) {
63
+ currentTimestamp = Number.parseInt(trimmed.slice(1), 10);
64
+ continue;
65
+ }
66
+
67
+ entries.push(buildEntry(currentTimestamp, trimmed));
68
+ }
69
+
70
+ return entries.filter(e => e.argv.length > 0);
71
+ }
72
+
73
+ function parseFishHistory(text: string): HistoryEntry[] {
74
+ const entries: HistoryEntry[] = [];
75
+ const blocks = text.split(/\n(?=- cmd: )/);
76
+
77
+ for (const block of blocks) {
78
+ const cmdMatch = block.match(/- cmd: (.*)/);
79
+ const whenMatch = block.match(/when: (\d+)/);
80
+ if (!cmdMatch) continue;
81
+ const timestamp = whenMatch ? Number.parseInt(whenMatch[1]!, 10) : 0;
82
+ entries.push(buildEntry(timestamp, cmdMatch[1]!));
83
+ }
84
+
85
+ return entries.filter(e => e.argv.length > 0);
86
+ }
87
+
88
+ function buildEntry(timestamp: number, rawCmd: string): HistoryEntry {
89
+ const cleaned = stripContinuations(rawCmd).trim();
90
+ const argv = tokenize(cleaned);
91
+ return { timestamp, raw: cleaned, argv };
92
+ }
93
+
94
+ function stripContinuations(cmd: string): string {
95
+ return cmd.replace(/\\\n/g, " ").replace(/\n\s*/g, " ");
96
+ }
97
+
98
+ export function tokenize(cmd: string): string[] {
99
+ const tokens: string[] = [];
100
+ let current = "";
101
+ let inSingle = false;
102
+ let inDouble = false;
103
+ let escape = false;
104
+
105
+ for (let i = 0; i < cmd.length; i++) {
106
+ const ch = cmd[i]!;
107
+ if (escape) {
108
+ current += ch;
109
+ escape = false;
110
+ continue;
111
+ }
112
+ if (ch === "\\" && !inSingle) {
113
+ escape = true;
114
+ continue;
115
+ }
116
+ if (ch === "'" && !inDouble) {
117
+ inSingle = !inSingle;
118
+ continue;
119
+ }
120
+ if (ch === '"' && !inSingle) {
121
+ inDouble = !inDouble;
122
+ continue;
123
+ }
124
+ if (/\s/.test(ch) && !inSingle && !inDouble) {
125
+ if (current) {
126
+ tokens.push(current);
127
+ current = "";
128
+ }
129
+ continue;
130
+ }
131
+ if ((ch === "|" || ch === ";" || ch === "&") && !inSingle && !inDouble) {
132
+ if (current) {
133
+ tokens.push(current);
134
+ current = "";
135
+ }
136
+ return tokens;
137
+ }
138
+ current += ch;
139
+ }
140
+
141
+ if (current) tokens.push(current);
142
+ return tokens;
143
+ }
144
+
145
+ export function defaultHistoryPath(): string | null {
146
+ const home = process.env.HOME ?? "";
147
+ if (!home) return null;
148
+
149
+ const candidates = [
150
+ `${home}/.zsh_history`,
151
+ `${home}/.bash_history`,
152
+ `${home}/.config/fish/fish_history`,
153
+ ];
154
+
155
+ return candidates[0] ?? null;
156
+ }
@@ -0,0 +1,76 @@
1
+ import { parseHistory, readHistoryFile, defaultHistoryPath, tokenize, detectHistoryFormat } from "./history";
2
+ import { segmentSessions, filterByCli } from "./sessions";
3
+ import { buildTransitions, normalizeTransitions, extractSubcommand, extractSubcommandPath } from "./transitions";
4
+ import { extractPaths, clusterIntoWorkflows } from "./workflows";
5
+ import { computeStats } from "./stats";
6
+ import { suggestSkills, type SkillSuggestion } from "./suggest";
7
+ import type { HistoryEntry, Session, Transition, MinedWorkflow, CliUsageStats, MineOptions } from "./types";
8
+
9
+ export type { HistoryEntry, Session, Transition, MinedWorkflow, CliUsageStats, MineOptions, SkillSuggestion };
10
+ export {
11
+ parseHistory,
12
+ readHistoryFile,
13
+ defaultHistoryPath,
14
+ tokenize,
15
+ detectHistoryFormat,
16
+ segmentSessions,
17
+ filterByCli,
18
+ buildTransitions,
19
+ normalizeTransitions,
20
+ extractSubcommand,
21
+ extractSubcommandPath,
22
+ extractPaths,
23
+ clusterIntoWorkflows,
24
+ computeStats,
25
+ suggestSkills,
26
+ };
27
+
28
+ export interface MineResult {
29
+ cli: string;
30
+ stats: CliUsageStats;
31
+ transitions: Transition[];
32
+ workflows: MinedWorkflow[];
33
+ suggestions: SkillSuggestion[];
34
+ sessionsAnalyzed: number;
35
+ }
36
+
37
+ export async function mineCli(cli: string, options: MineOptions = {}): Promise<MineResult> {
38
+ const path = options.historyPath ?? defaultHistoryPath();
39
+ if (!path) {
40
+ throw new Error("Could not determine shell history path. Pass historyPath explicitly.");
41
+ }
42
+
43
+ const text = await readHistoryFile(path);
44
+ const entries = parseHistory(text);
45
+ const allSessions = segmentSessions(entries, options.sessionGapMinutes ?? 10);
46
+ const cliSessions = filterByCli(allSessions, cli);
47
+
48
+ const stats = computeStats(entries, allSessions, cli);
49
+
50
+ const rawTransitions = buildTransitions(cliSessions);
51
+ const transitions = normalizeTransitions(
52
+ rawTransitions,
53
+ options.minSupport ?? 3,
54
+ options.minConfidence ?? 0.2,
55
+ );
56
+
57
+ const paths = extractPaths(
58
+ cliSessions,
59
+ options.minPathLength ?? 2,
60
+ options.maxPathLength ?? 6,
61
+ );
62
+ const workflows = clusterIntoWorkflows(paths, cli, {
63
+ minSupport: options.minSupport ?? 3,
64
+ });
65
+
66
+ const suggestions = suggestSkills(workflows, stats);
67
+
68
+ return {
69
+ cli,
70
+ stats,
71
+ transitions,
72
+ workflows,
73
+ suggestions,
74
+ sessionsAnalyzed: cliSessions.length,
75
+ };
76
+ }
@@ -0,0 +1,39 @@
1
+ import type { HistoryEntry, Session } from "./types";
2
+
3
+ export function segmentSessions(entries: HistoryEntry[], gapMinutes = 10): Session[] {
4
+ if (entries.length === 0) return [];
5
+
6
+ const sorted = [...entries].sort((a, b) => a.timestamp - b.timestamp);
7
+ const gapSeconds = gapMinutes * 60;
8
+ const sessions: Session[] = [];
9
+
10
+ let current: HistoryEntry[] = [sorted[0]!];
11
+ let sessionStart = sorted[0]!.timestamp;
12
+ let lastTimestamp = sorted[0]!.timestamp;
13
+
14
+ for (let i = 1; i < sorted.length; i++) {
15
+ const entry = sorted[i]!;
16
+ const gap = entry.timestamp - lastTimestamp;
17
+
18
+ if (gap > gapSeconds) {
19
+ sessions.push({ start: sessionStart, end: lastTimestamp, entries: current });
20
+ current = [entry];
21
+ sessionStart = entry.timestamp;
22
+ } else {
23
+ current.push(entry);
24
+ }
25
+ lastTimestamp = entry.timestamp;
26
+ }
27
+
28
+ sessions.push({ start: sessionStart, end: lastTimestamp, entries: current });
29
+ return sessions;
30
+ }
31
+
32
+ export function filterByCli(sessions: Session[], cli: string): Session[] {
33
+ return sessions
34
+ .map(session => ({
35
+ ...session,
36
+ entries: session.entries.filter(e => e.argv[0] === cli),
37
+ }))
38
+ .filter(s => s.entries.length > 0);
39
+ }
@@ -0,0 +1,43 @@
1
+ import type { HistoryEntry, CliUsageStats, Session } from "./types";
2
+ import { extractSubcommand } from "./transitions";
3
+
4
+ export function computeStats(entries: HistoryEntry[], sessions: Session[], cli: string): CliUsageStats {
5
+ const cliEntries = entries.filter(e => e.argv[0] === cli);
6
+ const cliSessions = sessions.filter(s => s.entries.some(e => e.argv[0] === cli));
7
+
8
+ const subcommandCounts = new Map<string, number>();
9
+ const flagCounts = new Map<string, number>();
10
+
11
+ for (const entry of cliEntries) {
12
+ const sub = extractSubcommand(entry);
13
+ subcommandCounts.set(sub, (subcommandCounts.get(sub) ?? 0) + 1);
14
+
15
+ for (const arg of entry.argv.slice(1)) {
16
+ if (arg.startsWith("--")) {
17
+ const flag = arg.split("=")[0]!;
18
+ flagCounts.set(flag, (flagCounts.get(flag) ?? 0) + 1);
19
+ } else if (arg.startsWith("-") && arg.length === 2) {
20
+ flagCounts.set(arg, (flagCounts.get(arg) ?? 0) + 1);
21
+ }
22
+ }
23
+ }
24
+
25
+ const topSubcommands = Array.from(subcommandCounts.entries())
26
+ .map(([subcommand, count]) => ({ subcommand, count }))
27
+ .sort((a, b) => b.count - a.count)
28
+ .slice(0, 10);
29
+
30
+ const topFlags = Array.from(flagCounts.entries())
31
+ .map(([flag, count]) => ({ flag, count }))
32
+ .sort((a, b) => b.count - a.count)
33
+ .slice(0, 10);
34
+
35
+ return {
36
+ cli,
37
+ totalInvocations: cliEntries.length,
38
+ uniqueSubcommands: subcommandCounts.size,
39
+ topSubcommands,
40
+ topFlags,
41
+ sessionCount: cliSessions.length,
42
+ };
43
+ }