shellrecap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/cli.cjs +1611 -0
- package/dist/cli.js +1590 -0
- package/dist/index.cjs +1308 -0
- package/dist/index.d.cts +314 -0
- package/dist/index.d.ts +314 -0
- package/dist/index.js +1244 -0
- package/package.json +71 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
type ShellFormat = "zsh-extended" | "bash" | "fish" | "plain";
|
|
2
|
+
/** One history entry: the command line as typed, plus an epoch timestamp if the format records one. */
|
|
3
|
+
interface HistoryEntry {
|
|
4
|
+
cmd: string;
|
|
5
|
+
/** Unix epoch seconds, when the shell recorded it (zsh extended / bash HISTTIMEFORMAT / fish). */
|
|
6
|
+
ts?: number;
|
|
7
|
+
}
|
|
8
|
+
/** One simple command inside an entry (entries can contain pipes / && / ;). */
|
|
9
|
+
interface Segment {
|
|
10
|
+
/** The base command, e.g. "git" (sudo/env wrappers stripped, paths basename'd). */
|
|
11
|
+
base: string;
|
|
12
|
+
/** "git status" style two-token form for known multi-part CLIs, else same as base. */
|
|
13
|
+
withSub: string;
|
|
14
|
+
/** Raw tokens of the segment (after wrapper stripping). */
|
|
15
|
+
tokens: string[];
|
|
16
|
+
sudo: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** A tokenized entry. */
|
|
19
|
+
interface ParsedEntry {
|
|
20
|
+
raw: string;
|
|
21
|
+
ts?: number;
|
|
22
|
+
segments: Segment[];
|
|
23
|
+
pipes: number;
|
|
24
|
+
}
|
|
25
|
+
interface CommandCount {
|
|
26
|
+
name: string;
|
|
27
|
+
count: number;
|
|
28
|
+
}
|
|
29
|
+
interface TimeStats {
|
|
30
|
+
/** True when the history format carried timestamps. */
|
|
31
|
+
hasTimestamps: boolean;
|
|
32
|
+
/** Counts by hour 0-23 (only meaningful when hasTimestamps). */
|
|
33
|
+
byHour: number[];
|
|
34
|
+
/** Counts by weekday 0=Sun..6=Sat. */
|
|
35
|
+
byWeekday: number[];
|
|
36
|
+
peakHour: number;
|
|
37
|
+
peakWeekday: number;
|
|
38
|
+
/** Percent of commands typed 22:00–04:59. */
|
|
39
|
+
nightOwlPct: number;
|
|
40
|
+
/** Percent typed on Sat/Sun. */
|
|
41
|
+
weekendPct: number;
|
|
42
|
+
/** First and last entry dates, YYYY-MM-DD. */
|
|
43
|
+
firstDate?: string;
|
|
44
|
+
lastDate?: string;
|
|
45
|
+
/** Busiest single day. */
|
|
46
|
+
busiestDay?: {
|
|
47
|
+
date: string;
|
|
48
|
+
commands: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
interface TypoHit {
|
|
52
|
+
typo: string;
|
|
53
|
+
/** What they meant. */
|
|
54
|
+
target: string;
|
|
55
|
+
count: number;
|
|
56
|
+
}
|
|
57
|
+
interface TypoStats {
|
|
58
|
+
hits: TypoHit[];
|
|
59
|
+
/** Total mistyped entries. */
|
|
60
|
+
total: number;
|
|
61
|
+
/** Wasted keystrokes (typo length × count). */
|
|
62
|
+
wastedKeystrokes: number;
|
|
63
|
+
}
|
|
64
|
+
interface AliasSuggestion {
|
|
65
|
+
/** The exact command line typed repeatedly. */
|
|
66
|
+
command: string;
|
|
67
|
+
/** Suggested alias name. */
|
|
68
|
+
alias: string;
|
|
69
|
+
count: number;
|
|
70
|
+
/** Keystrokes this alias would have saved: (len(command) - len(alias)) × count. */
|
|
71
|
+
saves: number;
|
|
72
|
+
}
|
|
73
|
+
interface AliasStats {
|
|
74
|
+
suggestions: AliasSuggestion[];
|
|
75
|
+
/** Total keystrokes all suggestions would have saved. */
|
|
76
|
+
totalSavable: number;
|
|
77
|
+
}
|
|
78
|
+
type SecretKind = "aws-access-key" | "github-token" | "slack-token" | "stripe-key" | "npm-token" | "openai-key" | "jwt" | "bearer-header" | "password-flag" | "password-env";
|
|
79
|
+
interface SecretHit {
|
|
80
|
+
kind: SecretKind;
|
|
81
|
+
/** SAFE masked preview, e.g. "ghp_Fk…". Never the full value. */
|
|
82
|
+
masked: string;
|
|
83
|
+
/** 1-based entry index in the parsed history (helps locate & delete the line). */
|
|
84
|
+
entryIndex: number;
|
|
85
|
+
}
|
|
86
|
+
interface Persona {
|
|
87
|
+
id: string;
|
|
88
|
+
title: string;
|
|
89
|
+
emoji: string;
|
|
90
|
+
blurb: string;
|
|
91
|
+
}
|
|
92
|
+
interface Totals {
|
|
93
|
+
entries: number;
|
|
94
|
+
/** Simple commands after splitting pipes/&&/; */
|
|
95
|
+
segments: number;
|
|
96
|
+
uniqueCommands: number;
|
|
97
|
+
uniqueBases: number;
|
|
98
|
+
pipePct: number;
|
|
99
|
+
sudoPct: number;
|
|
100
|
+
avgLength: number;
|
|
101
|
+
}
|
|
102
|
+
interface WrappedReport {
|
|
103
|
+
source: {
|
|
104
|
+
label: string;
|
|
105
|
+
format: ShellFormat;
|
|
106
|
+
};
|
|
107
|
+
totals: Totals;
|
|
108
|
+
time: TimeStats;
|
|
109
|
+
topCommands: CommandCount[];
|
|
110
|
+
topSubcommands: CommandCount[];
|
|
111
|
+
typos: TypoStats;
|
|
112
|
+
aliases: AliasStats;
|
|
113
|
+
secrets: SecretHit[];
|
|
114
|
+
persona: Persona;
|
|
115
|
+
}
|
|
116
|
+
type Theme = "tokyonight" | "dark" | "light" | "candy";
|
|
117
|
+
interface Config {
|
|
118
|
+
theme: Theme;
|
|
119
|
+
/** How many top commands / suggestions to show. */
|
|
120
|
+
top: number;
|
|
121
|
+
/** Minimum times a long command must repeat before suggesting an alias. */
|
|
122
|
+
aliasMinCount: number;
|
|
123
|
+
/** Minimum command length to consider for an alias. */
|
|
124
|
+
aliasMinLength: number;
|
|
125
|
+
/** Base commands to ignore entirely (e.g. your employer's internal CLI). */
|
|
126
|
+
ignoreCommands: string[];
|
|
127
|
+
/** Scan for secrets left in history (output is always masked). */
|
|
128
|
+
secretScan: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
declare function detectFormat(raw: string): ShellFormat;
|
|
132
|
+
declare function parseZshExtended(raw: string): HistoryEntry[];
|
|
133
|
+
declare function parseBash(raw: string): HistoryEntry[];
|
|
134
|
+
declare function parseFish(raw: string): HistoryEntry[];
|
|
135
|
+
declare function parsePlain(raw: string): HistoryEntry[];
|
|
136
|
+
/** Parse a history file's contents, auto-detecting the format unless given. */
|
|
137
|
+
declare function parseHistory(raw: string, format?: ShellFormat): {
|
|
138
|
+
entries: HistoryEntry[];
|
|
139
|
+
format: ShellFormat;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Split a command line on unquoted |, ||, &&, ; and newlines.
|
|
144
|
+
* A tiny state machine: enough shell awareness for stats, not a real parser.
|
|
145
|
+
*/
|
|
146
|
+
declare function splitSegments(cmd: string): {
|
|
147
|
+
parts: string[];
|
|
148
|
+
pipes: number;
|
|
149
|
+
};
|
|
150
|
+
/** Quote-aware tokens of a single simple command. */
|
|
151
|
+
declare function tokens(segment: string): string[];
|
|
152
|
+
/** Tokenize one simple command into a Segment (wrappers stripped, sudo tracked). */
|
|
153
|
+
declare function toSegment(part: string): Segment | null;
|
|
154
|
+
/** Tokenize a parsed history entry into segments. */
|
|
155
|
+
declare function tokenizeEntry(entry: HistoryEntry): ParsedEntry;
|
|
156
|
+
|
|
157
|
+
interface AnalyzeContext {
|
|
158
|
+
/** Display label for the source, e.g. "~/.zsh_history". */
|
|
159
|
+
sourceLabel: string;
|
|
160
|
+
format: ShellFormat;
|
|
161
|
+
/** Minutes to add to UTC for local time (KST = +540). The CLI injects the machine's. */
|
|
162
|
+
tzOffsetMin: number;
|
|
163
|
+
}
|
|
164
|
+
/** The single pure entrypoint: parsed history + config + context → recap. */
|
|
165
|
+
declare function analyze(rawEntries: HistoryEntry[], config: Config, ctx: AnalyzeContext): WrappedReport;
|
|
166
|
+
|
|
167
|
+
declare function computeTimeStats(entries: ParsedEntry[], tzOffsetMin: number): TimeStats;
|
|
168
|
+
|
|
169
|
+
declare const KNOWN_TYPOS: Record<string, string>;
|
|
170
|
+
declare const KNOWN_COMMANDS: Set<string>;
|
|
171
|
+
/**
|
|
172
|
+
* Optimal-string-alignment distance (Levenshtein + adjacent transposition as 1),
|
|
173
|
+
* capped at `max`. Transpositions matter: "gti"→"git" must be distance 1.
|
|
174
|
+
*/
|
|
175
|
+
declare function editDistance(a: string, b: string, max?: number): number;
|
|
176
|
+
/**
|
|
177
|
+
* Find likely typos. Two passes:
|
|
178
|
+
* 1. the curated dictionary (always wins);
|
|
179
|
+
* 2. frequency heuristic — a rare base (≤ max(3, 1% of target)) at edit distance 1
|
|
180
|
+
* from a much more popular base, where the rare one is NOT a known real command.
|
|
181
|
+
*/
|
|
182
|
+
declare function computeTypoStats(entries: ParsedEntry[]): TypoStats;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Derive an alias name from a command line: first letter of each word token,
|
|
186
|
+
* skipping flags ("docker compose up -d --build" → "dcu",
|
|
187
|
+
* "kubectl get pods -n production" → "kgp"). Falls back to extending with more
|
|
188
|
+
* letters on collision within this suggestion set.
|
|
189
|
+
*/
|
|
190
|
+
declare function suggestAliasName(command: string, taken: Set<string>): string;
|
|
191
|
+
/**
|
|
192
|
+
* Suggest aliases for exact command lines typed over and over.
|
|
193
|
+
* Deterministic: grouped by exact text, ranked by keystrokes saved.
|
|
194
|
+
*/
|
|
195
|
+
declare function computeAliasStats(entries: ParsedEntry[], opts: {
|
|
196
|
+
minCount: number;
|
|
197
|
+
minLength: number;
|
|
198
|
+
top: number;
|
|
199
|
+
}): AliasStats;
|
|
200
|
+
|
|
201
|
+
/** Mask a matched secret: keep a short identifying prefix, drop the rest. */
|
|
202
|
+
declare function mask(value: string): string;
|
|
203
|
+
/**
|
|
204
|
+
* Scan entries for secrets. `entryIndex` is 1-based over the parsed entries so
|
|
205
|
+
* the user can find and delete the line. Output is ALWAYS masked.
|
|
206
|
+
*/
|
|
207
|
+
declare function computeSecretHits(entries: ParsedEntry[]): SecretHit[];
|
|
208
|
+
declare const SECRET_KIND_LABELS: Record<SecretKind, string>;
|
|
209
|
+
|
|
210
|
+
declare const PERSONAS: {
|
|
211
|
+
readonly nightOwl: {
|
|
212
|
+
readonly id: "night-owl";
|
|
213
|
+
readonly title: "The Night Owl";
|
|
214
|
+
readonly emoji: "🦉";
|
|
215
|
+
readonly blurb: "Your terminal sees more moonlight than sunlight.";
|
|
216
|
+
};
|
|
217
|
+
readonly pipeWizard: {
|
|
218
|
+
readonly id: "pipe-wizard";
|
|
219
|
+
readonly title: "The Pipe Wizard";
|
|
220
|
+
readonly emoji: "🧙";
|
|
221
|
+
readonly blurb: "Why run one command when five can hold hands?";
|
|
222
|
+
};
|
|
223
|
+
readonly sudoSummoner: {
|
|
224
|
+
readonly id: "sudo-summoner";
|
|
225
|
+
readonly title: "The Sudo Summoner";
|
|
226
|
+
readonly emoji: "🔱";
|
|
227
|
+
readonly blurb: "Asking for forgiveness, never for permission.";
|
|
228
|
+
};
|
|
229
|
+
readonly gitGremlin: {
|
|
230
|
+
readonly id: "git-gremlin";
|
|
231
|
+
readonly title: "The Git Gremlin";
|
|
232
|
+
readonly emoji: "🐙";
|
|
233
|
+
readonly blurb: "Your shell is basically a git client with extra steps.";
|
|
234
|
+
};
|
|
235
|
+
readonly containerCaptain: {
|
|
236
|
+
readonly id: "container-captain";
|
|
237
|
+
readonly title: "The Container Captain";
|
|
238
|
+
readonly emoji: "🐳";
|
|
239
|
+
readonly blurb: "Everything's fine — it works in the container.";
|
|
240
|
+
};
|
|
241
|
+
readonly vimMonk: {
|
|
242
|
+
readonly id: "vim-monk";
|
|
243
|
+
readonly title: "The Vim Monk";
|
|
244
|
+
readonly emoji: "🧘";
|
|
245
|
+
readonly blurb: "You live inside the editor. The shell is just the hallway.";
|
|
246
|
+
};
|
|
247
|
+
readonly typoArtist: {
|
|
248
|
+
readonly id: "typo-artist";
|
|
249
|
+
readonly title: "The Typo Artist";
|
|
250
|
+
readonly emoji: "🎨";
|
|
251
|
+
readonly blurb: "gti, sl, dokcer — close enough, every time.";
|
|
252
|
+
};
|
|
253
|
+
readonly navigator: {
|
|
254
|
+
readonly id: "navigator";
|
|
255
|
+
readonly title: "The Navigator";
|
|
256
|
+
readonly emoji: "🧭";
|
|
257
|
+
readonly blurb: "cd, ls, cd, ls — forever exploring, never lost (mostly).";
|
|
258
|
+
};
|
|
259
|
+
readonly polyglot: {
|
|
260
|
+
readonly id: "polyglot";
|
|
261
|
+
readonly title: "The Polyglot";
|
|
262
|
+
readonly emoji: "🌐";
|
|
263
|
+
readonly blurb: "Is there a CLI you haven't tried?";
|
|
264
|
+
};
|
|
265
|
+
readonly steadyOperator: {
|
|
266
|
+
readonly id: "steady-operator";
|
|
267
|
+
readonly title: "The Steady Operator";
|
|
268
|
+
readonly emoji: "🎛️";
|
|
269
|
+
readonly blurb: "Calm, consistent, quietly unstoppable.";
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
interface PersonaInput {
|
|
273
|
+
totals: Totals;
|
|
274
|
+
time: TimeStats;
|
|
275
|
+
typos: TypoStats;
|
|
276
|
+
topCommands: CommandCount[];
|
|
277
|
+
}
|
|
278
|
+
/** First matching rule wins — fully deterministic. */
|
|
279
|
+
declare function derivePersona(input: PersonaInput, allBases: CommandCount[]): Persona;
|
|
280
|
+
|
|
281
|
+
/** Render the recap as a self-contained, shareable SVG. */
|
|
282
|
+
declare function renderCard(report: WrappedReport, theme?: Theme): string;
|
|
283
|
+
|
|
284
|
+
/** Stable, pretty-printed JSON of the full report (secrets stay masked by construction). */
|
|
285
|
+
declare function toJSON(report: WrappedReport): string;
|
|
286
|
+
|
|
287
|
+
/** A shareable Markdown recap. Secrets are masked by construction. */
|
|
288
|
+
declare function toMarkdown(r: WrappedReport): string;
|
|
289
|
+
|
|
290
|
+
declare const DEFAULT_CONFIG: Config;
|
|
291
|
+
declare const CONFIG_FILENAMES: string[];
|
|
292
|
+
/** Coerce arbitrary parsed JSON into a safe partial config (unknown keys ignored). */
|
|
293
|
+
declare function parseConfig(raw: unknown): Partial<Config>;
|
|
294
|
+
declare function mergeConfig(base: Config, override: Partial<Config>): Config;
|
|
295
|
+
declare function isValidTheme(t: string): t is Theme;
|
|
296
|
+
|
|
297
|
+
/** Local hour 0-23 for an epoch-seconds value shifted by tzOffsetMin. */
|
|
298
|
+
declare function epochToHour(ts: number, tzOffsetMin: number): number;
|
|
299
|
+
/** Weekday 0=Sunday..6=Saturday (1970-01-01 was a Thursday = 4). */
|
|
300
|
+
declare function epochToWeekday(ts: number, tzOffsetMin: number): number;
|
|
301
|
+
/** Civil date from a days-since-epoch value (Howard Hinnant's civil_from_days). */
|
|
302
|
+
declare function daysToYMD(z: number): {
|
|
303
|
+
y: number;
|
|
304
|
+
m: number;
|
|
305
|
+
d: number;
|
|
306
|
+
};
|
|
307
|
+
/** "YYYY-MM-DD" for an epoch-seconds value shifted by tzOffsetMin. */
|
|
308
|
+
declare function epochToDate(ts: number, tzOffsetMin: number): string;
|
|
309
|
+
declare const WEEKDAY_NAMES: string[];
|
|
310
|
+
declare const WEEKDAY_SHORT: string[];
|
|
311
|
+
/** "14" -> "2 PM", "0" -> "12 AM". */
|
|
312
|
+
declare function hourLabel(h: number): string;
|
|
313
|
+
|
|
314
|
+
export { type AliasStats, type AliasSuggestion, type AnalyzeContext, CONFIG_FILENAMES, type CommandCount, type Config, DEFAULT_CONFIG, type HistoryEntry, KNOWN_COMMANDS, KNOWN_TYPOS, PERSONAS, type ParsedEntry, type Persona, SECRET_KIND_LABELS, type SecretHit, type SecretKind, type Segment, type ShellFormat, type Theme, type TimeStats, type Totals, type TypoHit, type TypoStats, WEEKDAY_NAMES, WEEKDAY_SHORT, type WrappedReport, analyze, computeAliasStats, computeSecretHits, computeTimeStats, computeTypoStats, daysToYMD, derivePersona, detectFormat, editDistance, epochToDate, epochToHour, epochToWeekday, hourLabel, isValidTheme, mask, mergeConfig, parseBash, parseConfig, parseFish, parseHistory, parsePlain, parseZshExtended, renderCard, splitSegments, suggestAliasName, toJSON, toMarkdown, toSegment, tokenizeEntry, tokens };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
type ShellFormat = "zsh-extended" | "bash" | "fish" | "plain";
|
|
2
|
+
/** One history entry: the command line as typed, plus an epoch timestamp if the format records one. */
|
|
3
|
+
interface HistoryEntry {
|
|
4
|
+
cmd: string;
|
|
5
|
+
/** Unix epoch seconds, when the shell recorded it (zsh extended / bash HISTTIMEFORMAT / fish). */
|
|
6
|
+
ts?: number;
|
|
7
|
+
}
|
|
8
|
+
/** One simple command inside an entry (entries can contain pipes / && / ;). */
|
|
9
|
+
interface Segment {
|
|
10
|
+
/** The base command, e.g. "git" (sudo/env wrappers stripped, paths basename'd). */
|
|
11
|
+
base: string;
|
|
12
|
+
/** "git status" style two-token form for known multi-part CLIs, else same as base. */
|
|
13
|
+
withSub: string;
|
|
14
|
+
/** Raw tokens of the segment (after wrapper stripping). */
|
|
15
|
+
tokens: string[];
|
|
16
|
+
sudo: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** A tokenized entry. */
|
|
19
|
+
interface ParsedEntry {
|
|
20
|
+
raw: string;
|
|
21
|
+
ts?: number;
|
|
22
|
+
segments: Segment[];
|
|
23
|
+
pipes: number;
|
|
24
|
+
}
|
|
25
|
+
interface CommandCount {
|
|
26
|
+
name: string;
|
|
27
|
+
count: number;
|
|
28
|
+
}
|
|
29
|
+
interface TimeStats {
|
|
30
|
+
/** True when the history format carried timestamps. */
|
|
31
|
+
hasTimestamps: boolean;
|
|
32
|
+
/** Counts by hour 0-23 (only meaningful when hasTimestamps). */
|
|
33
|
+
byHour: number[];
|
|
34
|
+
/** Counts by weekday 0=Sun..6=Sat. */
|
|
35
|
+
byWeekday: number[];
|
|
36
|
+
peakHour: number;
|
|
37
|
+
peakWeekday: number;
|
|
38
|
+
/** Percent of commands typed 22:00–04:59. */
|
|
39
|
+
nightOwlPct: number;
|
|
40
|
+
/** Percent typed on Sat/Sun. */
|
|
41
|
+
weekendPct: number;
|
|
42
|
+
/** First and last entry dates, YYYY-MM-DD. */
|
|
43
|
+
firstDate?: string;
|
|
44
|
+
lastDate?: string;
|
|
45
|
+
/** Busiest single day. */
|
|
46
|
+
busiestDay?: {
|
|
47
|
+
date: string;
|
|
48
|
+
commands: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
interface TypoHit {
|
|
52
|
+
typo: string;
|
|
53
|
+
/** What they meant. */
|
|
54
|
+
target: string;
|
|
55
|
+
count: number;
|
|
56
|
+
}
|
|
57
|
+
interface TypoStats {
|
|
58
|
+
hits: TypoHit[];
|
|
59
|
+
/** Total mistyped entries. */
|
|
60
|
+
total: number;
|
|
61
|
+
/** Wasted keystrokes (typo length × count). */
|
|
62
|
+
wastedKeystrokes: number;
|
|
63
|
+
}
|
|
64
|
+
interface AliasSuggestion {
|
|
65
|
+
/** The exact command line typed repeatedly. */
|
|
66
|
+
command: string;
|
|
67
|
+
/** Suggested alias name. */
|
|
68
|
+
alias: string;
|
|
69
|
+
count: number;
|
|
70
|
+
/** Keystrokes this alias would have saved: (len(command) - len(alias)) × count. */
|
|
71
|
+
saves: number;
|
|
72
|
+
}
|
|
73
|
+
interface AliasStats {
|
|
74
|
+
suggestions: AliasSuggestion[];
|
|
75
|
+
/** Total keystrokes all suggestions would have saved. */
|
|
76
|
+
totalSavable: number;
|
|
77
|
+
}
|
|
78
|
+
type SecretKind = "aws-access-key" | "github-token" | "slack-token" | "stripe-key" | "npm-token" | "openai-key" | "jwt" | "bearer-header" | "password-flag" | "password-env";
|
|
79
|
+
interface SecretHit {
|
|
80
|
+
kind: SecretKind;
|
|
81
|
+
/** SAFE masked preview, e.g. "ghp_Fk…". Never the full value. */
|
|
82
|
+
masked: string;
|
|
83
|
+
/** 1-based entry index in the parsed history (helps locate & delete the line). */
|
|
84
|
+
entryIndex: number;
|
|
85
|
+
}
|
|
86
|
+
interface Persona {
|
|
87
|
+
id: string;
|
|
88
|
+
title: string;
|
|
89
|
+
emoji: string;
|
|
90
|
+
blurb: string;
|
|
91
|
+
}
|
|
92
|
+
interface Totals {
|
|
93
|
+
entries: number;
|
|
94
|
+
/** Simple commands after splitting pipes/&&/; */
|
|
95
|
+
segments: number;
|
|
96
|
+
uniqueCommands: number;
|
|
97
|
+
uniqueBases: number;
|
|
98
|
+
pipePct: number;
|
|
99
|
+
sudoPct: number;
|
|
100
|
+
avgLength: number;
|
|
101
|
+
}
|
|
102
|
+
interface WrappedReport {
|
|
103
|
+
source: {
|
|
104
|
+
label: string;
|
|
105
|
+
format: ShellFormat;
|
|
106
|
+
};
|
|
107
|
+
totals: Totals;
|
|
108
|
+
time: TimeStats;
|
|
109
|
+
topCommands: CommandCount[];
|
|
110
|
+
topSubcommands: CommandCount[];
|
|
111
|
+
typos: TypoStats;
|
|
112
|
+
aliases: AliasStats;
|
|
113
|
+
secrets: SecretHit[];
|
|
114
|
+
persona: Persona;
|
|
115
|
+
}
|
|
116
|
+
type Theme = "tokyonight" | "dark" | "light" | "candy";
|
|
117
|
+
interface Config {
|
|
118
|
+
theme: Theme;
|
|
119
|
+
/** How many top commands / suggestions to show. */
|
|
120
|
+
top: number;
|
|
121
|
+
/** Minimum times a long command must repeat before suggesting an alias. */
|
|
122
|
+
aliasMinCount: number;
|
|
123
|
+
/** Minimum command length to consider for an alias. */
|
|
124
|
+
aliasMinLength: number;
|
|
125
|
+
/** Base commands to ignore entirely (e.g. your employer's internal CLI). */
|
|
126
|
+
ignoreCommands: string[];
|
|
127
|
+
/** Scan for secrets left in history (output is always masked). */
|
|
128
|
+
secretScan: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
declare function detectFormat(raw: string): ShellFormat;
|
|
132
|
+
declare function parseZshExtended(raw: string): HistoryEntry[];
|
|
133
|
+
declare function parseBash(raw: string): HistoryEntry[];
|
|
134
|
+
declare function parseFish(raw: string): HistoryEntry[];
|
|
135
|
+
declare function parsePlain(raw: string): HistoryEntry[];
|
|
136
|
+
/** Parse a history file's contents, auto-detecting the format unless given. */
|
|
137
|
+
declare function parseHistory(raw: string, format?: ShellFormat): {
|
|
138
|
+
entries: HistoryEntry[];
|
|
139
|
+
format: ShellFormat;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Split a command line on unquoted |, ||, &&, ; and newlines.
|
|
144
|
+
* A tiny state machine: enough shell awareness for stats, not a real parser.
|
|
145
|
+
*/
|
|
146
|
+
declare function splitSegments(cmd: string): {
|
|
147
|
+
parts: string[];
|
|
148
|
+
pipes: number;
|
|
149
|
+
};
|
|
150
|
+
/** Quote-aware tokens of a single simple command. */
|
|
151
|
+
declare function tokens(segment: string): string[];
|
|
152
|
+
/** Tokenize one simple command into a Segment (wrappers stripped, sudo tracked). */
|
|
153
|
+
declare function toSegment(part: string): Segment | null;
|
|
154
|
+
/** Tokenize a parsed history entry into segments. */
|
|
155
|
+
declare function tokenizeEntry(entry: HistoryEntry): ParsedEntry;
|
|
156
|
+
|
|
157
|
+
interface AnalyzeContext {
|
|
158
|
+
/** Display label for the source, e.g. "~/.zsh_history". */
|
|
159
|
+
sourceLabel: string;
|
|
160
|
+
format: ShellFormat;
|
|
161
|
+
/** Minutes to add to UTC for local time (KST = +540). The CLI injects the machine's. */
|
|
162
|
+
tzOffsetMin: number;
|
|
163
|
+
}
|
|
164
|
+
/** The single pure entrypoint: parsed history + config + context → recap. */
|
|
165
|
+
declare function analyze(rawEntries: HistoryEntry[], config: Config, ctx: AnalyzeContext): WrappedReport;
|
|
166
|
+
|
|
167
|
+
declare function computeTimeStats(entries: ParsedEntry[], tzOffsetMin: number): TimeStats;
|
|
168
|
+
|
|
169
|
+
declare const KNOWN_TYPOS: Record<string, string>;
|
|
170
|
+
declare const KNOWN_COMMANDS: Set<string>;
|
|
171
|
+
/**
|
|
172
|
+
* Optimal-string-alignment distance (Levenshtein + adjacent transposition as 1),
|
|
173
|
+
* capped at `max`. Transpositions matter: "gti"→"git" must be distance 1.
|
|
174
|
+
*/
|
|
175
|
+
declare function editDistance(a: string, b: string, max?: number): number;
|
|
176
|
+
/**
|
|
177
|
+
* Find likely typos. Two passes:
|
|
178
|
+
* 1. the curated dictionary (always wins);
|
|
179
|
+
* 2. frequency heuristic — a rare base (≤ max(3, 1% of target)) at edit distance 1
|
|
180
|
+
* from a much more popular base, where the rare one is NOT a known real command.
|
|
181
|
+
*/
|
|
182
|
+
declare function computeTypoStats(entries: ParsedEntry[]): TypoStats;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Derive an alias name from a command line: first letter of each word token,
|
|
186
|
+
* skipping flags ("docker compose up -d --build" → "dcu",
|
|
187
|
+
* "kubectl get pods -n production" → "kgp"). Falls back to extending with more
|
|
188
|
+
* letters on collision within this suggestion set.
|
|
189
|
+
*/
|
|
190
|
+
declare function suggestAliasName(command: string, taken: Set<string>): string;
|
|
191
|
+
/**
|
|
192
|
+
* Suggest aliases for exact command lines typed over and over.
|
|
193
|
+
* Deterministic: grouped by exact text, ranked by keystrokes saved.
|
|
194
|
+
*/
|
|
195
|
+
declare function computeAliasStats(entries: ParsedEntry[], opts: {
|
|
196
|
+
minCount: number;
|
|
197
|
+
minLength: number;
|
|
198
|
+
top: number;
|
|
199
|
+
}): AliasStats;
|
|
200
|
+
|
|
201
|
+
/** Mask a matched secret: keep a short identifying prefix, drop the rest. */
|
|
202
|
+
declare function mask(value: string): string;
|
|
203
|
+
/**
|
|
204
|
+
* Scan entries for secrets. `entryIndex` is 1-based over the parsed entries so
|
|
205
|
+
* the user can find and delete the line. Output is ALWAYS masked.
|
|
206
|
+
*/
|
|
207
|
+
declare function computeSecretHits(entries: ParsedEntry[]): SecretHit[];
|
|
208
|
+
declare const SECRET_KIND_LABELS: Record<SecretKind, string>;
|
|
209
|
+
|
|
210
|
+
declare const PERSONAS: {
|
|
211
|
+
readonly nightOwl: {
|
|
212
|
+
readonly id: "night-owl";
|
|
213
|
+
readonly title: "The Night Owl";
|
|
214
|
+
readonly emoji: "🦉";
|
|
215
|
+
readonly blurb: "Your terminal sees more moonlight than sunlight.";
|
|
216
|
+
};
|
|
217
|
+
readonly pipeWizard: {
|
|
218
|
+
readonly id: "pipe-wizard";
|
|
219
|
+
readonly title: "The Pipe Wizard";
|
|
220
|
+
readonly emoji: "🧙";
|
|
221
|
+
readonly blurb: "Why run one command when five can hold hands?";
|
|
222
|
+
};
|
|
223
|
+
readonly sudoSummoner: {
|
|
224
|
+
readonly id: "sudo-summoner";
|
|
225
|
+
readonly title: "The Sudo Summoner";
|
|
226
|
+
readonly emoji: "🔱";
|
|
227
|
+
readonly blurb: "Asking for forgiveness, never for permission.";
|
|
228
|
+
};
|
|
229
|
+
readonly gitGremlin: {
|
|
230
|
+
readonly id: "git-gremlin";
|
|
231
|
+
readonly title: "The Git Gremlin";
|
|
232
|
+
readonly emoji: "🐙";
|
|
233
|
+
readonly blurb: "Your shell is basically a git client with extra steps.";
|
|
234
|
+
};
|
|
235
|
+
readonly containerCaptain: {
|
|
236
|
+
readonly id: "container-captain";
|
|
237
|
+
readonly title: "The Container Captain";
|
|
238
|
+
readonly emoji: "🐳";
|
|
239
|
+
readonly blurb: "Everything's fine — it works in the container.";
|
|
240
|
+
};
|
|
241
|
+
readonly vimMonk: {
|
|
242
|
+
readonly id: "vim-monk";
|
|
243
|
+
readonly title: "The Vim Monk";
|
|
244
|
+
readonly emoji: "🧘";
|
|
245
|
+
readonly blurb: "You live inside the editor. The shell is just the hallway.";
|
|
246
|
+
};
|
|
247
|
+
readonly typoArtist: {
|
|
248
|
+
readonly id: "typo-artist";
|
|
249
|
+
readonly title: "The Typo Artist";
|
|
250
|
+
readonly emoji: "🎨";
|
|
251
|
+
readonly blurb: "gti, sl, dokcer — close enough, every time.";
|
|
252
|
+
};
|
|
253
|
+
readonly navigator: {
|
|
254
|
+
readonly id: "navigator";
|
|
255
|
+
readonly title: "The Navigator";
|
|
256
|
+
readonly emoji: "🧭";
|
|
257
|
+
readonly blurb: "cd, ls, cd, ls — forever exploring, never lost (mostly).";
|
|
258
|
+
};
|
|
259
|
+
readonly polyglot: {
|
|
260
|
+
readonly id: "polyglot";
|
|
261
|
+
readonly title: "The Polyglot";
|
|
262
|
+
readonly emoji: "🌐";
|
|
263
|
+
readonly blurb: "Is there a CLI you haven't tried?";
|
|
264
|
+
};
|
|
265
|
+
readonly steadyOperator: {
|
|
266
|
+
readonly id: "steady-operator";
|
|
267
|
+
readonly title: "The Steady Operator";
|
|
268
|
+
readonly emoji: "🎛️";
|
|
269
|
+
readonly blurb: "Calm, consistent, quietly unstoppable.";
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
interface PersonaInput {
|
|
273
|
+
totals: Totals;
|
|
274
|
+
time: TimeStats;
|
|
275
|
+
typos: TypoStats;
|
|
276
|
+
topCommands: CommandCount[];
|
|
277
|
+
}
|
|
278
|
+
/** First matching rule wins — fully deterministic. */
|
|
279
|
+
declare function derivePersona(input: PersonaInput, allBases: CommandCount[]): Persona;
|
|
280
|
+
|
|
281
|
+
/** Render the recap as a self-contained, shareable SVG. */
|
|
282
|
+
declare function renderCard(report: WrappedReport, theme?: Theme): string;
|
|
283
|
+
|
|
284
|
+
/** Stable, pretty-printed JSON of the full report (secrets stay masked by construction). */
|
|
285
|
+
declare function toJSON(report: WrappedReport): string;
|
|
286
|
+
|
|
287
|
+
/** A shareable Markdown recap. Secrets are masked by construction. */
|
|
288
|
+
declare function toMarkdown(r: WrappedReport): string;
|
|
289
|
+
|
|
290
|
+
declare const DEFAULT_CONFIG: Config;
|
|
291
|
+
declare const CONFIG_FILENAMES: string[];
|
|
292
|
+
/** Coerce arbitrary parsed JSON into a safe partial config (unknown keys ignored). */
|
|
293
|
+
declare function parseConfig(raw: unknown): Partial<Config>;
|
|
294
|
+
declare function mergeConfig(base: Config, override: Partial<Config>): Config;
|
|
295
|
+
declare function isValidTheme(t: string): t is Theme;
|
|
296
|
+
|
|
297
|
+
/** Local hour 0-23 for an epoch-seconds value shifted by tzOffsetMin. */
|
|
298
|
+
declare function epochToHour(ts: number, tzOffsetMin: number): number;
|
|
299
|
+
/** Weekday 0=Sunday..6=Saturday (1970-01-01 was a Thursday = 4). */
|
|
300
|
+
declare function epochToWeekday(ts: number, tzOffsetMin: number): number;
|
|
301
|
+
/** Civil date from a days-since-epoch value (Howard Hinnant's civil_from_days). */
|
|
302
|
+
declare function daysToYMD(z: number): {
|
|
303
|
+
y: number;
|
|
304
|
+
m: number;
|
|
305
|
+
d: number;
|
|
306
|
+
};
|
|
307
|
+
/** "YYYY-MM-DD" for an epoch-seconds value shifted by tzOffsetMin. */
|
|
308
|
+
declare function epochToDate(ts: number, tzOffsetMin: number): string;
|
|
309
|
+
declare const WEEKDAY_NAMES: string[];
|
|
310
|
+
declare const WEEKDAY_SHORT: string[];
|
|
311
|
+
/** "14" -> "2 PM", "0" -> "12 AM". */
|
|
312
|
+
declare function hourLabel(h: number): string;
|
|
313
|
+
|
|
314
|
+
export { type AliasStats, type AliasSuggestion, type AnalyzeContext, CONFIG_FILENAMES, type CommandCount, type Config, DEFAULT_CONFIG, type HistoryEntry, KNOWN_COMMANDS, KNOWN_TYPOS, PERSONAS, type ParsedEntry, type Persona, SECRET_KIND_LABELS, type SecretHit, type SecretKind, type Segment, type ShellFormat, type Theme, type TimeStats, type Totals, type TypoHit, type TypoStats, WEEKDAY_NAMES, WEEKDAY_SHORT, type WrappedReport, analyze, computeAliasStats, computeSecretHits, computeTimeStats, computeTypoStats, daysToYMD, derivePersona, detectFormat, editDistance, epochToDate, epochToHour, epochToWeekday, hourLabel, isValidTheme, mask, mergeConfig, parseBash, parseConfig, parseFish, parseHistory, parsePlain, parseZshExtended, renderCard, splitSegments, suggestAliasName, toJSON, toMarkdown, toSegment, tokenizeEntry, tokens };
|