@vortex-os/base 0.12.0 → 0.12.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.
@@ -147,7 +147,7 @@ function parseStatuslineInput(text) {
147
147
  modelName: rawName.replace(/^Claude\s+/, ""),
148
148
  effortLevel: str(effort.level),
149
149
  transcriptPath: str(root.transcript_path),
150
- dir: str(workspace.current_dir) ?? str(root.cwd),
150
+ dir: str(workspace.project_dir) ?? str(workspace.current_dir) ?? str(root.cwd),
151
151
  contextWindowSize: windowSize > 0 ? windowSize : 2e5,
152
152
  usedPercentage: clampPct(num(ctx.used_percentage)),
153
153
  cacheReadTokens: nonneg(usage.cache_read_input_tokens),
@@ -498,4 +498,4 @@ export {
498
498
  ensureStatusline,
499
499
  runStatuslineCli
500
500
  };
501
- //# sourceMappingURL=chunk-EAKDR5B2.js.map
501
+ //# sourceMappingURL=chunk-P7IMUUNY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../plugins/session-rituals/src/statusline.ts","../../plugins/session-rituals/src/ensure-hooks.ts"],"sourcesContent":["/**\n * `vortex statusline` — a Claude Code statusLine renderer.\n *\n * Claude Code pipes a JSON snapshot of the session (model, context window,\n * cost, rate limits, workspace) to the configured statusLine command on every\n * refresh and renders whatever the command prints. This module turns that\n * snapshot into a colored status bar:\n *\n * full (default, 3 lines + a VortEX line inside an instance):\n * 🧠 Fable 5 │ █ max │ █░░░░░░░░░ 12% · 120K/1M │ 🕐 22:25 │ 💰 $9.22\n * 5h ██████░░ 75%(2h13m) │ 7d ███████░ 96%(0d8h) │ 📦 cache 98%\n * 📁 my-project │ ⎇ main 2b0949d │ ⏱ 1h19m │ +78 -38 │ ⧉ 1\n * 🌀 VortEX v0.11.0 │ last: 2026-06-10_0452-….md\n *\n * lite (1 line):\n * 📁 my-project │ ⎇ main │ 🧠 Fable 5 │ █ max │ ░░░░░░░░ 12% · 120K/1M │ 5h 75% · 7d 96% │ ⧉ 1 │ 🌀 v0.11.0 │ 🕐 22:25\n *\n * Design notes:\n * - Rendering is PURE (`renderStatusline(data, probes, mode)`) — every\n * environment lookup (git, process list, clock, VortEX instance files) is\n * collected separately in `collectStatuslineProbes`, so the composition is\n * unit-testable without a repo or a terminal.\n * - Reasoning effort is shown as one bar whose height + color encode the\n * level. Claude Code reports `ultracode` as plain `xhigh` (ultracode is\n * \"xhigh + workflow orchestration\"), so when the level reads `xhigh` we\n * additionally sniff the session transcript for the last `/effort` change\n * notice — a best-effort heuristic that degrades to showing `xhigh`.\n * - Every probe is wrapped: a statusline must NEVER throw or block the bar —\n * on any failure a segment silently falls back to a neutral value.\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport { closeSync, existsSync, openSync, readSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parseSettings, serializeSettings, type ClaudeSettings } from \"./ensure-hooks.js\";\n\n// --- ANSI palette (kept as raw escapes so the bar renders in any ANSI terminal) ---\nconst RST = \"\\x1b[0m\";\nconst CYAN = \"\\x1b[36m\";\nconst GREEN = \"\\x1b[32m\";\nconst YELLOW = \"\\x1b[33m\";\nconst RED = \"\\x1b[31m\";\nconst DIM = \"\\x1b[2m\";\nconst BOLD = \"\\x1b[1m\";\nconst WHITE = \"\\x1b[37m\";\nconst MAGENTA = \"\\x1b[35m\";\nconst BLUE = \"\\x1b[34m\";\nconst GREY = \"\\x1b[38;5;242m\";\nconst ORANGE = \"\\x1b[38;5;208m\";\n\nconst SEP = ` ${DIM}│${RST} `;\n\n/** The slice of Claude Code's statusLine input JSON this renderer consumes. */\nexport interface StatuslineData {\n readonly modelName: string;\n /** Reasoning effort level as reported (`effort.level`), or null. */\n readonly effortLevel: string | null;\n readonly transcriptPath: string | null;\n /**\n * Project ROOT directory. `workspace.project_dir` first — `current_dir` and\n * `cwd` FOLLOW the session shell as it `cd`s (measured), which made the bar\n * show a sibling repo and lose the VortEX line mid-session. The bar's\n * identity (project name, git, instance line) stays pinned to the project.\n */\n readonly dir: string | null;\n readonly contextWindowSize: number;\n /** Context used, percent — kept as reported (may be fractional). */\n readonly usedPercentage: number;\n readonly cacheReadTokens: number;\n readonly cacheCreationTokens: number;\n readonly costUsd: number;\n readonly durationMs: number;\n readonly linesAdded: number;\n readonly linesRemoved: number;\n readonly fiveHourUsedPct: number;\n readonly fiveHourResetsAt: number;\n readonly sevenDayUsedPct: number;\n readonly sevenDayResetsAt: number;\n}\n\n/** Environment lookups the renderer composes in — collected impurely, injected purely. */\nexport interface StatuslineProbes {\n readonly gitBranch: string;\n readonly gitHash: string;\n readonly sessionCount: number;\n /** Set when `dir` is a VortEX instance root; lastWorklog is the newest worklog file name. */\n readonly vortex: { readonly version: string | null; readonly lastWorklog: string | null } | null;\n /** Effort level after the ultracode transcript sniff (null → use data.effortLevel). */\n readonly effortLevel: string | null;\n readonly now: Date;\n}\n\nfunction num(v: unknown, fallback = 0): number {\n return typeof v === \"number\" && Number.isFinite(v) ? v : fallback;\n}\n/** Non-negative numeric field (counters, costs, sizes — a negative is sender garbage). */\nfunction nonneg(v: unknown, fallback = 0): number {\n return Math.max(0, num(v, fallback));\n}\n/** Clamp a reported percentage into the displayable 0..100 range. */\nfunction clampPct(v: number): number {\n return Math.min(100, Math.max(0, v));\n}\n/**\n * Make a dynamic string safe to put in a single-line ANSI bar: drop control\n * chars (incl. ESC — no injected sequences/newlines), collapse whitespace, and\n * truncate long values. The bar must render intact whatever a branch name,\n * model name, or worklog filename contains.\n */\nexport function safeSegment(s: string, max = 60): string {\n // eslint-disable-next-line no-control-regex\n const cleaned = s.replace(/[\\u0000-\\u001f\\u007f]/g, \" \").replace(/\\s+/g, \" \").trim();\n return cleaned.length > max ? cleaned.slice(0, max - 1) + \"…\" : cleaned;\n}\nfunction str(v: unknown): string | null {\n return typeof v === \"string\" && v.length > 0 ? v : null;\n}\nfunction obj(v: unknown): Record<string, unknown> {\n return typeof v === \"object\" && v !== null ? (v as Record<string, unknown>) : {};\n}\n\n/** Parse Claude Code's statusLine stdin JSON into the fields the bar uses. Never throws on shape — only on non-JSON. */\nexport function parseStatuslineInput(text: string): StatuslineData {\n const root = obj(JSON.parse(text));\n const model = obj(root.model);\n const effort = obj(root.effort);\n const workspace = obj(root.workspace);\n const cost = obj(root.cost);\n const ctx = obj(root.context_window);\n const usage = obj(ctx.current_usage);\n const limits = obj(root.rate_limits);\n const five = obj(limits.five_hour);\n const seven = obj(limits.seven_day);\n\n const rawName = str(model.display_name) ?? \"Claude\";\n const windowSize = nonneg(ctx.context_window_size, 200_000);\n return {\n modelName: rawName.replace(/^Claude\\s+/, \"\"),\n effortLevel: str(effort.level),\n transcriptPath: str(root.transcript_path),\n dir: str(workspace.project_dir) ?? str(workspace.current_dir) ?? str(root.cwd),\n contextWindowSize: windowSize > 0 ? windowSize : 200_000,\n usedPercentage: clampPct(num(ctx.used_percentage)),\n cacheReadTokens: nonneg(usage.cache_read_input_tokens),\n cacheCreationTokens: nonneg(usage.cache_creation_input_tokens),\n costUsd: nonneg(cost.total_cost_usd),\n durationMs: nonneg(cost.total_duration_ms),\n linesAdded: nonneg(cost.total_lines_added),\n linesRemoved: nonneg(cost.total_lines_removed),\n fiveHourUsedPct: clampPct(num(five.used_percentage)),\n fiveHourResetsAt: nonneg(five.resets_at),\n sevenDayUsedPct: clampPct(num(seven.used_percentage)),\n sevenDayResetsAt: nonneg(seven.resets_at),\n };\n}\n\n/** One-character effort meter: height + color encode the level (`█✦` for ultracode). */\nexport function effortMeter(level: string | null): { meter: string; color: string } | null {\n switch (level) {\n case \"low\":\n return { meter: \"▂\", color: GREY };\n case \"medium\":\n return { meter: \"▄\", color: GREEN };\n case \"high\":\n return { meter: \"▆\", color: YELLOW };\n case \"xhigh\":\n return { meter: \"▇\", color: ORANGE };\n case \"max\":\n return { meter: \"█\", color: RED };\n case \"ultracode\":\n return { meter: \"█✦\", color: MAGENTA };\n default:\n return null;\n }\n}\n\n/** `7` → `7`, `70_000` → `70K`, `1_200_000` → `1.2M` (token quantities). */\nexport function formatTokens(t: number): string {\n if (t >= 1_000_000) return `${Math.floor(t / 1_000_000)}.${Math.floor((t % 1_000_000) / 100_000)}M`;\n if (t >= 1_000) return `${Math.floor(t / 1_000)}K`;\n return String(Math.floor(t));\n}\n\n/** Window sizes render without a decimal: `200_000` → `200K`, `1_000_000` → `1M`. */\nexport function formatWindow(t: number): string {\n if (t >= 1_000_000) return `${Math.floor(t / 1_000_000)}M`;\n if (t >= 1_000) return `${Math.floor(t / 1_000)}K`;\n return String(Math.floor(t));\n}\n\n/** `█`-filled gauge, `width` cells, floor-scaled so 100% and only 100% fills it. */\nexport function makeBar(pct: number, width: number): string {\n const filled = Math.min(width, Math.max(0, Math.floor((pct * width) / 100)));\n return \"█\".repeat(filled) + \"░\".repeat(width - filled);\n}\n\n/** Color for a USAGE percentage (high = bad): <50 green, <80 yellow, else red. */\nfunction usageColor(pct: number): string {\n if (pct >= 80) return RED;\n if (pct >= 50) return YELLOW;\n return GREEN;\n}\n\n/** Color for a REMAINING percentage (low = bad): <=30 red, <=60 yellow, else green. */\nfunction healthColor(remaining: number): string {\n if (remaining <= 30) return RED;\n if (remaining <= 60) return YELLOW;\n return GREEN;\n}\n\nfunction formatDuration(ms: number): string {\n const totalMin = Math.floor(ms / 60_000);\n if (totalMin >= 60) return `${Math.floor(totalMin / 60)}h${totalMin % 60}m`;\n return `${totalMin}m${Math.floor(ms / 1000) % 60}s`;\n}\n\n/** `(2h13m)` until an epoch-seconds reset, or \"\" when absent/past. */\nfunction untilReset(resetsAtSec: number, now: Date): string {\n if (resetsAtSec <= 0) return \"\";\n const diffSec = resetsAtSec - Math.floor(now.getTime() / 1000);\n if (diffSec <= 0) return \"\";\n if (diffSec >= 86_400) return `${Math.floor(diffSec / 86_400)}d${Math.floor((diffSec % 86_400) / 3600)}h`;\n return `${Math.floor(diffSec / 3600)}h${Math.floor((diffSec % 3600) / 60)}m`;\n}\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, \"0\");\n}\n\n/** Compose the status bar. Pure — see `collectStatuslineProbes` for the impure half. */\nexport function renderStatusline(\n d: StatuslineData,\n p: StatuslineProbes,\n mode: \"full\" | \"lite\" = \"full\",\n): string {\n const usedPctDisplay = Math.round(d.usedPercentage);\n const ctxColor = usageColor(usedPctDisplay);\n const ctxUsedTokens = Math.round((d.usedPercentage * d.contextWindowSize) / 100);\n const ctxText = `${WHITE}${formatTokens(ctxUsedTokens)}/${formatWindow(d.contextWindowSize)}${RST}`;\n const clock = `🕐 ${WHITE}${pad2(p.now.getHours())}:${pad2(p.now.getMinutes())}${RST}`;\n\n const level = p.effortLevel ?? d.effortLevel;\n const effort = effortMeter(level);\n const effortSeg = effort\n ? `${effort.color}${effort.meter}${RST} ${GREY}${safeSegment(level ?? \"\")}${RST}`\n : null;\n const modelSeg = `🧠 ${BOLD}${CYAN}${safeSegment(d.modelName, 30)}${RST}`;\n\n const fiveRemain = Math.max(0, 100 - Math.round(d.fiveHourUsedPct));\n const sevenRemain = Math.max(0, 100 - Math.round(d.sevenDayUsedPct));\n const fiveColor = healthColor(fiveRemain);\n const sevenColor = healthColor(sevenRemain);\n const sessionSeg = `${WHITE}⧉ ${Math.max(1, Math.floor(p.sessionCount))}${RST}`;\n\n const project = safeSegment(\n p.vortex || d.dir ? (d.dir ?? \"\").replace(/[\\\\/]+$/, \"\").split(/[\\\\/]/).pop() || \"?\" : \"?\",\n 40,\n );\n const gitBranch = safeSegment(p.gitBranch, 40);\n const gitHash = safeSegment(p.gitHash, 16);\n\n if (mode === \"lite\") {\n const parts = [\n `📁 ${BLUE}${project}${RST}`,\n `${WHITE}⎇${RST} ${YELLOW}${gitBranch}${RST}`,\n effortSeg ? `${modelSeg}${SEP}${effortSeg}` : modelSeg,\n `${ctxColor}${makeBar(usedPctDisplay, 8)} ${usedPctDisplay}%${RST} ${DIM}·${RST} ${ctxText}`,\n `${GREY}5h${RST} ${fiveColor}${fiveRemain}%${RST} ${DIM}·${RST} ${GREY}7d${RST} ${sevenColor}${sevenRemain}%${RST}`,\n sessionSeg,\n ...(p.vortex?.version ? [`🌀 ${MAGENTA}v${safeSegment(p.vortex.version, 20)}${RST}`] : []),\n clock,\n ];\n return parts.join(SEP);\n }\n\n const l1 = [\n effortSeg ? `${modelSeg}${SEP}${effortSeg}` : modelSeg,\n `${ctxColor}${makeBar(usedPctDisplay, 10)} ${usedPctDisplay}%${RST} ${DIM}·${RST} ${ctxText}`,\n clock,\n `💰 ${BOLD}${YELLOW}$${d.costUsd.toFixed(2)}${RST}`,\n ].join(SEP);\n\n const cacheTotal = d.cacheReadTokens + d.cacheCreationTokens;\n const cachePct = cacheTotal > 0 ? Math.floor((d.cacheReadTokens * 100) / cacheTotal) : 0;\n const fiveReset = untilReset(d.fiveHourResetsAt, p.now);\n const sevenReset = untilReset(d.sevenDayResetsAt, p.now);\n const l2 = [\n `${GREY}5h${RST} ${fiveColor}${makeBar(fiveRemain, 8)} ${fiveRemain}%${RST}` +\n (fiveReset ? `${GREY}(${fiveReset})${RST}` : \"\"),\n `${GREY}7d${RST} ${sevenColor}${makeBar(sevenRemain, 8)} ${sevenRemain}%${RST}` +\n (sevenReset ? `${GREY}(${sevenReset})${RST}` : \"\"),\n `📦 ${GREEN}cache ${cachePct}%${RST}`,\n ].join(SEP);\n\n const l3 = [\n `📁 ${BLUE}${project}${RST}`,\n `${WHITE}⎇${RST} ${YELLOW}${gitBranch}${RST} ${GREY}${gitHash}${RST}`,\n `⏱ ${MAGENTA}${formatDuration(d.durationMs)}${RST}`,\n `${GREEN}+${d.linesAdded}${RST} ${RED}-${d.linesRemoved}${RST}`,\n sessionSeg,\n ].join(SEP);\n\n const lines = [l1, l2, l3];\n if (p.vortex) {\n let vx = `🌀 ${BOLD}${MAGENTA}VortEX${RST}`;\n if (p.vortex.version) vx += ` ${GREY}v${safeSegment(p.vortex.version, 20)}${RST}`;\n if (p.vortex.lastWorklog) vx += `${SEP}${GREY}last:${RST} ${safeSegment(p.vortex.lastWorklog, 70)}`;\n lines.push(vx);\n }\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Impure probes — every lookup is individually guarded; a failure falls back.\n// ---------------------------------------------------------------------------\n\nfunction gitOut(dir: string, args: readonly string[]): string {\n return execFileSync(\"git\", [\"-C\", dir, ...args], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n}\n\n/**\n * Claude Code reports `ultracode` as `xhigh`; the transcript keeps the last\n * `/effort` change notice. Read the transcript tail and return the level named\n * by the LAST such notice, or null. Best-effort by design — a format change\n * simply degrades the display to `xhigh`.\n */\nexport function sniffEffortFromTranscript(transcriptPath: string, maxBytes = 4_000_000): string | null {\n try {\n const size = statSync(transcriptPath).size;\n const start = Math.max(0, size - maxBytes);\n const length = size - start;\n if (length <= 0) return null;\n const buf = Buffer.alloc(length);\n const fd = openSync(transcriptPath, \"r\");\n try {\n readSync(fd, buf, 0, length, start);\n } finally {\n closeSync(fd);\n }\n const text = buf.toString(\"utf8\");\n const matches = text.match(/Set effort level to [a-z]+/g);\n if (!matches || matches.length === 0) return null;\n return matches[matches.length - 1]!.slice(\"Set effort level to \".length);\n } catch {\n return null;\n }\n}\n\n/** Count open Claude Code sessions (one `claude` process per session). Falls back to 1. */\nfunction countClaudeSessions(): number {\n try {\n if (process.platform === \"win32\") {\n const out = execFileSync(\"tasklist\", [], { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] });\n const n = (out.match(/claude\\.exe/gi) ?? []).length;\n return n > 0 ? n : 1;\n }\n const out = execFileSync(\"pgrep\", [\"-x\", \"claude\"], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n });\n const n = out.split(/\\r?\\n/).filter(Boolean).length;\n return n > 0 ? n : 1;\n } catch {\n return 1;\n }\n}\n\n/** Newest worklog file name under `data/worklog` — by name, which sorts chronologically. */\nfunction newestWorklogName(repoRoot: string): string | null {\n try {\n const dir = join(repoRoot, \"data\", \"worklog\");\n const entries = readdirSync(dir, { recursive: true }) as string[];\n let best: string | null = null;\n for (const rel of entries) {\n const s = String(rel);\n if (!s.endsWith(\".md\") || s.endsWith(\"_INDEX.md\")) continue;\n if (best === null || s > best) best = s;\n }\n return best ? best.replace(/\\\\/g, \"/\").split(\"/\").pop()! : null;\n } catch {\n return null;\n }\n}\n\n/** Collect every environment lookup the renderer needs for this input. */\nexport function collectStatuslineProbes(d: StatuslineData, now = new Date()): StatuslineProbes {\n const dir = d.dir ?? process.cwd();\n\n let gitBranch = \"-\";\n let gitHash = \"-\";\n try {\n const branch = gitOut(dir, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n gitBranch = branch === \"HEAD\" ? \"detached\" : branch || \"-\";\n gitHash = gitOut(dir, [\"rev-parse\", \"--short\", \"HEAD\"]) || \"-\";\n } catch {\n // not a git repo / git absent — keep \"-\"\n }\n\n let vortex: StatuslineProbes[\"vortex\"] = null;\n try {\n if (existsSync(join(dir, \".agent\", \"vortex.json\"))) {\n let version: string | null = null;\n try {\n const pkg = JSON.parse(\n readFileSync(join(dir, \"node_modules\", \"@vortex-os\", \"base\", \"package.json\"), \"utf8\"),\n ) as { version?: string };\n version = typeof pkg.version === \"string\" ? pkg.version : null;\n } catch {\n // base not installed locally — show the VortEX line without a version\n }\n vortex = { version, lastWorklog: newestWorklogName(dir) };\n }\n } catch {\n vortex = null;\n }\n\n let effortLevel: string | null = null;\n if (d.effortLevel === \"xhigh\" && d.transcriptPath) {\n const sniffed = sniffEffortFromTranscript(d.transcriptPath);\n if (sniffed === \"ultracode\") effortLevel = \"ultracode\";\n }\n\n return {\n gitBranch,\n gitHash,\n sessionCount: countClaudeSessions(),\n vortex,\n effortLevel,\n now,\n };\n}\n\n// ---------------------------------------------------------------------------\n// `vortex statusline install [--lite]` — merge-safe settings wiring.\n// ---------------------------------------------------------------------------\n\n/**\n * The statusLine command written by `install`.\n *\n * The host runs the statusLine command in the session's CURRENT working\n * directory — which MOVES as the session `cd`s around (measured: the hook cwd\n * followed the shell into a sibling repo). Anything cwd-relative (bare `npx`\n * lookup, `./node_modules/...`) therefore silently resolves nothing the moment\n * the session leaves the project, and the bar just vanishes. So:\n * - with a local install (`directRoot`): invoke the bin by ABSOLUTE path —\n * immune to cwd, and ~0.5s faster per refresh than npx (the other observed\n * disappearing-bar mode: npx latency under load) — with the npx lookup\n * chained as a fallback for when the repo has moved;\n * - global-only installs: the npx form alone (cwd-sensitive, but there is no\n * local path to pin).\n * Always self-silencing (`|| exit 0`).\n */\nexport function statuslineCommand(lite: boolean, directRoot?: string): string {\n const sub = `statusline${lite ? \" lite\" : \"\"}`;\n const npxForm = `npx --no-install vortex ${sub}`;\n if (directRoot) {\n const bin = `${directRoot.replace(/\\\\/g, \"/\").replace(/\\/+$/, \"\")}/node_modules/@vortex-os/base/bin/vortex.mjs`;\n return `node \"${bin}\" ${sub} || ${npxForm} || exit 0`;\n }\n return `${npxForm} || exit 0`;\n}\n\n/** Relative path of the locally-installed bin, probed by `install` to pick the direct form. */\nexport const LOCAL_BIN_RELPATH = [\"node_modules\", \"@vortex-os\", \"base\", \"bin\", \"vortex.mjs\"] as const;\n\n/**\n * Is this `statusLine.command` one of OURS (any machine's absolute direct form,\n * with or without the npx fallback, full or lite — or the plain npx form)?\n * Used so `install` updates our own bar in place but never clobbers a foreign one.\n */\nexport function isOurStatuslineCommand(cmd: string | undefined): boolean {\n if (typeof cmd !== \"string\") return false;\n return /^(?:node \".*\\/node_modules\\/@vortex-os\\/base\\/bin\\/vortex\\.mjs\" statusline(?: lite)? \\|\\| )?npx --no-install vortex statusline(?: lite)? \\|\\| exit 0$/.test(\n cmd,\n );\n}\n\nexport interface StatuslineInstallResult {\n readonly status: \"installed\" | \"already-ours\" | \"kept-existing\";\n readonly settingsPath: string;\n readonly command?: string;\n /** Present when status is \"kept-existing\": the command we refused to overwrite. */\n readonly existing?: string;\n}\n\n/**\n * Merge `statusLine` into a settings object. NON-DESTRUCTIVE: an existing\n * statusLine that is not ours is kept (the user owns their bar) unless `force`.\n * Switching full↔lite of our own command counts as ours and is updated.\n */\nexport function ensureStatusline(\n existing: ClaudeSettings,\n lite: boolean,\n force = false,\n directRoot?: string,\n): { settings: ClaudeSettings; status: StatuslineInstallResult[\"status\"]; existing?: string } {\n const command = statuslineCommand(lite, directRoot);\n const current = existing.statusLine as { type?: string; command?: string } | undefined;\n const currentCmd = typeof current?.command === \"string\" ? current.command : undefined;\n // Ours = any of our shapes (full/lite × npx/any-machine-direct) — switching\n // between them (incl. the npx→direct upgrade) is an update, never a \"foreign bar\".\n const isOurs = isOurStatuslineCommand(currentCmd);\n // `already-ours` requires the whole entry to be well-formed — a matching\n // command under a wrong/missing `type` is rewritten (repaired) below.\n if (currentCmd === command && current?.type === \"command\") {\n return { settings: existing, status: \"already-ours\" };\n }\n if (current && !isOurs && !force) {\n return { settings: existing, status: \"kept-existing\", existing: currentCmd ?? JSON.stringify(current) };\n }\n return {\n settings: { ...existing, statusLine: { type: \"command\", command } },\n status: \"installed\",\n };\n}\n\n// ---------------------------------------------------------------------------\n// CLI entry — `vortex statusline [lite] [install [--lite] [--force]]`\n// ---------------------------------------------------------------------------\n\n/**\n * Run the statusline CLI. `install` wires `.claude/settings.json` (at\n * `repoRoot`); anything else renders the bar from stdin JSON. A render must\n * never break the bar: bad/absent input prints nothing and exits 0.\n */\nexport async function runStatuslineCli(\n argv: readonly string[],\n repoRoot: string,\n out: (s: string) => void,\n err: (s: string) => void,\n): Promise<number> {\n if (argv[0] === \"install\") {\n const lite = argv.includes(\"--lite\");\n const force = argv.includes(\"--force\");\n // Prefer the absolute direct-node form when the package is installed\n // locally — immune to the host's cwd-following statusline execution and\n // ~0.5s faster per refresh than npx. Global-only installs keep npx.\n const directRoot = existsSync(join(repoRoot, ...LOCAL_BIN_RELPATH)) ? repoRoot : undefined;\n const settingsPath = join(repoRoot, \".claude\", \"settings.json\");\n const existingText = existsSync(settingsPath) ? readFileSync(settingsPath, \"utf8\") : null;\n const parsed = parseSettings(existingText);\n const result = ensureStatusline(parsed, lite, force, directRoot);\n if (result.status === \"installed\") {\n const { mkdirSync, writeFileSync } = await import(\"node:fs\");\n mkdirSync(join(repoRoot, \".claude\"), { recursive: true });\n writeFileSync(settingsPath, serializeSettings(result.settings), \"utf8\");\n }\n const payload: StatuslineInstallResult = {\n status: result.status,\n settingsPath,\n ...(result.status !== \"kept-existing\" ? { command: statuslineCommand(lite, directRoot) } : {}),\n ...(result.existing !== undefined ? { existing: result.existing } : {}),\n };\n out(JSON.stringify(payload, null, 2) + \"\\n\");\n if (result.status === \"kept-existing\") {\n err(\n \"[vortex] statusLine already configured with a different command — kept it. Re-run with --force to replace.\\n\",\n );\n }\n return 0;\n }\n\n const mode: \"full\" | \"lite\" = argv[0] === \"lite\" ? \"lite\" : \"full\";\n if (process.stdin.isTTY) {\n // Typed by hand with no piped JSON — don't block waiting on stdin.\n err(\n \"[vortex] statusline renders Claude Code's statusLine stdin JSON — pipe it in, \" +\n \"or wire it up with `vortex statusline install [--lite]`.\\n\",\n );\n return 0;\n }\n let raw = \"\";\n try {\n raw = readFileSync(0, \"utf8\");\n } catch {\n raw = \"\";\n }\n if (!raw.trim()) return 0; // no input — print nothing, never an error in the bar\n let data: StatuslineData;\n try {\n data = parseStatuslineInput(raw);\n } catch {\n return 0; // malformed input — keep the bar blank rather than erroring\n }\n out(renderStatusline(data, collectStatuslineProbes(data), mode));\n return 0;\n}\n","/**\r\n * Hook wiring for `/vortex init`: make sure the instance's\r\n * `.claude/settings.json` registers the VortEX SessionStart / SessionEnd hooks,\r\n * so the boot report + worklog-net fire automatically without the user knowing\r\n * any command. NON-DESTRUCTIVE — like the MCP install merge, this preserves\r\n * every other hook and top-level field and only adds our two entries if absent.\r\n *\r\n * Pure functions here (parse / merge / detect); the command does the actual\r\n * file read/write around them. Keeping them pure makes the merge unit-testable\r\n * and the \"writes only what's missing\" guarantee verifiable.\r\n */\r\n\r\n// Hook commands invoke the published CLI via `npx`, NOT checkout-relative\r\n// `node plugins/...` paths. An npm-installed instance (`npm i @vortex-os/base`)\r\n// has no monorepo checkout — but it does have the `vortex` bin on disk (the\r\n// instance's local `node_modules/.bin`, or the global npm bin on PATH), so\r\n// `npx --no-install vortex session-{start,end}` runs it. Two deliberate choices:\r\n// • `--no-install` — a SessionStart/End hook fires automatically; bare\r\n// `npx vortex` would silently install an arbitrary `vortex` package from the\r\n// network on a cache miss. `--no-install` fails closed instead.\r\n// • Resolve by BIN NAME (no `-p @vortex-os/base`) — npx searches the current\r\n// folder's `node_modules/.bin` FIRST, then PATH, which includes a global\r\n// `npm i -g @vortex-os/base`. This is what makes `vortex global-setup` fire in\r\n// EVERY folder: a `-p <pkg>` form (this command's earlier shape) resolves only\r\n// a LOCAL install and ignores the global one — so the GLOBAL hook silently\r\n// no-opped everywhere except the instance folder, defeating global-setup's one\r\n// job. Resolving the bin instead keeps \"local install wins\" (so the instance's\r\n// pinned version still runs there) while letting the global bin cover all other\r\n// folders. The cost: a same-named `vortex` bin earlier on PATH/local could\r\n// shadow ours — but `--no-install` still blocks the network-install case and a\r\n// colliding bin is unlikely, an acceptable trade for the any-folder guarantee.\r\n// These map to the `session-start` / `session-end` subcommands of the CLI.\r\n//\r\n// The trailing `|| exit 0` makes the hook SELF-SILENCING: the global hook fires in\r\n// every folder, but `npx --no-install` still fails where NO `vortex` is resolvable\r\n// (no local install AND nothing on PATH) — and Claude Code surfaces a non-zero\r\n// SessionStart hook as a \"hook error\" to the user. `|| exit 0` swallows that\r\n// (exit 0 → no error notice; stderr not injected) so such folders stay quiet.\r\n// `||` + `exit 0` are valid in both cmd.exe (the default Windows child-process\r\n// shell) and POSIX sh. Keeping the per-instance and global commands BYTE-IDENTICAL\r\n// is what lets Claude Code dedup them (no double session-start in the instance).\r\nexport const SESSION_START_COMMAND =\r\n \"npx --no-install vortex session-start || exit 0\";\r\nexport const SESSION_END_COMMAND =\r\n \"npx --no-install vortex session-end || exit 0\";\r\n\r\n// PreToolUse guard wired by `vortex init` (NOT global-setup — see\r\n// `ensureVortexHooks`): denies literal control bytes in to-be-written text.\r\n// Same conventions as the session hooks: bare bin name, `--no-install`\r\n// fail-closed, `|| exit 0` self-silencing (a deny is JSON on stdout, exit 0,\r\n// so the wrapper never masks it). Kept in sync with guard.ts.\r\nexport { GUARD_WRITE_COMMAND, GUARD_WRITE_MATCHER } from \"./guard.js\";\r\nimport { GUARD_WRITE_COMMAND, GUARD_WRITE_MATCHER } from \"./guard.js\";\r\n\r\n// Older command shapes a prior `init`/`global-setup` may have written. On the next\r\n// `ensureVortexHooks` run each migrates IN PLACE to the current command, so the\r\n// per-instance hook keeps matching the global one (and stays dedup-able). Covers\r\n// the `-p @vortex-os/base` form (self-silencing and its pre-`|| exit 0` variant)\r\n// that this command carried before resolution moved to the bare bin name.\r\nconst LEGACY_COMMANDS: Record<\"SessionStart\" | \"SessionEnd\", readonly string[]> = {\r\n SessionStart: [\r\n \"npx --no-install -p @vortex-os/base vortex session-start || exit 0\",\r\n \"npx --no-install -p @vortex-os/base vortex session-start\",\r\n ],\r\n SessionEnd: [\r\n \"npx --no-install -p @vortex-os/base vortex session-end || exit 0\",\r\n \"npx --no-install -p @vortex-os/base vortex session-end\",\r\n ],\r\n};\r\n\r\ninterface HookCommand {\r\n readonly type: \"command\";\r\n readonly command: string;\r\n}\r\ninterface HookGroup {\r\n readonly hooks: readonly HookCommand[];\r\n readonly matcher?: string;\r\n}\r\nexport interface ClaudeSettings {\r\n hooks?: {\r\n SessionStart?: HookGroup[];\r\n SessionEnd?: HookGroup[];\r\n [event: string]: HookGroup[] | undefined;\r\n };\r\n [key: string]: unknown;\r\n}\r\n\r\nexport interface EnsureHooksResult {\r\n readonly settings: ClaudeSettings;\r\n /**\r\n * Hook events that CHANGED — a VortEX entry was added, or a legacy one was\r\n * migrated/de-duplicated in place (empty when nothing changed). Callers should\r\n * key \"did we need to write?\" off `alreadyWired`, not the name \"added\".\r\n */\r\n readonly added: readonly string[];\r\n /** True when nothing changed — every VortEX hook was already present. */\r\n readonly alreadyWired: boolean;\r\n}\r\n\r\n/**\r\n * Parse existing settings.json text. Empty/whitespace → `{}` (fresh). Throws on\r\n * malformed JSON so the caller aborts rather than clobbering a hand-edited file.\r\n */\r\nexport function parseSettings(text: string | null | undefined): ClaudeSettings {\r\n const trimmed = (text ?? \"\").trim();\r\n if (trimmed.length === 0) return {};\r\n let parsed: unknown;\r\n try {\r\n parsed = JSON.parse(trimmed);\r\n } catch (e) {\r\n throw new Error(\r\n `.claude/settings.json is not valid JSON — refusing to overwrite. Fix or remove it first. (${(e as Error).message})`,\r\n );\r\n }\r\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\r\n throw new Error(\".claude/settings.json is not a JSON object — refusing to overwrite.\");\r\n }\r\n return parsed as ClaudeSettings;\r\n}\r\n\r\n/**\r\n * Merge the VortEX hooks into an existing settings object WITHOUT mutating the\r\n * input. A hook event is left untouched if it already references our command\r\n * (idempotent); otherwise our group is appended alongside any existing groups.\r\n *\r\n * `opts.guard` additionally wires the PreToolUse control-byte guard\r\n * (`vortex guard write`) for the file-writing tools. It is requested by\r\n * `vortex init` (instance settings) but NOT by `vortex global-setup`: a\r\n * machine-global deny hook would fire in every project on the machine, which\r\n * is a stronger default than \"the instance opted into VortEX conventions\".\r\n */\r\nexport function ensureVortexHooks(\r\n existing: ClaudeSettings | null | undefined,\r\n opts?: { readonly guard?: boolean },\r\n): EnsureHooksResult {\r\n const base: ClaudeSettings = existing && typeof existing === \"object\" ? existing : {};\r\n const hooks = { ...(base.hooks ?? {}) };\r\n const added: string[] = [];\r\n\r\n const wire = (event: \"SessionStart\" | \"SessionEnd\", command: string) => {\r\n const legacy = LEGACY_COMMANDS[event];\r\n const src = hooks[event] ?? [];\r\n let changed = false;\r\n let kept = false; // already retained one entry with the current command?\r\n const groups: HookGroup[] = [];\r\n for (const g of src) {\r\n const hookList: HookCommand[] = [];\r\n for (const h of g.hooks ?? []) {\r\n // Migrate a LEGACY VortEX command to the current one (in place, so the\r\n // group's other hooks and its `matcher` are preserved).\r\n const migrated = legacy.includes(h.command);\r\n const cmd = migrated ? command : h.command;\r\n if (cmd === command) {\r\n if (kept) {\r\n changed = true; // drop a duplicate VortEX hook (e.g. legacy + current)\r\n continue;\r\n }\r\n kept = true;\r\n if (migrated) changed = true;\r\n hookList.push(migrated ? { ...h, command } : h);\r\n } else {\r\n hookList.push(h);\r\n }\r\n }\r\n if (hookList.length > 0) groups.push({ ...g, hooks: hookList });\r\n else changed = true; // group held only legacy/duplicate VortEX hooks → drop it\r\n }\r\n if (!kept) {\r\n groups.push({ hooks: [{ type: \"command\", command }] });\r\n changed = true;\r\n }\r\n hooks[event] = groups;\r\n if (changed) added.push(event);\r\n };\r\n\r\n wire(\"SessionStart\", SESSION_START_COMMAND);\r\n wire(\"SessionEnd\", SESSION_END_COMMAND);\r\n\r\n // PreToolUse control-byte guard (matcher-scoped to the file-writing tools).\r\n // Idempotency is by COMMAND: if any PreToolUse group already carries our\r\n // command — even under a user-customized matcher — it counts as wired and is\r\n // left exactly as the user shaped it.\r\n if (opts?.guard) {\r\n const src = hooks.PreToolUse ?? [];\r\n const wired = src.some((g) => (g.hooks ?? []).some((h) => h.command === GUARD_WRITE_COMMAND));\r\n if (!wired) {\r\n hooks.PreToolUse = [\r\n ...src,\r\n { matcher: GUARD_WRITE_MATCHER, hooks: [{ type: \"command\", command: GUARD_WRITE_COMMAND }] },\r\n ];\r\n added.push(\"PreToolUse\");\r\n }\r\n }\r\n\r\n const settings: ClaudeSettings = { ...base, hooks };\r\n return { settings, added, alreadyWired: added.length === 0 };\r\n}\r\n\r\n/** Serialize settings the way Claude writes them (2-space, trailing newline). */\r\nexport function serializeSettings(settings: ClaudeSettings): string {\r\n return JSON.stringify(settings, null, 2) + \"\\n\";\r\n}\r\n"],"mappings":"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","names":["out","n"]}
package/dist/index.d.ts CHANGED
@@ -3481,7 +3481,12 @@ interface StatuslineData {
3481
3481
  /** Reasoning effort level as reported (`effort.level`), or null. */
3482
3482
  readonly effortLevel: string | null;
3483
3483
  readonly transcriptPath: string | null;
3484
- /** Project directory (workspace.current_dir, falling back to cwd). */
3484
+ /**
3485
+ * Project ROOT directory. `workspace.project_dir` first — `current_dir` and
3486
+ * `cwd` FOLLOW the session shell as it `cd`s (measured), which made the bar
3487
+ * show a sibling repo and lose the VortEX line mid-session. The bar's
3488
+ * identity (project name, git, instance line) stays pinned to the project.
3489
+ */
3485
3490
  readonly dir: string | null;
3486
3491
  readonly contextWindowSize: number;
3487
3492
  /** Context used, percent — kept as reported (may be fractional). */
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  serializeSettings,
28
28
  sniffEffortFromTranscript,
29
29
  statuslineCommand
30
- } from "./chunk-EAKDR5B2.js";
30
+ } from "./chunk-P7IMUUNY.js";
31
31
  import {
32
32
  GUARD_WRITE_COMMAND,
33
33
  GUARD_WRITE_MATCHER,
@@ -6246,10 +6246,36 @@ async function runUpdate(input, tokens) {
6246
6246
  const dryRun = tokens.includes("--dry-run");
6247
6247
  const adopt = parseAdoptArgs(tokens);
6248
6248
  const templatesDir = resolveTemplatesDir();
6249
- const result = await runTemplatesUpdate(input.context, templatesDir, {
6249
+ let result = await runTemplatesUpdate(input.context, templatesDir, {
6250
6250
  dryRun,
6251
6251
  adopt: adopt.size > 0 ? adopt : void 0
6252
6252
  });
6253
+ if (!dryRun) {
6254
+ try {
6255
+ const settingsPath = join24(input.context.repoRoot, ".claude", "settings.json");
6256
+ const existingText = existsSync11(settingsPath) ? await readFile21(settingsPath, "utf8") : null;
6257
+ const { settings, added, alreadyWired } = ensureVortexHooks(parseSettings(existingText), { guard: true });
6258
+ if (!alreadyWired) {
6259
+ await mkdir9(join24(input.context.repoRoot, ".claude"), { recursive: true });
6260
+ await writeFile10(settingsPath, serializeSettings(settings), "utf8");
6261
+ result = {
6262
+ ...result,
6263
+ nextActions: [
6264
+ ...result.nextActions,
6265
+ `Wired missing ${added.join(" + ")} hook(s) into .claude/settings.json` + (added.includes("PreToolUse") ? " \u2014 the write guard now denies literal control bytes in file writes (remove the PreToolUse group to opt out)." : ".")
6266
+ ]
6267
+ };
6268
+ }
6269
+ } catch (e) {
6270
+ result = {
6271
+ ...result,
6272
+ nextActions: [
6273
+ ...result.nextActions,
6274
+ `\u26A0\uFE0F Could not verify/wire session hooks: ${e.message}`
6275
+ ]
6276
+ };
6277
+ }
6278
+ }
6253
6279
  if (dryRun || result.status === "no-manifest" || result.status === "no-templates")
6254
6280
  return result;
6255
6281
  if (!loadVortexConfig(input.context).autoRecord.commitFrameworkChanges)
@@ -8520,7 +8546,7 @@ async function runVortexCli(argv, io) {
8520
8546
  const err = io?.stderr ?? ((s) => process.stderr.write(s));
8521
8547
  try {
8522
8548
  if (argv[0] === "statusline") {
8523
- const { runStatuslineCli: runStatuslineCli2 } = await import("./statusline-6KSHISXO.js");
8549
+ const { runStatuslineCli: runStatuslineCli2 } = await import("./statusline-JTSL5CCH.js");
8524
8550
  const statuslineRoot = process.env.VORTEX_REPO_ROOT?.trim() || process.cwd();
8525
8551
  return runStatuslineCli2(argv.slice(1), statuslineRoot, out, err);
8526
8552
  }