@djolex999/vir-cli 0.4.1 → 0.6.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/README.md CHANGED
@@ -18,7 +18,7 @@ developer-tools, mcp, local-first, cross-platform, llm-wiki
18
18
  <a href="https://www.npmjs.com/package/@djolex999/vir-cli"><img src="https://img.shields.io/npm/v/@djolex999/vir-cli?color=7c6af7&label=npm" alt="npm version"></a>
19
19
  <a href="https://www.npmjs.com/package/@djolex999/vir-cli"><img src="https://img.shields.io/npm/dw/@djolex999/vir-cli?color=4fd1a0" alt="npm downloads"></a>
20
20
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-22d3ee" alt="license"></a>
21
- <a href="#project-status"><img src="https://img.shields.io/badge/tests-50%20passing-22c55e" alt="tests"></a>
21
+ <a href="#project-status"><img src="https://img.shields.io/badge/tests-79%20passing-22c55e" alt="tests"></a>
22
22
  <a href="#project-status"><img src="https://img.shields.io/badge/platforms-macOS%20%7C%20Linux-lightgrey" alt="platforms"></a>
23
23
  <a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-server-c084fc" alt="mcp"></a>
24
24
  <a href="#"><img src="https://img.shields.io/badge/local--first-yes-f59e0b" alt="local-first"></a>
@@ -33,8 +33,12 @@ instead of resetting at the end of every session. He ended his post with: _"I
33
33
  think there is room here for an incredible new product instead of a hacky
34
34
  collection of scripts."_
35
35
 
36
- Vir is one implementation of that pattern, focused on Claude Code sessions with
37
- Obsidian as the frontend.
36
+ Vir is one implementation of that pattern, with Obsidian as the frontend.
37
+
38
+ > Vir reads two input sources today: Claude Code session transcripts (`.jsonl`)
39
+ > and web articles (markdown clipped via Obsidian Web Clipper). Both get
40
+ > distilled into the same vault. Future versions will add PDFs, code repos, and
41
+ > images — matching the full LLM Wiki pattern.
38
42
 
