lakonai 0.6.1

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.
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { backupFile, restoreAllBackups } = require('./backup');
6
+ const { installHook, uninstallHook } = require('./claude-hook');
7
+ const { installCommands, uninstallCommands } = require('./claude-commands');
8
+ const { claudeConfigDir } = require('./paths');
9
+
10
+ const MARK_BEGIN = '<!-- lakon:begin -->';
11
+ const MARK_END = '<!-- lakon:end -->';
12
+
13
+ function wrap(rule) {
14
+ return `${MARK_BEGIN}\n${rule.trim()}\n${MARK_END}\n`;
15
+ }
16
+
17
+ function ensureDir(p) {
18
+ fs.mkdirSync(p, { recursive: true });
19
+ }
20
+
21
+ function dirExists(p) {
22
+ try { return fs.statSync(p).isDirectory(); } catch { return false; }
23
+ }
24
+
25
+ function readSafe(p) {
26
+ try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
27
+ }
28
+
29
+ function hasBlock(filePath) {
30
+ const existing = readSafe(filePath);
31
+ /* c8 ignore next */
32
+ if (!existing) return false;
33
+ const re = new RegExp(`${MARK_BEGIN}[\\s\\S]*?${MARK_END}`);
34
+ return re.test(existing);
35
+ }
36
+
37
+ function upsertBlock(platformId, filePath, rule) {
38
+ ensureDir(path.dirname(filePath));
39
+ if (fs.existsSync(filePath) && !hasBlock(filePath)) {
40
+ backupFile(platformId, filePath);
41
+ }
42
+ const existing = readSafe(filePath);
43
+ const block = wrap(rule);
44
+ const re = new RegExp(`${MARK_BEGIN}[\\s\\S]*?${MARK_END}\\n?`);
45
+ const next = re.test(existing) ? existing.replace(re, block) : (existing ? `${existing.trim()}\n\n${block}` : block);
46
+ fs.writeFileSync(filePath, next, 'utf8');
47
+ return filePath;
48
+ }
49
+
50
+ function stripBlock(filePath) {
51
+ const existing = readSafe(filePath);
52
+ if (!existing) return null;
53
+ const re = new RegExp(`\\n*${MARK_BEGIN}[\\s\\S]*?${MARK_END}\\n?`);
54
+ if (!re.test(existing)) return null;
55
+ const next = existing.replace(re, '').trim();
56
+ if (next) fs.writeFileSync(filePath, next + '\n', 'utf8');
57
+ else fs.unlinkSync(filePath);
58
+ return filePath;
59
+ }
60
+
61
+ function revertPlatform(platformId) {
62
+ return restoreAllBackups(platformId);
63
+ }
64
+
65
+ const PLATFORMS = [
66
+ {
67
+ id: 'claude-code',
68
+ label: 'Claude Code',
69
+ scope: 'global',
70
+ detect: (home) => !!process.env.CLAUDE_CONFIG_DIR || dirExists(claudeConfigDir(home)),
71
+ install: ({ home, rule, id }) => {
72
+ const rulePath = upsertBlock(id, path.join(claudeConfigDir(home), 'CLAUDE.md'), rule);
73
+ const hookResult = installHook(home);
74
+ const cmds = installCommands(home);
75
+ /* c8 ignore next */
76
+ const suffixHook = hookResult.settingsMerged ? '+ PreToolUse hook' : `(hook: ${hookResult.note})`;
77
+ /* c8 ignore next */
78
+ const suffixCmds = cmds.length ? `+ ${cmds.join(' ')}` : '';
79
+ return [rulePath, suffixHook, suffixCmds].filter(Boolean).join(' ');
80
+ },
81
+ uninstall: ({ home }) => {
82
+ uninstallHook(home);
83
+ uninstallCommands(home);
84
+ return stripBlock(path.join(claudeConfigDir(home), 'CLAUDE.md'));
85
+ },
86
+ },
87
+ {
88
+ id: 'codex',
89
+ label: 'Codex CLI',
90
+ scope: 'global',
91
+ detect: (home) => dirExists(path.join(home, '.codex')),
92
+ install: ({ home, rule, id }) => upsertBlock(id, path.join(home, '.codex', 'AGENTS.md'), rule),
93
+ uninstall: ({ home }) => stripBlock(path.join(home, '.codex', 'AGENTS.md')),
94
+ },
95
+ {
96
+ id: 'cursor',
97
+ label: 'Cursor (per-repo rule)',
98
+ scope: 'project',
99
+ detect: () => fs.existsSync(path.join(process.cwd(), '.cursor')),
100
+ install: ({ rule, id }) => upsertBlock(id, path.join(process.cwd(), '.cursor', 'rules', 'lakon.mdc'), rule),
101
+ uninstall: () => stripBlock(path.join(process.cwd(), '.cursor', 'rules', 'lakon.mdc')),
102
+ },
103
+ {
104
+ id: 'windsurf',
105
+ label: 'Windsurf (per-repo rule)',
106
+ scope: 'project',
107
+ detect: () => fs.existsSync(path.join(process.cwd(), '.windsurf')),
108
+ install: ({ rule, id }) => upsertBlock(id, path.join(process.cwd(), '.windsurf', 'rules', 'lakon.md'), rule),
109
+ uninstall: () => stripBlock(path.join(process.cwd(), '.windsurf', 'rules', 'lakon.md')),
110
+ },
111
+ {
112
+ id: 'cline',
113
+ label: 'Cline (per-repo rule)',
114
+ scope: 'project',
115
+ detect: () => fs.existsSync(path.join(process.cwd(), '.clinerules')),
116
+ install: ({ rule, id }) => upsertBlock(id, path.join(process.cwd(), '.clinerules', 'lakon.md'), rule),
117
+ uninstall: () => stripBlock(path.join(process.cwd(), '.clinerules', 'lakon.md')),
118
+ },
119
+ {
120
+ id: 'gemini',
121
+ label: 'Gemini CLI',
122
+ scope: 'global',
123
+ detect: (home) => dirExists(path.join(home, '.gemini')),
124
+ install: ({ home, rule, id }) => upsertBlock(id, path.join(home, '.gemini', 'GEMINI.md'), rule),
125
+ uninstall: ({ home }) => stripBlock(path.join(home, '.gemini', 'GEMINI.md')),
126
+ },
127
+ ];
128
+
129
+ module.exports = { list: () => PLATFORMS, revertPlatform };
@@ -0,0 +1,103 @@
1
+ # lakon — terse response style
2
+
3
+ You are operating in **lakon mode** (named for Lakonía — the region of ancient Sparta whose people gave us the word "laconic"). Respond like a Spartan officer in the field: drop filler words, keep technical content 100% accurate.
4
+
5
+ ## Rules
6
+
7
+ 1. **Drop the preamble.** No "Sure!", "Happy to help", "Great question". Start with the answer.
8
+ 2. **No restating.** Don't repeat what the user said. Don't summarize your own answer.
9
+ 3. **Sentence fragments are fine.** "Bug in auth middleware. Token check uses `<` not `<=`. Fix:"
10
+ 4. **Bullet over prose.** Lists, code, and tables beat paragraphs.
11
+ 5. **One short line for what you did.** Not a paragraph. The diff speaks for itself.
12
+ 6. **Keep precision.** Identifiers, file paths, line numbers, error messages — verbatim. Never compress these.
13
+
14
+ ## When to drop the style (auto-clarity)
15
+
16
+ Switch to normal prose for:
17
+
18
+ - Security warnings or destructive-action confirmations
19
+ - Multi-step instructions where ambiguity could cause harm
20
+ - The user is confused or repeating the same question
21
+ - The user explicitly asks for an explanation
22
+
23
+ Resume terse mode after.
24
+
25
+ ## Examples
26
+
27
+ **Don't:**
28
+ > Sure! I'd be happy to help you with that. The reason your React component is re-rendering is likely because you're creating a new object reference on each render cycle. When you pass an inline object as a prop, React's shallow comparison sees it as a different object every time…
29
+
30
+ **Do:**
31
+ > New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`.
32
+
33
+ ---
34
+
35
+ **Don't:**
36
+ > I've successfully fixed the bug! I updated the authentication middleware to properly validate the token expiry time. The change was in `src/auth/middleware.ts` on line 42.
37
+
38
+ **Do:**
39
+ > Fixed. `src/auth/middleware.ts:42` — token check `<` → `<=`.
40
+
41
+ ---
42
+
43
+ ## Shell commands — use the `lakon` prefix
44
+
45
+ For any shell command whose output you read into context, prefix it with `lakon`. It runs the same command but compresses the output before it reaches you. Same exit code, same behavior — just less to read.
46
+
47
+ | Use this | Instead of |
48
+ |--------------------|-----------------|
49
+ | `lakon git log` | `git log` |
50
+ | `lakon git status` | `git status` |
51
+ | `lakon git diff` | `git diff` |
52
+ | `lakon ls -la` | `ls -la` |
53
+ | `lakon cat <file>` | `cat <file>` |
54
+ | `lakon grep -r …` | `grep -r …` |
55
+
56
+ The short alias `lak` works identically: `lak git log`.
57
+
58
+ Unsupported commands run unchanged through `lakon`, so when in doubt, prefix it.
59
+
60
+ **Skip the prefix only when:**
61
+ - The user explicitly asks for raw, unfiltered output.
62
+ - You're piping into another command (`git log | head` — pipe `lakon git log | head` instead).
63
+ - You need a specific format the filter would strip (e.g. machine-parseable `git log --format=...`).
64
+
65
+ ## File reads — grep first, then Read with offset/limit
66
+
67
+ Reading entire files is the single biggest token sink. Before using `Read` on any file:
68
+
69
+ 1. **Don't Read what you don't need.** If you're looking for one symbol or section, `lakon grep -n <pattern> <file>` first. The output gives you line numbers — then `Read` with `offset` and `limit` to fetch only that block.
70
+ 2. **Never Read these — grep them or skip:**
71
+ - `node_modules/**`, `vendor/**`, `dist/**`, `build/**`, `target/**`, `.next/**`, `.turbo/**`, `coverage/**`
72
+ - Lockfiles: `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `Cargo.lock`, `go.sum`, `*.lock`
73
+ - Build artifacts: `*.tsbuildinfo`, `*.min.js`, `*.min.css`, source maps, log files, `*.pyc`, `*.so`, `*.class`
74
+ 3. **For files > 300 lines:** start with `lakon grep -n` to locate, then `Read` a slice. Don't `Read` a 2000-line file to find one function.
75
+ 4. **Use `Glob` to find files**, not `Read` on the directory listing.
76
+
77
+ These reads cost real context. A `node_modules` peek is 50k tokens of nothing.
78
+
79
+ ## Think in code — for analysis, write a script, don't ingest the data
80
+
81
+ When the user asks you to **count, filter, parse, compare, or extract** data:
82
+
83
+ - **Don't** `Read` a 5k-line log and reason about it line by line.
84
+ - **Don't** `Read` a JSON blob and re-summarize it in prose.
85
+ - **Don't** `cat` a CSV and count rows in your head.
86
+
87
+ **Do** write a one-shot script via `Bash` and consume only its `console.log` output:
88
+
89
+ ```bash
90
+ node -e 'const fs = require("fs"); const lines = fs.readFileSync("app.log", "utf8").split("\n"); console.log(lines.filter(l => l.includes("ERROR")).length)'
91
+ ```
92
+
93
+ ```bash
94
+ node -e 'const data = JSON.parse(require("fs").readFileSync("users.json", "utf8")); console.log(data.filter(u => u.role === "admin").map(u => u.email).join("\n"))'
95
+ ```
96
+
97
+ ```bash
98
+ awk -F, '$3 > 100 { print $1 }' sales.csv | head -20
99
+ ```
100
+
101
+ The script reads the data; you read only the answer. One script replaces ten tool calls.
102
+
103
+ This applies whenever you'd otherwise pull bulky input into context just to derive something small from it.
@@ -0,0 +1,232 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const HOUR_MS = 60 * 60 * 1000;
8
+ const DAY_MS = 24 * HOUR_MS;
9
+ const WEEK_MS = 7 * DAY_MS;
10
+
11
+ function dataDir() {
12
+ /* c8 ignore next */
13
+ return process.env.LAKON_HOME || path.join(os.homedir(), '.lakon');
14
+ }
15
+
16
+ function logPath() {
17
+ return path.join(dataDir(), 'log.jsonl');
18
+ }
19
+
20
+ function record({ cmd, args, rawTokens, filteredTokens }) {
21
+ if (process.env.LAKON_NO_TRACK === '1') return;
22
+ try {
23
+ fs.mkdirSync(dataDir(), { recursive: true });
24
+ const entry = {
25
+ t: Date.now(),
26
+ cmd,
27
+ args: Array.isArray(args) ? args.slice(0, 4) : [],
28
+ raw: rawTokens,
29
+ out: filteredTokens,
30
+ saved: Math.max(0, rawTokens - filteredTokens),
31
+ };
32
+ fs.appendFileSync(logPath(), JSON.stringify(entry) + '\n');
33
+ /* c8 ignore next 3 */
34
+ } catch {
35
+ // never let tracking break a user command
36
+ }
37
+ }
38
+
39
+ function readEntries() {
40
+ try {
41
+ const raw = fs.readFileSync(logPath(), 'utf8');
42
+ return raw
43
+ .split('\n')
44
+ .filter(Boolean)
45
+ .map((line) => {
46
+ try { return JSON.parse(line); } catch { return null; }
47
+ })
48
+ .filter(Boolean);
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
54
+ function isSessionEntry(e) {
55
+ return e.cmd === 'session';
56
+ }
57
+
58
+ function aggregate(entries) {
59
+ const filtered = entries.filter((e) => !isSessionEntry(e));
60
+ const sum = (xs, k) => xs.reduce((a, e) => a + (e[k] || 0), 0);
61
+ return {
62
+ calls: filtered.length,
63
+ raw: sum(filtered, 'raw'),
64
+ out: sum(filtered, 'out'),
65
+ saved: sum(filtered, 'saved'),
66
+ };
67
+ }
68
+
69
+ function aggregateSessions(entries) {
70
+ const sessions = entries.filter(isSessionEntry);
71
+ const sum = (k) => sessions.reduce((a, e) => a + (e[k] || 0), 0);
72
+ return {
73
+ turns: sessions.length,
74
+ in_tokens: sum('in_tokens'),
75
+ out_tokens: sum('out_tokens'),
76
+ cache_read: sum('cache_read'),
77
+ };
78
+ }
79
+
80
+ function inWindow(entries, ms) {
81
+ if (ms === Infinity) return entries;
82
+ const cutoff = Date.now() - ms;
83
+ return entries.filter((e) => e.t >= cutoff);
84
+ }
85
+
86
+ function byWindow(entries, ms) {
87
+ return aggregate(inWindow(entries, ms));
88
+ }
89
+
90
+ function byWindowSessions(entries, ms) {
91
+ return aggregateSessions(inWindow(entries, ms));
92
+ }
93
+
94
+ function byCommand(entries) {
95
+ const map = new Map();
96
+ for (const e of entries) {
97
+ if (isSessionEntry(e)) continue;
98
+ const k = e.cmd || 'unknown';
99
+ if (!map.has(k)) map.set(k, { cmd: k, calls: 0, raw: 0, out: 0, saved: 0 });
100
+ const acc = map.get(k);
101
+ acc.calls += 1;
102
+ acc.raw += e.raw || 0;
103
+ acc.out += e.out || 0;
104
+ acc.saved += e.saved || 0;
105
+ }
106
+ return [...map.values()].sort((a, b) => b.saved - a.saved);
107
+ }
108
+
109
+ function pct(saved, raw) {
110
+ if (!raw) return 0;
111
+ return Math.round((saved / raw) * 100);
112
+ }
113
+
114
+ function fmt(n) {
115
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
116
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'k';
117
+ return String(n);
118
+ }
119
+
120
+ function tok(n) {
121
+ return fmt(n) + ' tok';
122
+ }
123
+
124
+ const WINDOW_LABELS = [
125
+ ['1h ', HOUR_MS],
126
+ ['24h', DAY_MS],
127
+ ['7d ', WEEK_MS],
128
+ ['30d', 30 * DAY_MS],
129
+ ['all', Infinity],
130
+ ];
131
+
132
+ function useColor() {
133
+ if (process.env.NO_COLOR) return false;
134
+ if (process.env.LAKON_COLOR === '0') return false;
135
+ if (process.env.LAKON_COLOR === '1') return true;
136
+ return !!process.stdout.isTTY;
137
+ }
138
+
139
+ function paint(s, codes) {
140
+ if (!useColor()) return s;
141
+ return `\x1b[${codes}m${s}\x1b[0m`;
142
+ }
143
+ const dim = (s) => paint(s, '2');
144
+ const bold = (s) => paint(s, '1');
145
+ const green = (s) => paint(s, '32');
146
+ const cyan = (s) => paint(s, '36');
147
+
148
+ function pad(s, n) {
149
+ s = String(s);
150
+ /* c8 ignore next */
151
+ if (s.length >= n) return s;
152
+ return s + ' '.repeat(n - s.length);
153
+ }
154
+ function rpad(s, n) {
155
+ s = String(s);
156
+ if (s.length >= n) return s;
157
+ return ' '.repeat(n - s.length) + s;
158
+ }
159
+
160
+ function report() {
161
+ const entries = readEntries();
162
+ if (!entries.length) {
163
+ return 'lakon: no usage recorded yet. Run a few commands through `lakon` first.\n';
164
+ }
165
+
166
+ const lines = [];
167
+ const W = byWindow(entries, WEEK_MS);
168
+ const headlinePct = pct(W.saved, W.raw);
169
+ lines.push(
170
+ `${bold('lakon')} ${dim('— savings this week:')} ` +
171
+ `${green(tok(W.saved))} ${dim('saved across')} ${W.calls} ${dim('shell calls')} ${green(`(${headlinePct}%)`)}`
172
+ );
173
+ lines.push('');
174
+
175
+ lines.push(cyan('shell + read/grep guards') + dim(' (tokens filtered before context)'));
176
+ lines.push(dim(' win calls before after saved %'));
177
+ for (const [label, ms] of WINDOW_LABELS) {
178
+ const agg = byWindow(entries, ms);
179
+ if (agg.calls === 0) continue;
180
+ const row =
181
+ ` ${pad(label, 6)}` +
182
+ `${rpad(agg.calls, 5)} ` +
183
+ `${rpad(tok(agg.raw), 12)} ` +
184
+ `${rpad(tok(agg.out), 12)} ` +
185
+ `${rpad(green(tok(agg.saved)), 12 + (useColor() ? 9 : 0))} ` +
186
+ `${rpad(green(pct(agg.saved, agg.raw) + '%'), 4 + (useColor() ? 9 : 0))}`;
187
+ lines.push(row);
188
+ }
189
+
190
+ const sessionsAll = aggregateSessions(entries);
191
+ if (sessionsAll.turns > 0) {
192
+ lines.push('');
193
+ lines.push(cyan('llm output') + dim(' (model tokens — terse style trims this side)'));
194
+ lines.push(dim(' win turns input output cache-read'));
195
+ for (const [label, ms] of WINDOW_LABELS) {
196
+ const agg = byWindowSessions(entries, ms);
197
+ if (agg.turns === 0) continue;
198
+ const row =
199
+ ` ${pad(label, 6)}` +
200
+ `${rpad(agg.turns, 5)} ` +
201
+ `${rpad(tok(agg.in_tokens), 12)} ` +
202
+ `${rpad(tok(agg.out_tokens), 12)} ` +
203
+ `${rpad(tok(agg.cache_read), 12)}`;
204
+ lines.push(row);
205
+ }
206
+ }
207
+
208
+ const top = byCommand(entries).slice(0, 5);
209
+ if (top.length) {
210
+ lines.push('');
211
+ lines.push(cyan('top commands') + dim(' (all time)'));
212
+ for (const c of top) {
213
+ const row =
214
+ ` ${pad(c.cmd, 8)}` +
215
+ `${rpad(c.calls + 'x', 6)} ` +
216
+ `${dim('saved')} ${rpad(green(tok(c.saved)), 10 + (useColor() ? 9 : 0))} ` +
217
+ `${green(pct(c.saved, c.raw) + '%')}`;
218
+ lines.push(row);
219
+ }
220
+ }
221
+
222
+ lines.push('');
223
+ lines.push(dim(`log: ${logPath()}`));
224
+ return lines.join('\n') + '\n';
225
+ }
226
+
227
+ function reset() {
228
+ try { fs.unlinkSync(logPath()); return true; }
229
+ catch { return false; }
230
+ }
231
+
232
+ module.exports = { record, report, reset, readEntries, logPath };