@djolex999/vir-cli 0.1.0 → 0.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djolex999/vir-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Distills Claude Code sessions into a compounding knowledge vault",
5
5
  "author": "Djordje Marković <djordje@growthq.rs>",
6
6
  "license": "MIT",
@@ -1,273 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import type { Config } from "../config.js";
5
- import type { DistilledRow, StateDb } from "../state/db.js";
6
- import { kebab } from "../pipeline/writer.js";
7
-
8
- export const VIR_START = "<!-- VIR:START -->";
9
- export const VIR_END = "<!-- VIR:END -->";
10
- const TOP_N_PER_CATEGORY = 5;
11
-
12
- export interface Entry {
13
- slug: string;
14
- topic: string;
15
- category: string;
16
- confidence: number;
17
- startedAt: string | null;
18
- }
19
-
20
- export interface DiffResult {
21
- added: Entry[];
22
- removed: { slug: string }[];
23
- upgraded: Array<{ slug: string; oldConf: number; newConf: number }>;
24
- unchanged: Entry[];
25
- }
26
-
27
- export interface PlanItem {
28
- target: string;
29
- exists: boolean;
30
- hasBlock: boolean;
31
- lastUpdated: string | null;
32
- newBlock: string;
33
- diff: DiffResult;
34
- scope: "global" | { project: string };
35
- }
36
-
37
- export function planUpdates(
38
- _cfg: Config,
39
- db: StateDb,
40
- options: { project?: string; globalOnly?: boolean } = {},
41
- ): PlanItem[] {
42
- const rows = db.listDistilled();
43
- const plans: PlanItem[] = [];
44
-
45
- if (!options.project) {
46
- plans.push(buildPlan(globalClaudePath(), rows, { scope: "global" }));
47
- }
48
- if (options.globalOnly) return plans;
49
-
50
- const byProject = new Map<string, DistilledRow[]>();
51
- for (const r of rows) {
52
- const slug = kebab(r.project);
53
- if (slug.length === 0) continue;
54
- if (options.project && slug !== options.project) continue;
55
- let arr = byProject.get(slug);
56
- if (!arr) {
57
- arr = [];
58
- byProject.set(slug, arr);
59
- }
60
- arr.push(r);
61
- }
62
-
63
- for (const [slug, projectRows] of byProject) {
64
- const target = projectClaudePath(slug);
65
- plans.push(buildPlan(target, projectRows, { scope: { project: slug } }));
66
- }
67
-
68
- return plans;
69
- }
70
-
71
- function buildPlan(
72
- target: string,
73
- rows: DistilledRow[],
74
- meta: { scope: "global" | { project: string } },
75
- ): PlanItem {
76
- const entries = selectTopEntries(rows);
77
- const newBlock = renderBlock(entries);
78
- const existsAtPath = existsSync(target);
79
-
80
- let existingBlock = "";
81
- let lastUpdated: string | null = null;
82
- if (existsAtPath) {
83
- try {
84
- const raw = readFileSync(target, "utf8");
85
- existingBlock = extractBlock(raw);
86
- lastUpdated = extractLastUpdated(existingBlock);
87
- } catch {
88
- // ignore
89
- }
90
- }
91
-
92
- const oldEntries = parseEntries(existingBlock);
93
- const diff = computeDiff(oldEntries, entries);
94
-
95
- return {
96
- target,
97
- exists: existsAtPath,
98
- hasBlock: existingBlock.length > 0,
99
- lastUpdated,
100
- newBlock,
101
- diff,
102
- scope: meta.scope,
103
- };
104
- }
105
-
106
- function selectTopEntries(rows: DistilledRow[]): Entry[] {
107
- const byCategory: Record<string, DistilledRow[]> = {
108
- pattern: [],
109
- gotcha: [],
110
- decision: [],
111
- tool: [],
112
- };
113
- for (const r of rows) {
114
- const bucket = byCategory[r.category];
115
- if (bucket) bucket.push(r);
116
- }
117
- const out: Entry[] = [];
118
- for (const cat of Object.keys(byCategory)) {
119
- const sorted = (byCategory[cat] ?? [])
120
- .slice()
121
- .sort((a, b) => b.confidence - a.confidence)
122
- .slice(0, TOP_N_PER_CATEGORY);
123
- for (const r of sorted) {
124
- out.push({
125
- slug: `${cat}/${kebab(r.topic)}`,
126
- topic: r.topic,
127
- category: cat,
128
- confidence: r.confidence,
129
- startedAt: r.startedAt,
130
- });
131
- }
132
- }
133
- return out;
134
- }
135
-
136
- function renderBlock(entries: Entry[]): string {
137
- const today = new Date().toISOString().slice(0, 10);
138
- const lines: string[] = [];
139
- lines.push(VIR_START);
140
- lines.push(`<!-- vir-last-updated: ${today} -->`);
141
- lines.push("");
142
- lines.push("## Distilled Knowledge (from Vir)");
143
- lines.push("");
144
- const byCat: Record<string, Entry[]> = {
145
- pattern: [],
146
- gotcha: [],
147
- decision: [],
148
- tool: [],
149
- };
150
- for (const e of entries) {
151
- const cat = byCat[e.category];
152
- if (cat) cat.push(e);
153
- }
154
- const order: Array<[string, string]> = [
155
- ["pattern", "Patterns"],
156
- ["gotcha", "Gotchas"],
157
- ["decision", "Decisions"],
158
- ["tool", "Tools"],
159
- ];
160
- for (const [key, label] of order) {
161
- const list = byCat[key] ?? [];
162
- if (list.length === 0) continue;
163
- lines.push(`### ${label}`);
164
- for (const e of list) {
165
- lines.push(
166
- `- ${e.slug} (conf ${e.confidence.toFixed(2)}) — ${e.topic}`,
167
- );
168
- }
169
- lines.push("");
170
- }
171
- lines.push(VIR_END);
172
- return lines.join("\n");
173
- }
174
-
175
- function extractBlock(raw: string): string {
176
- const start = raw.indexOf(VIR_START);
177
- const end = raw.indexOf(VIR_END);
178
- if (start === -1 || end === -1 || end < start) return "";
179
- return raw.slice(start, end + VIR_END.length);
180
- }
181
-
182
- function extractLastUpdated(block: string): string | null {
183
- const m = block.match(/vir-last-updated:\s*(\d{4}-\d{2}-\d{2})/);
184
- return m ? (m[1] ?? null) : null;
185
- }
186
-
187
- function parseEntries(block: string): Entry[] {
188
- if (block.length === 0) return [];
189
- const out: Entry[] = [];
190
- const lines = block.split("\n");
191
- for (const line of lines) {
192
- // - pattern/topic (conf 0.84) — display topic
193
- const m = line.match(
194
- /^- ([a-z]+\/[a-z0-9-]+) \(conf ([\d.]+)\)\s*—\s*(.+)$/i,
195
- );
196
- if (!m) continue;
197
- const slug = m[1] ?? "";
198
- const conf = Number(m[2] ?? 0);
199
- const topic = m[3] ?? "";
200
- const category = slug.split("/")[0] ?? "";
201
- out.push({
202
- slug,
203
- topic,
204
- category,
205
- confidence: Number.isFinite(conf) ? conf : 0,
206
- startedAt: null,
207
- });
208
- }
209
- return out;
210
- }
211
-
212
- function computeDiff(old: Entry[], next: Entry[]): DiffResult {
213
- const oldBySlug = new Map(old.map((e) => [e.slug, e]));
214
- const newBySlug = new Map(next.map((e) => [e.slug, e]));
215
- const added: Entry[] = [];
216
- const removed: { slug: string }[] = [];
217
- const upgraded: Array<{ slug: string; oldConf: number; newConf: number }> = [];
218
- const unchanged: Entry[] = [];
219
-
220
- for (const e of next) {
221
- const prev = oldBySlug.get(e.slug);
222
- if (!prev) {
223
- added.push(e);
224
- } else if (Math.abs(prev.confidence - e.confidence) > 0.05) {
225
- upgraded.push({
226
- slug: e.slug,
227
- oldConf: prev.confidence,
228
- newConf: e.confidence,
229
- });
230
- } else {
231
- unchanged.push(e);
232
- }
233
- }
234
- for (const e of old) {
235
- if (!newBySlug.has(e.slug)) removed.push({ slug: e.slug });
236
- }
237
- return { added, removed, upgraded, unchanged };
238
- }
239
-
240
- export function applyPlan(plan: PlanItem): boolean {
241
- if (!plan.exists) return false;
242
- let raw: string;
243
- try {
244
- raw = readFileSync(plan.target, "utf8");
245
- } catch {
246
- return false;
247
- }
248
-
249
- let updated: string;
250
- if (raw.includes(VIR_START) && raw.includes(VIR_END)) {
251
- const start = raw.indexOf(VIR_START);
252
- const end = raw.indexOf(VIR_END) + VIR_END.length;
253
- updated = raw.slice(0, start) + plan.newBlock + raw.slice(end);
254
- } else {
255
- const sep = raw.endsWith("\n") ? "\n" : "\n\n";
256
- updated = raw + sep + plan.newBlock + "\n";
257
- }
258
-
259
- try {
260
- writeFileSync(plan.target, updated);
261
- return true;
262
- } catch {
263
- return false;
264
- }
265
- }
266
-
267
- export function globalClaudePath(): string {
268
- return join(homedir(), ".claude", "CLAUDE.md");
269
- }
270
-
271
- export function projectClaudePath(projectSlug: string): string {
272
- return join(homedir(), "projects", projectSlug, "CLAUDE.md");
273
- }