39
43
  [Karpathy's post →](https://x.com/karpathy/status/2039805659525644595)
40
44
 
@@ -72,6 +76,10 @@ results, not better."_ Fair. Vir addresses it in layers:
72
76
  inspect.
73
77
  - **Lint and dedupe.** `vir lint` flags contradictions and stale notes;
74
78
  `vir dedupe` merges similar notes that have drifted apart.
79
+ - **Active learning** via `vir review`. Walk through new distillations and
80
+ approve, edit, or reject each one. Verified notes get retrieval priority over
81
+ unverified ones (in `vir query` and the MCP server). Rejected notes are moved
82
+ to `.rejected/` — recoverable, not deleted.
75
83
 
76
84
  The bet: with these controls, signal-to-noise stays high enough that the vault
77
85
  is a net positive. If your discipline is strong enough to maintain `CLAUDE.md`
@@ -91,6 +99,13 @@ cross-linked with wikilinks and indexed. State lives in local SQLite; content
91
99
  hashes make reruns idempotent. Optional Ollama embeddings power semantic search,
92
100
  and an MCP server exposes the whole vault to Claude Code mid-session.
93
101
 
102
+ Web articles saved to a `raw/` directory (e.g. via Obsidian Web Clipper) flow
103
+ through a **parallel pipeline** with its own taxonomy — `concept`, `technique`,
104
+ `reference`, `opinion` — filed under `articles/` in the same vault, embedded and
105
+ indexed alongside session notes, and queryable through the same MCP tools.
106
+ Articles always keep their source URL in frontmatter for backlinks; distillation
107
+ paraphrases and never reproduces more than a short quote.
108
+
94
109
  ```
95
110
  Claude Code sessions
96
111
 
@@ -191,11 +206,16 @@ npm install -g @djolex999/vir-cli
191
206
  ## Quick start
192
207
 
193
208
  ```bash
194
- vir init # guided wizard: provider, models, vault, cadence
209
+ vir init # guided wizard: provider, models, vault, cadence,
210
+ # and an optional web-articles (raw/) folder
195
211
  vir run # one pass over your sessions → notes in your vault
196
212
  vir schedule install # register the daemon (runs every 3h by default)
197
213
  ```
198
214
 
215
+ `vir init` asks whether you save web articles to a folder (e.g. Obsidian Web
216
+ Clipper). Point it at that `raw/` directory and Vir distills those articles into
217
+ the same vault. Leave it blank to keep Vir session-only.
218
+
199
219
  `vir schedule install` works on Linux too: systemd is preferred, with cron used
200
220
  as a fallback when `systemctl` isn't available.
201
221
 
@@ -238,6 +258,7 @@ with your distro, init system, and Node version.
238
258
  | `vir run` | cheap | Process new sessions |
239
259
  | `vir run --full` | $$ | Reprocess all sessions |
240
260
  | `vir run --rewrite-only` | free | Reformat notes, no API calls |
261
+ | `vir run --articles-only` | cheap | Distill only web articles, skip sessions |
241
262
  | `vir run --yes` | cheap | Skip cost confirmation |
242
263
  | `vir query "<question>"` | cheap | Semantic search your vault |
243
264
  | `vir summarize <project>` | cheap | Cross-session project synthesis |
@@ -247,6 +268,9 @@ with your distro, init system, and Node version.
247
268
  | `vir lint --stale` | free | Staleness check only |
248
269
  | `vir lint --contradictions` | cheap | Contradiction check (Haiku) |
249
270
  | `vir dedupe` | cheap | Interactive duplicate detection |
271
+ | `vir review` | free | Walk new notes: approve/edit/reject |
272
+ | `vir review --project <s>` | free | Review one project's notes |
273
+ | `vir review --all` | free | Re-review, including verified notes |
250
274
  | `vir sync-claude` | free | Inject top knowledge into CLAUDE.md |
251
275
  | `vir sync-claude --dry-run` | free | Preview changes, no writes |
252
276
  | `vir sync-claude --force` | free | Apply without confirmation |
@@ -268,8 +292,13 @@ Register Vir with Claude Code:
268
292
  vir mcp install
269
293
  ```
270
294
 
271
- Restart Claude Code. The vault is now queryable mid-session via four tools:
272
- `vir_query`, `vir_status`, `vir_recent_notes`, `vir_project_summary`.
295
+ Restart Claude Code. The vault is now queryable mid-session via five tools:
296
+ `vir_query`, `vir_status`, `vir_recent_notes`, `vir_recent_articles`,
297
+ `vir_project_summary`. `vir_query` takes a `type` filter
298
+ (`session` | `article` | `all`) so Claude can scope a search to your dev
299
+ sessions or your saved articles. Human-verified notes (approved via
300
+ `vir review`) are ranked first; pass `verified_only: true` to `vir_query` or
301
+ `vir_recent_notes` to see only those.
273
302
 
274
303
  To unregister:
275
304
 
@@ -310,6 +339,8 @@ Located at `~/.vir/config.json`.
310
339
  | `anthropicApiKey` | — | Required if `provider=anthropic` |
311
340
  | `kieApiKey` | — | Required if `provider=kie` |
312
341
  | `filterThreshold` | `0.4` | Heuristic pre-filter (0..1) |
342
+ | `articlesDir` | _(unset)_ | `raw/` dir for web articles. Unset → article ingestion off |
343
+ | `distillArticles` | `true` | Distill articles alongside sessions (needs `articlesDir`) |
313
344
  | `filterToolCalls` | `moderate` | Tool-output filtering: `aggressive` \| `moderate` \| `off` |
314
345
  | `models.classify` | `claude-haiku-4-5-20251001` | Classify model |
315
346
  | `models.distill` | `claude-sonnet-4-6` | Distill model |
@@ -324,6 +355,7 @@ vault/vir/
324
355
  gotchas/ # bugs, footguns, and edge cases
325
356
  decisions/ # architecture decisions with their rationale
326
357
  tools/ # per-tool knowledge and usage notes
358
+ articles/ # web articles distilled from your raw/ folder
327
359
  projects/ # cross-session project summaries
328
360
  archived/ # deduplicated notes (kept, never deleted)
329
361
  ```
@@ -340,7 +372,7 @@ vault/vir/
340
372
 
341
373
  | | |
342
374
  | -------------- | ----------------------------------------- |
343
- | Tests | 50 passing |
375
+ | Tests | 79 passing |
344
376
  | Platforms | macOS (launchd), Linux (systemd/cron) |
345
377
  | Node | 20+ |
346
378
  | First-run cost | $1–5 (Kie.ai recommended for 72% savings) |
@@ -349,7 +381,9 @@ vault/vir/
349
381
  ## Roadmap
350
382
 
351
383
  - [x] Linux support (systemd timer + cron fallback) — experimental
352
- - [ ] Active learning — `vir review` to approve, edit, or reject distillations, with verified notes prioritized in retrieval
384
+ - [x] Active learning — `vir review` to approve, edit, or reject distillations, with verified notes prioritized in retrieval
385
+ - [x] Web article ingestion — distill markdown clipped via Obsidian Web Clipper into the same vault (the LLM Wiki pivot)
386
+ - [ ] More input sources — PDFs, code repos, images (the full LLM Wiki pattern)
353
387
  - [ ] Windows support
354
388
  - [ ] GUI installer for non-developers
355
389
  - [ ] Obsidian plugin for in-vault queries
@@ -0,0 +1,262 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import { stdin, stdout } from "node:process";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { loadConfig } from "../config.js";
7
+ import { kebab } from "../pipeline/writer.js";
8
+ import * as ui from "../ui/display.js";
9
+ // The four typed category dirs hold reviewable notes. `.rejected/`, `archived/`,
10
+ // `projects/`, index.md and log.md are intentionally never walked.
11
+ const CATEGORY_DIRS = ["patterns", "gotchas", "decisions", "tools"];
12
+ const REJECTED_DIR = ".rejected";
13
+ // Frontmatter is line-oriented `key: value`. Mirrors mcp/server.ts so review
14
+ // reads notes the same way the rest of the codebase does — strips surrounding
15
+ // quotes so a value like `topic: "x"` parses to `x`.
16
+ export function parseFrontmatter(content) {
17
+ const m = content.match(/^---\n([\s\S]*?)\n---/);
18
+ const block = m?.[1];
19
+ if (block === undefined)
20
+ return {};
21
+ const out = {};
22
+ for (const line of block.split("\n")) {
23
+ const idx = line.indexOf(":");
24
+ if (idx === -1)
25
+ continue;
26
+ const key = line.slice(0, idx).trim();
27
+ if (key.length === 0)
28
+ continue;
29
+ let val = line.slice(idx + 1).trim();
30
+ if ((val.startsWith('"') && val.endsWith('"')) ||
31
+ (val.startsWith("'") && val.endsWith("'"))) {
32
+ val = val.slice(1, -1).replace(/\\"/g, '"');
33
+ }
34
+ out[key] = val;
35
+ }
36
+ return out;
37
+ }
38
+ // Upsert keys in the YAML frontmatter, preserving every other line (and the
39
+ // whole body) verbatim. Existing keys are replaced in place; new keys are
40
+ // appended just before the closing `---`. Values are written raw — callers
41
+ // pass already-safe scalars (booleans, ISO dates).
42
+ export function setFrontmatter(content, updates) {
43
+ const m = content.match(/^(---\n)([\s\S]*?)(\n---)/);
44
+ if (!m) {
45
+ const block = Object.entries(updates)
46
+ .map(([k, v]) => `${k}: ${v}`)
47
+ .join("\n");
48
+ return `---\n${block}\n---\n\n${content}`;
49
+ }
50
+ const remaining = { ...updates };
51
+ const lines = (m[2] ?? "").split("\n").map((line) => {
52
+ const idx = line.indexOf(":");
53
+ if (idx === -1)
54
+ return line;
55
+ const key = line.slice(0, idx).trim();
56
+ if (key in remaining) {
57
+ const val = remaining[key];
58
+ delete remaining[key];
59
+ return `${key}: ${val}`;
60
+ }
61
+ return line;
62
+ });
63
+ for (const [k, v] of Object.entries(remaining))
64
+ lines.push(`${k}: ${v}`);
65
+ return ((m[1] ?? "---\n") +
66
+ lines.join("\n") +
67
+ (m[3] ?? "\n---") +
68
+ content.slice((m.index ?? 0) + m[0].length));
69
+ }
70
+ // Approve: stamp verified + reviewed_at. Re-reads the file each call so it also
71
+ // captures any edits made via $EDITOR immediately before approval.
72
+ export function approveNote(filePath, now = new Date().toISOString()) {
73
+ const content = readFileSync(filePath, "utf8");
74
+ writeFileSync(filePath, setFrontmatter(content, { verified: "true", reviewed_at: now }));
75
+ }
76
+ // Reject: stamp rejected_at and move the note into `.rejected/` (recoverable,
77
+ // never deleted). Returns the new path.
78
+ export function rejectNote(filePath, vaultRoot, now = new Date().toISOString()) {
79
+ const content = readFileSync(filePath, "utf8");
80
+ const updated = setFrontmatter(content, { rejected_at: now });
81
+ const rejectedDir = join(vaultRoot, REJECTED_DIR);
82
+ if (!existsSync(rejectedDir))
83
+ mkdirSync(rejectedDir, { recursive: true });
84
+ const dest = join(rejectedDir, basename(filePath));
85
+ writeFileSync(dest, updated);
86
+ rmSync(filePath);
87
+ return dest;
88
+ }
89
+ // Walk the category dirs and return reviewable notes, newest first. Default
90
+ // behavior hides verified notes (and `.rejected/` is never on the walk path);
91
+ // `all` includes verified ones for re-review.
92
+ export function collectNotes(vaultRoot, opts = {}) {
93
+ const projSlug = opts.project ? kebab(opts.project) : null;
94
+ const out = [];
95
+ for (const dir of CATEGORY_DIRS) {
96
+ const full = join(vaultRoot, dir);
97
+ if (!existsSync(full))
98
+ continue;
99
+ let names;
100
+ try {
101
+ names = readdirSync(full);
102
+ }
103
+ catch {
104
+ continue;
105
+ }
106
+ for (const name of names) {
107
+ if (!name.endsWith(".md"))
108
+ continue;
109
+ const filePath = join(full, name);
110
+ let content;
111
+ try {
112
+ content = readFileSync(filePath, "utf8");
113
+ }
114
+ catch {
115
+ continue;
116
+ }
117
+ const fm = parseFrontmatter(content);
118
+ const verified = fm.verified === "true";
119
+ if (!opts.all && verified)
120
+ continue;
121
+ if (projSlug && kebab(fm.project ?? "") !== projSlug)
122
+ continue;
123
+ out.push({
124
+ filePath,
125
+ relPath: join(dir, name),
126
+ topic: fm.topic ?? name.replace(/\.md$/, ""),
127
+ category: fm.category ?? dir.replace(/s$/, ""),
128
+ project: fm.project ?? "",
129
+ confidence: Number(fm.confidence ?? "0") || 0,
130
+ date: fm.date ?? "",
131
+ verified,
132
+ });
133
+ }
134
+ }
135
+ out.sort((a, b) => b.date.localeCompare(a.date));
136
+ if (opts.limit && opts.limit > 0)
137
+ return out.slice(0, opts.limit);
138
+ return out;
139
+ }
140
+ // Body sans frontmatter and the injected "Project:/Category:" wikilink header,
141
+ // collapsed to a single paragraph for a compact preview.
142
+ function excerpt(content) {
143
+ const body = content.replace(/^---\n[\s\S]*?\n---\n?/, "");
144
+ const lines = body.split("\n");
145
+ // Drop the leading wikilink header lines and any blank padding around them.
146
+ while (lines.length > 0) {
147
+ const first = (lines[0] ?? "").trim();
148
+ if (first === "" || /^(Project|Category):/.test(first)) {
149
+ lines.shift();
150
+ continue;
151
+ }
152
+ break;
153
+ }
154
+ return lines.join(" ").replace(/\s+/g, " ").trim();
155
+ }
156
+ // Opens the note in $EDITOR (or $VISUAL), falling back to nano. Synchronous so
157
+ // the review loop blocks until the editor exits. Returns false if the editor
158
+ // couldn't be launched at all.
159
+ function openInEditor(filePath) {
160
+ const editor = process.env.EDITOR || process.env.VISUAL || "nano";
161
+ const res = spawnSync(editor, [filePath], { stdio: "inherit" });
162
+ return !res.error;
163
+ }
164
+ function renderNote(n, idx, total) {
165
+ const catColor = ui.colorForCategory[n.category] ?? ui.text;
166
+ ui.line(`${ui.dim(`[${idx + 1}/${total}]`)} ${ui.text(ui.shortNotePath(n.relPath))}`);
167
+ ui.line(`${catColor(n.category)} ${ui.dim(ui.BULLET)} ${ui.text(n.project || "—")}` +
168
+ ` ${ui.dim(ui.BULLET)} ${ui.dim("conf")} ${ui.info(n.confidence.toFixed(2))}` +
169
+ (n.verified ? ` ${ui.dim(ui.BULLET)} ${ui.success("verified")}` : ""));
170
+ ui.blank();
171
+ let body = "";
172
+ try {
173
+ body = excerpt(readFileSync(n.filePath, "utf8"));
174
+ }
175
+ catch {
176
+ body = "";
177
+ }
178
+ ui.line(ui.dim(ui.wrap(body.slice(0, 320), 64)));
179
+ ui.blank();
180
+ }
181
+ export async function runReview(opts) {
182
+ const cfg = loadConfig();
183
+ const vaultRoot = join(cfg.vaultPath, cfg.outputDir);
184
+ const parsedLimit = opts.limit ? Number.parseInt(opts.limit, 10) : 50;
185
+ const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 50;
186
+ const notes = collectNotes(vaultRoot, {
187
+ all: opts.all,
188
+ project: opts.project,
189
+ limit,
190
+ });
191
+ ui.header("review");
192
+ ui.blank();
193
+ if (notes.length === 0) {
194
+ ui.row(ui.success(ui.CHECK), ui.text(opts.all
195
+ ? "no notes found to review"
196
+ : "no unreviewed notes — you're all caught up"));
197
+ return;
198
+ }
199
+ const scope = opts.project ? ` in ${opts.project}` : "";
200
+ ui.line(ui.dim(`Found ${notes.length} ${opts.all ? "" : "unreviewed "}note${notes.length === 1 ? "" : "s"}${scope}.`));
201
+ ui.blank();
202
+ const rl = createInterface({ input: stdin, output: stdout });
203
+ let approved = 0;
204
+ let edited = 0;
205
+ let rejected = 0;
206
+ let skipped = 0;
207
+ let quit = false;
208
+ try {
209
+ for (let i = 0; i < notes.length; i += 1) {
210
+ const n = notes[i];
211
+ if (!n)
212
+ continue;
213
+ ui.divider();
214
+ renderNote(n, i, notes.length);
215
+ const ans = (await rl.question(ui.muted("[a]pprove [e]dit [r]eject [s]kip [q]uit: ")))
216
+ .trim()
217
+ .toLowerCase();
218
+ if (ans === "a") {
219
+ approveNote(n.filePath);
220
+ approved += 1;
221
+ ui.row(ui.success(ui.CHECK), ui.text("approved"));
222
+ }
223
+ else if (ans === "e") {
224
+ rl.pause();
225
+ const launched = openInEditor(n.filePath);
226
+ rl.resume();
227
+ if (!launched) {
228
+ ui.row(ui.warn(ui.WARN_GLYPH), ui.text("could not open editor — left unreviewed"));
229
+ skipped += 1;
230
+ continue;
231
+ }
232
+ approveNote(n.filePath);
233
+ edited += 1;
234
+ ui.row(ui.success(ui.CHECK), ui.text("edited + approved"));
235
+ }
236
+ else if (ans === "r") {
237
+ const dest = rejectNote(n.filePath, vaultRoot);
238
+ rejected += 1;
239
+ ui.row(ui.warn(ui.CROSS), ui.text(`rejected → ${ui.shortNotePath(dest)}`));
240
+ }
241
+ else if (ans === "q") {
242
+ quit = true;
243
+ break;
244
+ }
245
+ else {
246
+ // skip (explicit [s], empty, or any unrecognized key): no mutation.
247
+ skipped += 1;
248
+ }
249
+ }
250
+ }
251
+ finally {
252
+ rl.close();
253
+ }
254
+ const reviewed = approved + edited + rejected;
255
+ ui.blank();
256
+ ui.divider();
257
+ ui.line(ui.text(`Reviewed ${reviewed} note${reviewed === 1 ? "" : "s"}: ` +
258
+ `${approved} approved, ${edited} edited, ${rejected} rejected, ${skipped} skipped.` +
259
+ (quit ? ui.dim(" (quit early)") : "")));
260
+ ui.divider();
261
+ }
262
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/cli/review.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -21,6 +21,7 @@ import { embeddingForNote, isOllamaAvailable, } from "./search/embedder.js";
21
21
  import { search } from "./search/retriever.js";
22
22
  import { synthesize } from "./search/synthesizer.js";
23
23
  import { runMcpServer } from "./mcp/server.js";
24
+ import { runReview } from "./cli/review.js";
24
25
  import { installToClaudeCode, isClaudeAvailable, isInstalled, uninstallFromClaudeCode, } from "./mcp/install.js";
25
26
  import { install as installDaemon, status as daemonStatus, uninstall as uninstallDaemon, } from "./daemon/index.js";
26
27
  import { StateDb } from "./state/db.js";
@@ -48,17 +49,20 @@ program
48
49
  .option("--full", "Re-process all sessions, ignoring state cache")
49
50
  .option("--daemon", "Quiet output, write to daemon log file")
50
51
  .option("--rewrite-only", "Skip scan/filter/LLM; re-render stored notes from SQLite")
52
+ .option("--articles-only", "Distill only web articles, skip sessions")
51
53
  .option("--yes", "Skip the cost confirmation prompt")
52
54
  .action(async (opts) => {
53
55
  const cfg = loadConfig();
54
56
  const daemon = opts.daemon === true;
55
57
  const rewriteOnly = opts.rewriteOnly === true;
56
- const skipPrompt = opts.yes === true || daemon || rewriteOnly;
58
+ const articlesOnly = opts.articlesOnly === true;
59
+ const skipPrompt = opts.yes === true || daemon || rewriteOnly || articlesOnly;
57
60
  await runPipeline(cfg, {
58
61
  full: opts.full,
59
62
  quiet: daemon,
60
63
  logToFile: daemon,
61
64
  rewriteOnly,
65
+ articlesOnly,
62
66
  onConfirm: skipPrompt
63
67
  ? undefined
64
68
  : async (newCount) => confirmCostIfNeeded(cfg, newCount),
@@ -492,6 +496,13 @@ program
492
496
  ui.blank();
493
497
  renderDaemon(ds, cfg.cadenceHours);
494
498
  });
499
+ program
500
+ .command("review")
501
+ .description("Walk through new distilled notes and approve/edit/reject")
502
+ .option("--all", "Review all notes, including verified ones")
503
+ .option("--project <slug>", "Filter by project")
504
+ .option("--limit <n>", "Max notes to review in this session", "50")
505
+ .action(runReview);
495
506
  program
496
507
  .command("doctor")
497
508
  .description("Run diagnostic checks on Vir installation")
@@ -508,7 +519,8 @@ Quick start:
508
519
  After installing, restart Claude Code. Tools become available:
509
520
  vir_query search the vault (synthesized answer + sources)
510
521
  vir_status knowledge base overview + gaps
511
- vir_recent_notes most recently distilled notes
522
+ vir_recent_notes most recently distilled session notes
523
+ vir_recent_articles most recently distilled web articles
512
524
  vir_project_summary synthesized per-project summary`);
