@djolex999/vir-cli 0.3.0 → 0.3.2
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/dist/daemon/index.js +14 -7
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/systemd.js +53 -12
- package/dist/daemon/systemd.js.map +1 -1
- package/package.json +7 -1
- package/AGENTS.md +0 -201
package/dist/daemon/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { realpathSync } from "node:fs";
|
|
|
2
2
|
import * as cron from "./cron.js";
|
|
3
3
|
import * as launchd from "./launchd.js";
|
|
4
4
|
import * as systemd from "./systemd.js";
|
|
5
|
-
import { SystemdNotAvailableError } from "./systemd.js";
|
|
5
|
+
import { SystemdNotAvailableError, SystemdUserBusUnavailableError, } from "./systemd.js";
|
|
6
6
|
const NONE = {
|
|
7
7
|
installed: false,
|
|
8
8
|
active: false,
|
|
@@ -35,23 +35,30 @@ export async function install(cfg) {
|
|
|
35
35
|
}
|
|
36
36
|
if (platform === "linux") {
|
|
37
37
|
const opts = { ...resolvePaths(), cadenceHours: cfg.cadenceHours };
|
|
38
|
-
// systemd preferred
|
|
39
|
-
//
|
|
38
|
+
// systemd preferred. Fall back to cron when systemctl is missing
|
|
39
|
+
// (SystemdNotAvailableError) OR present-but-no-user-bus
|
|
40
|
+
// (SystemdUserBusUnavailableError — common on WSL/containers). Any other
|
|
41
|
+
// error is a real misconfig and propagates.
|
|
40
42
|
try {
|
|
41
43
|
systemd.install(opts);
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
46
|
catch (err) {
|
|
45
|
-
if (!(err instanceof SystemdNotAvailableError)
|
|
47
|
+
if (!(err instanceof SystemdNotAvailableError) &&
|
|
48
|
+
!(err instanceof SystemdUserBusUnavailableError)) {
|
|
46
49
|
throw err;
|
|
50
|
+
}
|
|
47
51
|
}
|
|
48
52
|
if (cron.isCronAvailable()) {
|
|
49
53
|
cron.install(opts);
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
52
|
-
throw new Error("
|
|
53
|
-
"
|
|
54
|
-
"
|
|
56
|
+
throw new Error("No daemon backend available.\n" +
|
|
57
|
+
" - systemd user bus not reachable (common on WSL, containers)\n" +
|
|
58
|
+
" - cron command not found\n" +
|
|
59
|
+
" Install one to use vir schedule:\n" +
|
|
60
|
+
" Ubuntu/Debian/WSL: sudo apt install cron\n" +
|
|
61
|
+
" Arch: sudo pacman -S cronie");
|
|
55
62
|
}
|
|
56
63
|
throwUnsupported(platform);
|
|
57
64
|
}
|
package/dist/daemon/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/daemon/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/daemon/index.ts"],"names":[],"mappings":"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"}
|
package/dist/daemon/systemd.js
CHANGED
|
@@ -10,6 +10,24 @@ export class SystemdNotAvailableError extends Error {
|
|
|
10
10
|
this.name = "SystemdNotAvailableError";
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
// Thrown when systemctl is on PATH but the user bus/manager is unreachable —
|
|
14
|
+
// the classic WSL/container case. Distinct from SystemdNotAvailableError so the
|
|
15
|
+
// router can fall back to cron for both. install() probes for this BEFORE
|
|
16
|
+
// writing any unit files.
|
|
17
|
+
export class SystemdUserBusUnavailableError extends Error {
|
|
18
|
+
constructor() {
|
|
19
|
+
super("systemd user bus not reachable");
|
|
20
|
+
this.name = "SystemdUserBusUnavailableError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Decides, from a `systemctl --user …` result, whether the failure is the user
|
|
24
|
+
// bus being unreachable. The definitive marker is systemctl's "Failed to
|
|
25
|
+
// connect to … bus" message (WSL, containers, no $XDG_RUNTIME_DIR). A non-zero
|
|
26
|
+
// exit *without* that marker (e.g. stdout "degraded") means the bus IS
|
|
27
|
+
// reachable, so we don't treat it as unavailable.
|
|
28
|
+
export function isUserBusUnavailable(probe) {
|
|
29
|
+
return /Failed to connect to .*bus/i.test(`${probe.stdout}\n${probe.stderr}`);
|
|
30
|
+
}
|
|
13
31
|
// User mode only — never touch /etc/systemd/system.
|
|
14
32
|
const SYSTEMD_USER_DIR = join(homedir(), ".config", "systemd", "user");
|
|
15
33
|
const SERVICE_PATH = join(SYSTEMD_USER_DIR, "vir.service");
|
|
@@ -55,31 +73,54 @@ export function parseTimerCadence(timer) {
|
|
|
55
73
|
const m = timer.match(/OnUnitActiveSec=(\d+)h/);
|
|
56
74
|
return m && m[1] ? Number(m[1]) : null;
|
|
57
75
|
}
|
|
76
|
+
// Turn a failed systemctl result into a typed error: a bus-connection failure
|
|
77
|
+
// becomes SystemdUserBusUnavailableError (so the router falls back to cron),
|
|
78
|
+
// anything else stays a generic Error (a real misconfig the user should see).
|
|
79
|
+
function classifySystemctlError(op, res) {
|
|
80
|
+
if (isUserBusUnavailable(res))
|
|
81
|
+
return new SystemdUserBusUnavailableError();
|
|
82
|
+
return new Error(`systemctl ${op} failed: ${res.stderr.trim()}`);
|
|
83
|
+
}
|
|
84
|
+
function removeUnitFiles() {
|
|
85
|
+
if (existsSync(SERVICE_PATH))
|
|
86
|
+
rmSync(SERVICE_PATH);
|
|
87
|
+
if (existsSync(TIMER_PATH))
|
|
88
|
+
rmSync(TIMER_PATH);
|
|
89
|
+
}
|
|
58
90
|
export function install(opts) {
|
|
59
91
|
if (!isSystemdAvailable())
|
|
60
92
|
throw new SystemdNotAvailableError();
|
|
93
|
+
// systemctl can be on PATH while the user bus is unreachable (WSL,
|
|
94
|
+
// containers). Probe before writing anything so we never leave stale unit
|
|
95
|
+
// files behind in that case.
|
|
96
|
+
const probe = systemctl(["--user", "is-system-running"]);
|
|
97
|
+
if (isUserBusUnavailable(probe))
|
|
98
|
+
throw new SystemdUserBusUnavailableError();
|
|
61
99
|
if (!existsSync(SYSTEMD_USER_DIR)) {
|
|
62
100
|
mkdirSync(SYSTEMD_USER_DIR, { recursive: true });
|
|
63
101
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
102
|
+
try {
|
|
103
|
+
writeFileSync(SERVICE_PATH, renderService({ nodePath: opts.nodePath, cliPath: opts.cliPath }));
|
|
104
|
+
writeFileSync(TIMER_PATH, renderTimer(opts.cadenceHours));
|
|
105
|
+
const reload = systemctl(["--user", "daemon-reload"]);
|
|
106
|
+
if (reload.code !== 0)
|
|
107
|
+
throw classifySystemctlError("daemon-reload", reload);
|
|
108
|
+
const enable = systemctl(["--user", "enable", "--now", TIMER_UNIT]);
|
|
109
|
+
if (enable.code !== 0)
|
|
110
|
+
throw classifySystemctlError("enable", enable);
|
|
69
111
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw
|
|
112
|
+
catch (err) {
|
|
113
|
+
// Partial install — remove the unit files we wrote so a retry (or the cron
|
|
114
|
+
// fallback) starts clean — then re-throw for the router to classify.
|
|
115
|
+
removeUnitFiles();
|
|
116
|
+
throw err;
|
|
73
117
|
}
|
|
74
118
|
}
|
|
75
119
|
export function uninstall() {
|
|
76
120
|
if (!isSystemdAvailable())
|
|
77
121
|
return;
|
|
78
122
|
systemctl(["--user", "disable", "--now", TIMER_UNIT]);
|
|
79
|
-
|
|
80
|
-
rmSync(SERVICE_PATH);
|
|
81
|
-
if (existsSync(TIMER_PATH))
|
|
82
|
-
rmSync(TIMER_PATH);
|
|
123
|
+
removeUnitFiles();
|
|
83
124
|
systemctl(["--user", "daemon-reload"]);
|
|
84
125
|
}
|
|
85
126
|
export function status() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"systemd.js","sourceRoot":"","sources":["../../src/daemon/systemd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,sEAAsE;AACtE,2CAA2C;AAC3C,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACjD;QACE,KAAK,CAAC,qBAAqB,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED,oDAAoD;AACpD,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;AACvE,MAAM,YAAY,GAAG,IAAI,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC;AAC3D,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;AACvD,MAAM,UAAU,GAAG,WAAW,CAAC;AAe/B,MAAM,UAAU,kBAAkB;IAChC,OAAO,SAAS,CAAC,OAAO,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,SAAS,CAAC,IAAc;IAK/B,MAAM,GAAG,GAAG,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAC/D,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;QACtB,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE;QACxB,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE;KACzB,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,IAG7B;IACC,OAAO;;;;;YAKG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO;;;CAGxC,CAAC;AACF,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,YAAoB;IAC9C,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;IAChD,OAAO;4BACmB,CAAC;;;;kBAIX,CAAC;;;;CAIlB,CAAC;AACF,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAChD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,IAAiB;IACvC,IAAI,CAAC,kBAAkB,EAAE;QAAE,MAAM,IAAI,wBAAwB,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"systemd.js","sourceRoot":"","sources":["../../src/daemon/systemd.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djolex999/vir-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Distills Claude Code sessions into a compounding knowledge vault",
|
|
5
5
|
"author": "Djordje Marković <djordje@growthq.rs>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
"bin": {
|
|
25
25
|
"vir": "dist/cli.js"
|
|
26
26
|
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"assets",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
27
33
|
"scripts": {
|
|
28
34
|
"build": "tsc",
|
|
29
35
|
"dev": "tsc --watch",
|
package/AGENTS.md
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# vir
|
|
2
|
-
|
|
3
|
-
Local macOS daemon that distills Codex session transcripts into an
|
|
4
|
-
Obsidian vault.
|
|
5
|
-
|
|
6
|
-
Published to npm as `@djolex999/vir-cli` (scoped). Install:
|
|
7
|
-
`npm install -g @djolex999/vir-cli`. Repo: https://github.com/djolex999/vir. Reads `~/.Codex/projects/**/*.jsonl`, filters by heuristic,
|
|
8
|
-
classifies with Haiku, extracts durable knowledge with Sonnet, writes typed
|
|
9
|
-
notes to the vault, maintains an index, and syncs back into AGENTS.md files.
|
|
10
|
-
|
|
11
|
-
## Stack
|
|
12
|
-
|
|
13
|
-
- Node.js ≥ 20, TypeScript strict (`noImplicitAny`, `noUncheckedIndexedAccess`)
|
|
14
|
-
- `commander` for CLI, `chalk` for output, `zod` for config validation
|
|
15
|
-
- `better-sqlite3` for state (synchronous — no async complexity)
|
|
16
|
-
- `@anthropic-ai/sdk` for the Anthropic path; native `fetch` for the Kie path
|
|
17
|
-
- macOS `launchd` for the daemon, `osascript` for notifications
|
|
18
|
-
- `vitest` for unit tests (`npm test` → `vitest run`)
|
|
19
|
-
|
|
20
|
-
## Structure
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
src/
|
|
24
|
-
cli.ts # commander entry — every subcommand wired here
|
|
25
|
-
config.ts # ~/.vir/config.json schema + helpers
|
|
26
|
-
pipeline/
|
|
27
|
-
run.ts # orchestrator (scan → filter → scrub → distill → write)
|
|
28
|
-
scanner.ts # walk ~/.Codex/projects, SHA-256 each .jsonl
|
|
29
|
-
parser.ts # extract assistant/user text, tool calls, files touched
|
|
30
|
-
filter.ts # heuristic scorer (length, tools, files, signal words)
|
|
31
|
-
scrubber.ts # strip API keys, bearer tokens, absolute paths, emails
|
|
32
|
-
distiller.ts # callLLM helper, Haiku classify + Sonnet extract,
|
|
33
|
-
# withRateLimitRetry, buildAnthropicClient,
|
|
34
|
-
# normalizeModelName
|
|
35
|
-
writer.ts # frontmatter + body, wikilink injection, index/log
|
|
36
|
-
summarizer.ts # per-project knowledge summaries
|
|
37
|
-
types.ts # ParsedSession, Classification, DistilledNote, Category
|
|
38
|
-
search/
|
|
39
|
-
retriever.ts # unified async search: embeddings → TF-IDF fallback
|
|
40
|
-
embedder.ts # Ollama integration (nomic-embed-text), cosine sim
|
|
41
|
-
synthesizer.ts # Codex synthesis for `vir query`
|
|
42
|
-
lint/
|
|
43
|
-
linter.ts # orphans, staleness, contradictions
|
|
44
|
-
dedupe/
|
|
45
|
-
detector.ts # candidate pairs + LLM judgment
|
|
46
|
-
merger.ts # archive / keep / Sonnet-merge
|
|
47
|
-
Codex/
|
|
48
|
-
updater.ts # VIR:START / VIR:END block management
|
|
49
|
-
state/
|
|
50
|
-
db.ts # better-sqlite3, idempotent migrations
|
|
51
|
-
daemon/
|
|
52
|
-
launchd.ts # plist render + load/unload via spawnSync('launchctl')
|
|
53
|
-
mcp/
|
|
54
|
-
server.ts # @modelcontextprotocol/sdk stdio server — vir_query,
|
|
55
|
-
# vir_status, vir_recent_notes, vir_project_summary;
|
|
56
|
-
# thin facade over search+synthesize+summarizer, opens
|
|
57
|
-
# StateDb read-only, logs to stderr only
|
|
58
|
-
install.ts # register/unregister with Codex via
|
|
59
|
-
# spawnSync('Codex', ['mcp', 'add'|'remove'|'list', …])
|
|
60
|
-
ui/
|
|
61
|
-
display.ts # the ONE place console.log lives — palette, glyphs,
|
|
62
|
-
# header(), divider(), box(), spinner(), summary(),
|
|
63
|
-
# wrap(), sourceRow(), categoryRow()
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Key conventions
|
|
67
|
-
|
|
68
|
-
- **Path expansion.** All paths from config (`vaultPath`, `claudeProjectsDir`)
|
|
69
|
-
must run through `expandHome()` (or `os.homedir()` directly) before use.
|
|
70
|
-
Never assume `~/` works in `fs.*`.
|
|
71
|
-
- **Per-session try/catch.** The daemon must never die on a single bad
|
|
72
|
-
transcript. `pipeline/run.ts` wraps every session iteration and records the
|
|
73
|
-
error in the DB so it doesn't keep retrying.
|
|
74
|
-
- **Provider routing.**
|
|
75
|
-
- `provider: 'anthropic'` → `@anthropic-ai/sdk` (`buildAnthropicClient`).
|
|
76
|
-
- `provider: 'kie'` → native `fetch` to `https://api.kie.ai/Codex/v1/messages`
|
|
77
|
-
with `Authorization: Bearer <kieApiKey>`. The SDK is **not** used for Kie
|
|
78
|
-
even with a `baseURL` override — its `x-api-key` header conflicts.
|
|
79
|
-
- Both flow through `callLLM(config, client, opts)`.
|
|
80
|
-
- On the Kie path the Anthropic SDK is **never instantiated** —
|
|
81
|
-
`maybeAnthropicClient()` returns `null` for `provider: 'kie'`, and
|
|
82
|
-
`callLLM` accepts `Anthropic | null` (guards before the Anthropic branch).
|
|
83
|
-
All 6 LLM callers use `maybeAnthropicClient`, not `buildAnthropicClient`.
|
|
84
|
-
- **Model names.** `normalizeModelName(model, provider)` collapses any model
|
|
85
|
-
that *starts with* a Kie canonical id (`Codex-haiku-4-5`,
|
|
86
|
-
`Codex-sonnet-4-6`) back to the bare id. Fallback strips trailing
|
|
87
|
-
`-YYYYMMDD`. Anthropic path passes through untouched.
|
|
88
|
-
- **Retries.** Every LLM call is wrapped in `withRateLimitRetry()` — three
|
|
89
|
-
attempts with `60s / 120s / 240s` backoff. The `isRetryable()` predicate
|
|
90
|
-
retries `429` on both providers; on the Kie path (`HttpError`) it also
|
|
91
|
-
retries transient `5xx` (`500/502/503/504`). The Anthropic SDK path stays
|
|
92
|
-
`429`-only — the SDK retries `5xx` itself, so we don't double-retry.
|
|
93
|
-
`isRetryable` is exported and covered by `distiller.test.ts`. Sessions are
|
|
94
|
-
processed sequentially with a `2s` delay after each successful distill.
|
|
95
|
-
- **State is the source of truth.** `~/.vir/vir.db` records every session by
|
|
96
|
-
path with its SHA-256 hash. Reruns are idempotent; a session is only
|
|
97
|
-
re-distilled if its file content changes. Migrations are additive only
|
|
98
|
-
(`PRAGMA table_info(sessions)` + `ALTER TABLE ADD COLUMN`).
|
|
99
|
-
- **VIR:START / VIR:END markers are sacred.** `sync-Codex` only mutates bytes
|
|
100
|
-
between those markers. When a AGENTS.md has no block, the new one is
|
|
101
|
-
appended; the rest of the file is preserved verbatim.
|
|
102
|
-
- **No comments explaining obvious things.** Comment WHY a non-obvious
|
|
103
|
-
invariant exists — never WHAT the code does.
|
|
104
|
-
- **All user-facing output goes through `ui/display.ts`.** Other modules
|
|
105
|
-
must not call `console.log` directly. The pipeline's `fileLog()` writes
|
|
106
|
-
to `~/.vir/daemon.log` in plain text regardless of UI mode; the
|
|
107
|
-
display module renders only when `!opts.quiet`.
|
|
108
|
-
- **Ollama is best-effort.** `writer.maybeEmbed()` and the search path
|
|
109
|
-
both probe via `isOllamaAvailableCached()` and silently fall back
|
|
110
|
-
(TF-IDF for query; no-op for writer). An embedding failure must never
|
|
111
|
-
fail a write.
|
|
112
|
-
- **Cost prompts respect intent.** `--yes`, `--daemon`, and
|
|
113
|
-
`--rewrite-only` all skip the > 20 new-session confirmation. The
|
|
114
|
-
daemon path *never* prompts (it has no tty).
|
|
115
|
-
- **`vir init` is an @inquirer/* wizard.** Arrow-key `select` for
|
|
116
|
-
provider and models, `input` with `validate` for keys/numbers,
|
|
117
|
-
`confirm` for the "create missing path?" flow. Don't fall back to
|
|
118
|
-
raw readline here — the rest of the file already imports it for
|
|
119
|
-
dedupe/sync-Codex/cost prompts, but the init UX is the wizard.
|
|
120
|
-
- **`vir run` prints a preflight line.** `N files found · M cached · K
|
|
121
|
-
new` shows after the scan in dim text, and the same triple goes to
|
|
122
|
-
the daemon log. Diagnoses "fresh DB looks stale" misconfigurations
|
|
123
|
-
in one line.
|
|
124
|
-
- **MCP server is a read-only facade.** `vir mcp run` (and the bare `vir mcp`
|
|
125
|
-
alias) start an `@modelcontextprotocol/sdk` stdio server that reuses
|
|
126
|
-
`search()`, `synthesize()`, `summarizeProject()`, and
|
|
127
|
-
`db.listDistilled()`/`getStats()` — no new pipeline logic. It opens
|
|
128
|
-
`StateDb` with `{ readonly: true }` (skips the WAL pragma + migrations) and
|
|
129
|
-
must never mutate state. stdout is the JSON-RPC channel, so **all logs go to
|
|
130
|
-
stderr** via `process.stderr.write`. `SearchHit` carries no category/project,
|
|
131
|
-
so `vir_query` recovers them by parsing each note's frontmatter.
|
|
132
|
-
- **MCP registration shells out to `Codex`.** `vir mcp install/uninstall/
|
|
133
|
-
status` call `Codex mcp add|remove|list` via `spawnSync` arg-arrays (never
|
|
134
|
-
shell strings). A missing `Codex` binary (spawnSync `error.code === 'ENOENT'`)
|
|
135
|
-
must yield an actionable message, never a crash — `vir mcp status` shows
|
|
136
|
-
"Codex CLI not detected". The registration runs `vir mcp`, which is why the
|
|
137
|
-
bare alias must keep launching the server.
|
|
138
|
-
- **`vir --version` reads `package.json` at runtime.** `program.version()` loads
|
|
139
|
-
it from one dir up from `dist/cli.js` (rootDir is `./src`, so it can't be
|
|
140
|
-
imported) — never hardcode the version string or it drifts on every bump.
|
|
141
|
-
- **Tests run on Vitest.** `npm test` (`vitest run`). Test files are
|
|
142
|
-
`*.test.ts` colocated with source and listed in `tsconfig.json` `exclude`,
|
|
143
|
-
so `tsc`/`npm run build` never emit them to `dist/` (and `src/` is
|
|
144
|
-
npmignored, so they don't ship). Vitest discovers them via its own glob.
|
|
145
|
-
Import the unit-under-test with a `.js` extension (`./distiller.js`) per
|
|
146
|
-
NodeNext — Vitest resolves it to the `.ts`. Start with pure functions.
|
|
147
|
-
- **`sync-Codex` resolves project paths flexibly.** `projectClaudePath(slug)`
|
|
148
|
-
checks `~/projects/<slug>`, `~/projects/<slug>-*` (glob), `~/code/<slug>`,
|
|
149
|
-
and `~/dev/<slug>` (first existing wins; canonical fallback). The resolved
|
|
150
|
-
path flows into `PlanItem.target` and shows as the dry-run plan heading.
|
|
151
|
-
|
|
152
|
-
## Commands
|
|
153
|
-
|
|
154
|
-
```
|
|
155
|
-
vir init # interactive setup
|
|
156
|
-
vir run # one pass (used by daemon)
|
|
157
|
-
vir run --full # ignore state cache, re-process everything
|
|
158
|
-
vir run --rewrite-only # re-render notes from stored content
|
|
159
|
-
# (no scan, no LLM, free)
|
|
160
|
-
vir run --yes # skip the > 20 sessions cost prompt
|
|
161
|
-
vir schedule install # write + load ~/Library/LaunchAgents plist
|
|
162
|
-
vir schedule uninstall # unload + remove plist
|
|
163
|
-
vir status # daemon state + knowledge base breakdown
|
|
164
|
-
vir query "<question>" # embeddings (Ollama) → TF-IDF fallback
|
|
165
|
-
# → Codex synthesis
|
|
166
|
-
vir embed # generate Ollama embeddings for notes
|
|
167
|
-
vir embed --force # regenerate all embeddings
|
|
168
|
-
vir summarize <project> # generate per-project summary
|
|
169
|
-
vir summarize --all # all projects with notes
|
|
170
|
-
vir lint # orphans + stale + contradictions
|
|
171
|
-
vir lint --orphans # orphans only (free)
|
|
172
|
-
vir lint --stale # staleness only (free)
|
|
173
|
-
vir lint --contradictions # contradictions only (Haiku tokens)
|
|
174
|
-
vir dedupe # interactive duplicate review + merge
|
|
175
|
-
vir sync-Codex # diff then confirm AGENTS.md updates
|
|
176
|
-
vir sync-Codex --dry-run # diff only, never write
|
|
177
|
-
vir sync-Codex --force # apply without confirmation
|
|
178
|
-
vir sync-Codex <project> # specific project only
|
|
179
|
-
vir sync-Codex --global # only ~/.Codex/AGENTS.md
|
|
180
|
-
vir mcp # run MCP server over stdio (alias for run)
|
|
181
|
-
vir mcp run # run MCP server over stdio
|
|
182
|
-
vir mcp install # register with Codex (Codex mcp add)
|
|
183
|
-
vir mcp install --scope project # register at project scope (default: user)
|
|
184
|
-
vir mcp uninstall # unregister (Codex mcp remove vir)
|
|
185
|
-
vir mcp status # check registration (Codex mcp list)
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## File locations
|
|
189
|
-
|
|
190
|
-
- Config: `~/.vir/config.json`
|
|
191
|
-
- State: `~/.vir/vir.db` (better-sqlite3 with WAL). A one-shot rename in
|
|
192
|
-
`StateDb`'s constructor moves the pre-rename `~/.vir/state.db` over to
|
|
193
|
-
`~/.vir/vir.db` if it's still around — preserves cache, hides the
|
|
194
|
-
history of the doc/code mismatch.
|
|
195
|
-
- Daemon log: `~/.vir/daemon.log`
|
|
196
|
-
- launchd plist: `~/Library/LaunchAgents/lab.growthq.vir.plist`
|
|
197
|
-
- MCP registration: managed by Codex (`Codex mcp add/remove`), not by
|
|
198
|
-
vir — user scope writes to `~/.Codex.json`, project scope to `.mcp.json`
|
|
199
|
-
- Vault notes: `<vaultPath>/<outputDir>/{patterns,gotchas,decisions,tools,projects,archived}/`
|
|
200
|
-
- Vault index: `<vaultPath>/<outputDir>/index.md`
|
|
201
|
-
- Vault run log: `<vaultPath>/<outputDir>/log.md`
|