513
525
  // Shared by `vir mcp run` and the bare `vir mcp` alias below.
514
526
  const runMcp = async () => {
@@ -676,6 +688,43 @@ async function cmdInit() {
676
688
  if (cont)
677
689
  break;
678
690
  }
691
+ // ── web articles (optional second input source) ─────────────────────────
692
+ let articlesDir = existing?.articlesDir;
693
+ const wantsArticles = await confirm({
694
+ message: "Do you save web articles to a folder (e.g. Obsidian Web Clipper)?",
695
+ default: existing?.articlesDir !== undefined,
696
+ });
697
+ if (wantsArticles) {
698
+ for (;;) {
699
+ articlesDir = await input({
700
+ message: "Articles (raw/) directory",
701
+ default: existing?.articlesDir ??
702
+ join(homedir(), "Documents", "Obsidian", "raw"),
703
+ });
704
+ const expanded = expandHome(articlesDir);
705
+ if (existsSync(expanded))
706
+ break;
707
+ const create = await confirm({
708
+ message: `Path does not exist (${expanded}). Create it?`,
709
+ default: true,
710
+ });
711
+ if (create) {
712
+ try {
713
+ mkdirSync(expanded, { recursive: true });
714
+ break;
715
+ }
716
+ catch (err) {
717
+ console.error(chalk.red(`failed to create: ${err.message}`));
718
+ }
719
+ }
720
+ else {
721
+ break;
722
+ }
723
+ }
724
+ }
725
+ else {
726
+ articlesDir = undefined;
727
+ }
679
728
  const cadenceHours = Number(await input({
680
729
  message: "Cadence (hours)",
681
730
  default: String(existing?.cadenceHours ?? 3),
@@ -779,6 +828,8 @@ async function cmdInit() {
779
828
  anthropicApiKey,
780
829
  kieApiKey,
781
830
  filterThreshold,
831
+ articlesDir,
832
+ distillArticles: existing?.distillArticles,
782
833
  filterToolCalls: existing?.filterToolCalls,
783
834
  models: { classify: classifyModel, distill: distillModel },
784
835
  });