@vortex-os/base 0.11.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.
- package/README.md +29 -1
- package/dist/chunk-2FVNWW77.js +166 -0
- package/dist/chunk-2FVNWW77.js.map +1 -0
- package/dist/{chunk-DWANI3LV.js → chunk-P7IMUUNY.js} +41 -10
- package/dist/chunk-P7IMUUNY.js.map +1 -0
- package/dist/chunk-T53UWSTR.js +301 -0
- package/dist/chunk-T53UWSTR.js.map +1 -0
- package/dist/chunk-UV76ZEDC.js +292 -0
- package/dist/chunk-UV76ZEDC.js.map +1 -0
- package/dist/failures-PMURLMVB.js +25 -0
- package/dist/guard-IMJR6ET7.js +23 -0
- package/dist/guard-IMJR6ET7.js.map +1 -0
- package/dist/index.d.ts +295 -7
- package/dist/index.js +459 -629
- package/dist/index.js.map +1 -1
- package/dist/{statusline-NQKJ3NWD.js → statusline-JTSL5CCH.js} +8 -2
- package/dist/statusline-JTSL5CCH.js.map +1 -0
- package/package.json +1 -1
- package/templates/manifest.json +2 -2
- package/templates/routers/AI-RULES.md +3 -1
- package/dist/chunk-DWANI3LV.js.map +0 -1
- /package/dist/{statusline-NQKJ3NWD.js.map → failures-PMURLMVB.js.map} +0 -0
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ npx vortex init # scaffold the instance
|
|
|
40
40
|
|
|
41
41
|
- the per-agent files — `AGENTS.md` (the thin Codex-CLI entry, auto-loaded by Codex and other `AGENTS.md`-aware tools) plus thin routers `CLAUDE.md`, `GEMINI.md`, `.cursorrules`, all pointing at `AI-RULES.md` (the single source of truth for shared rules) — so any agent host finds VortEX's behavior contract (these are generic templates you personalize over time);
|
|
42
42
|
- the `data/` skeleton (`_memory/`, `worklog/`, `decision-log/`, `runbooks/`, `hubs/`, `inbox/`), your user-profile memory, and today's first worklog;
|
|
43
|
-
- `.claude/settings.json` with the session hooks wired as `npx --no-install vortex session-start` / `… session-end` (the `--no-install` flag makes the auto-firing hook fail closed rather than install on a cache miss; resolving the bare `vortex` bin — local `node_modules/.bin` first, then PATH — lets the `global-setup` hook fire from any folder, not only where a local install exists), plus the agent-mediated slash-commands in `.claude/commands/`;
|
|
43
|
+
- `.claude/settings.json` with the session hooks wired as `npx --no-install vortex session-start` / `… session-end` (the `--no-install` flag makes the auto-firing hook fail closed rather than install on a cache miss; resolving the bare `vortex` bin — local `node_modules/.bin` first, then PATH — lets the `global-setup` hook fire from any folder, not only where a local install exists), the write-guard hook (`vortex guard write` — denies literal control bytes in file writes; remove its `PreToolUse` group to opt out), plus the agent-mediated slash-commands in `.claude/commands/`;
|
|
44
44
|
- `.agent/vortex.json` (auto-record config) and a minimal `package.json` with `"type":"module"` if none exists.
|
|
45
45
|
|
|
46
46
|
`vortex import --from <folder>` brings an existing notes folder in — markdown is auto-classified (worklog / decision-log / …) and **attachments (PDFs, images, …) are copied byte-for-byte** into the same layout; credential files (`*.key`, `.env`, `secrets/` …) and oversized files are skipped and reported. As the framework improves, `vortex update` refreshes the installed templates **without ever overwriting a file you edited** (your edit is parked at `<file>.new`); `vortex doctor` checks instance health.
|
|
@@ -62,8 +62,36 @@ npx vortex session-end # no-op (kept for hook compatibility; gap handling i
|
|
|
62
62
|
npx vortex doctor # health diagnosis
|
|
63
63
|
npx vortex update # refresh framework templates (never clobbers your edits)
|
|
64
64
|
npx vortex statusline # Claude Code status-bar renderer (see below)
|
|
65
|
+
npx vortex failure # failure ledger: record / list (the self-improvement loop)
|
|
65
66
|
```
|
|
66
67
|
|
|
68
|
+
## Self-improvement loop (failure ledger)
|
|
69
|
+
|
|
70
|
+
Rules saved in `data/_memory/` are loaded at session start — but at action time
|
|
71
|
+
the model's attention is on the task, and a registered rule can still slip.
|
|
72
|
+
VortEX therefore counts: when the agent's own failure becomes visible (you
|
|
73
|
+
correct it, a test disproves it, a cross-check catches it), the agent records
|
|
74
|
+
it with `vortex failure record --key <root-cause-key> --what "<one line>"`
|
|
75
|
+
into `data/_failures/` (append-only, one file per occurrence). Recurrence is
|
|
76
|
+
machine-counted per key and escalates on a ladder:
|
|
77
|
+
|
|
78
|
+
1. **1st occurrence** — recorded.
|
|
79
|
+
2. **2nd, while a rule already covers it** — that rule's write-time gate
|
|
80
|
+
becomes mandatory.
|
|
81
|
+
3. **3rd+** — the agent must **propose** a deterministic defense (a hook, a
|
|
82
|
+
pre-commit check, a wrapper) — proposals only, nothing self-installs.
|
|
83
|
+
|
|
84
|
+
The session-start report surfaces recurring keys automatically, and promoting
|
|
85
|
+
a lesson into `_memory/` requires evidence links, a replay check ("would this
|
|
86
|
+
rule have prevented the recorded cases?") and your approval. Disable with
|
|
87
|
+
`autoRecord.failures: false` in `.agent/vortex.json`.
|
|
88
|
+
|
|
89
|
+
The first guard ships in the box: `vortex init` wires a Claude Code hook
|
|
90
|
+
(`vortex guard write`) that **denies file writes containing literal control
|
|
91
|
+
bytes** (escape notation always works instead) — a denial also records itself
|
|
92
|
+
into the ledger. Remove the `PreToolUse` group in `.claude/settings.json` to
|
|
93
|
+
opt out.
|
|
94
|
+
|
|
67
95
|
## Claude Code status bar (optional)
|
|
68
96
|
|
|
69
97
|
`vortex statusline` renders a colored status bar for Claude Code's `statusLine` setting — model + a reasoning-effort meter (one bar whose height/color encode `low`→`ultracode`), a context-window gauge with used/total tokens, a clock, session cost, the 5-hour/7-day rate-limit gauges with time-to-reset, cache hit-rate, project + git branch/commit, session duration, lines changed, the number of open Claude Code sessions (best-effort process count) — and, inside a VortEX instance, the framework version and the latest worklog file:
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadVortexConfig,
|
|
3
|
+
makeContext
|
|
4
|
+
} from "./chunk-T53UWSTR.js";
|
|
5
|
+
|
|
6
|
+
// ../plugins/session-rituals/dist/guard.js
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { dirname, join, resolve } from "path";
|
|
9
|
+
var GUARD_WRITE_COMMAND = "npx --no-install vortex guard write || exit 0";
|
|
10
|
+
var GUARD_WRITE_MATCHER = "Write|Edit|MultiEdit|NotebookEdit";
|
|
11
|
+
function isForbidden(codePoint) {
|
|
12
|
+
if (codePoint === 9 || codePoint === 10 || codePoint === 13)
|
|
13
|
+
return false;
|
|
14
|
+
return codePoint < 32 || codePoint === 127;
|
|
15
|
+
}
|
|
16
|
+
function findControlChar(text) {
|
|
17
|
+
for (let i = 0; i < text.length; i++) {
|
|
18
|
+
const cp = text.charCodeAt(i);
|
|
19
|
+
if (isForbidden(cp))
|
|
20
|
+
return { index: i, codePoint: cp };
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
var CONTROL_NAMES = {
|
|
25
|
+
0: "NUL",
|
|
26
|
+
7: "BEL",
|
|
27
|
+
8: "BS",
|
|
28
|
+
11: "VT",
|
|
29
|
+
12: "FF",
|
|
30
|
+
27: "ESC",
|
|
31
|
+
127: "DEL"
|
|
32
|
+
};
|
|
33
|
+
function describeCodePoint(cp) {
|
|
34
|
+
const hex = cp.toString(16).toUpperCase().padStart(4, "0");
|
|
35
|
+
const name = CONTROL_NAMES[cp];
|
|
36
|
+
return `U+${hex}${name ? ` (${name})` : ""}`;
|
|
37
|
+
}
|
|
38
|
+
function scanToolInput(toolInput) {
|
|
39
|
+
const check = (field, value) => {
|
|
40
|
+
if (typeof value !== "string")
|
|
41
|
+
return null;
|
|
42
|
+
const hit = findControlChar(value);
|
|
43
|
+
return hit ? { field, index: hit.index, codePoint: hit.codePoint } : null;
|
|
44
|
+
};
|
|
45
|
+
const direct = check("content", toolInput.content) ?? check("new_string", toolInput.new_string) ?? check("new_source", toolInput.new_source);
|
|
46
|
+
if (direct)
|
|
47
|
+
return direct;
|
|
48
|
+
const edits = toolInput.edits;
|
|
49
|
+
if (Array.isArray(edits)) {
|
|
50
|
+
for (let i = 0; i < edits.length; i++) {
|
|
51
|
+
const e = edits[i];
|
|
52
|
+
if (e && typeof e === "object") {
|
|
53
|
+
const hit = check(`edits[${i}].new_string`, e.new_string);
|
|
54
|
+
if (hit)
|
|
55
|
+
return hit;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function buildDenyDecision(finding) {
|
|
62
|
+
const reason = `VortEX guard: literal control character ${describeCodePoint(finding.codePoint)} at offset ${finding.index} of ${finding.field} \u2014 control bytes are never written literally into text files. Re-issue the write using escape notation for that character (e.g. \\x1b / \\u001b in code or regexes, the two-character sequence backslash+x, not the raw byte). If you are CLEANING an already-polluted file: old_string is not scanned \u2014 keep the raw byte only on the old_string side and put escape notation in the new text.`;
|
|
63
|
+
return JSON.stringify({
|
|
64
|
+
hookSpecificOutput: {
|
|
65
|
+
hookEventName: "PreToolUse",
|
|
66
|
+
permissionDecision: "deny",
|
|
67
|
+
permissionDecisionReason: reason
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
var GUARDED_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
72
|
+
function toolOutOfScope(payload) {
|
|
73
|
+
const name = payload.tool_name;
|
|
74
|
+
return typeof name === "string" && name.length > 0 && !GUARDED_TOOLS.has(name);
|
|
75
|
+
}
|
|
76
|
+
function guardWriteDecision(payloadText) {
|
|
77
|
+
try {
|
|
78
|
+
const payload = JSON.parse(payloadText);
|
|
79
|
+
if (toolOutOfScope(payload))
|
|
80
|
+
return null;
|
|
81
|
+
const toolInput = payload.tool_input;
|
|
82
|
+
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput))
|
|
83
|
+
return null;
|
|
84
|
+
const finding = scanToolInput(toolInput);
|
|
85
|
+
return finding ? buildDenyDecision(finding) : null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function runGuardCli(argv, out, err) {
|
|
91
|
+
if (argv[0] !== "write") {
|
|
92
|
+
err("[vortex] guard subcommands: write \u2014 PreToolUse hook body, reads the payload JSON from stdin.\n");
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
if (process.stdin.isTTY) {
|
|
96
|
+
err("[vortex] guard write reads a Claude Code PreToolUse payload from stdin \u2014 it is wired by `vortex init` as a hook, not run by hand.\n");
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
let raw = "";
|
|
100
|
+
try {
|
|
101
|
+
raw = readFileSync(0, "utf8");
|
|
102
|
+
} catch {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
let payload;
|
|
106
|
+
try {
|
|
107
|
+
payload = JSON.parse(raw);
|
|
108
|
+
} catch {
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
if (toolOutOfScope(payload))
|
|
112
|
+
return 0;
|
|
113
|
+
const toolInput = payload.tool_input;
|
|
114
|
+
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput))
|
|
115
|
+
return 0;
|
|
116
|
+
const finding = scanToolInput(toolInput);
|
|
117
|
+
if (!finding)
|
|
118
|
+
return 0;
|
|
119
|
+
out(buildDenyDecision(finding) + "\n");
|
|
120
|
+
try {
|
|
121
|
+
const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
|
|
122
|
+
const root = resolveInstanceRoot(cwd);
|
|
123
|
+
if (root) {
|
|
124
|
+
const ctx = makeContext(root);
|
|
125
|
+
if (loadVortexConfig(ctx).autoRecord.failures) {
|
|
126
|
+
const filePath = toolInput.file_path;
|
|
127
|
+
const fileNote = typeof filePath === "string" && filePath ? ` (target: ${filePath})` : "";
|
|
128
|
+
const { recordGuardDenial } = await import("./failures-PMURLMVB.js");
|
|
129
|
+
await recordGuardDenial(ctx.dataDir, `write guard denied a literal control character in ${finding.field}${fileNote}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
function resolveInstanceRoot(startDir) {
|
|
137
|
+
const isInstance = (d) => existsSync(join(d, ".agent", "vortex.json"));
|
|
138
|
+
const envRoot = process.env.VORTEX_REPO_ROOT?.trim();
|
|
139
|
+
if (envRoot && isInstance(envRoot))
|
|
140
|
+
return envRoot;
|
|
141
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR?.trim();
|
|
142
|
+
if (projectDir && isInstance(projectDir))
|
|
143
|
+
return projectDir;
|
|
144
|
+
let dir = resolve(startDir);
|
|
145
|
+
for (let i = 0; i < 16; i++) {
|
|
146
|
+
if (isInstance(dir))
|
|
147
|
+
return dir;
|
|
148
|
+
const parent = dirname(dir);
|
|
149
|
+
if (parent === dir)
|
|
150
|
+
return null;
|
|
151
|
+
dir = parent;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export {
|
|
157
|
+
GUARD_WRITE_COMMAND,
|
|
158
|
+
GUARD_WRITE_MATCHER,
|
|
159
|
+
findControlChar,
|
|
160
|
+
scanToolInput,
|
|
161
|
+
buildDenyDecision,
|
|
162
|
+
guardWriteDecision,
|
|
163
|
+
runGuardCli,
|
|
164
|
+
resolveInstanceRoot
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=chunk-2FVNWW77.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../plugins/session-rituals/src/guard.ts"],"sourcesContent":["/**\n * `vortex guard` — deterministic write-time guards, wired as Claude Code hooks.\n *\n * `guard write` is a PreToolUse hook for the file-writing tools\n * (Write / Edit / MultiEdit / NotebookEdit). It reads the hook payload JSON\n * from stdin and DENIES the call when the NEW text contains a literal control\n * character (anything below U+0020 except tab/LF/CR, or U+007F) — the\n * documented repeat-failure class where escape notation was intended but the\n * raw byte got written into source files.\n *\n * Why a hook and not (another) rule: the failure happens precisely when the\n * model's active context is saturated with the offending bytes, i.e. when\n * memory-based discipline is weakest. A deterministic check at the write\n * boundary does not depend on recall. This is the \"3rd occurrence → promote to\n * a deterministic guard\" rung of the failure-ledger ladder, shipped as a\n * framework default because the class has zero legitimate uses in text writes\n * (escape notation is always available).\n *\n * Contract (mirrors the statusline renderer's \"never break the host\"):\n * - only the NEW text is scanned (`content`, `new_string`, `edits[].new_string`,\n * `new_source`) — `old_string` is deliberately NOT scanned, so cleaning an\n * already-polluted file stays possible;\n * - a clean payload produces NO output (exit 0) — the normal permission flow\n * proceeds untouched;\n * - a malformed payload, an unexpected tool, or any internal error is\n * FAIL-OPEN: exit 0, no output, never a blocked write from a guard bug.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { loadVortexConfig, makeContext } from \"@vortex-os/core\";\n\n/** The PreToolUse hook command `vortex init` wires (self-silencing like the session hooks). */\nexport const GUARD_WRITE_COMMAND = \"npx --no-install vortex guard write || exit 0\";\n\n/** Tool-name matcher for the guard's hook group. */\nexport const GUARD_WRITE_MATCHER = \"Write|Edit|MultiEdit|NotebookEdit\";\n\n/** A literal control character found in to-be-written text. */\nexport interface GuardFinding {\n /** Which payload field carried it (e.g. `content`, `edits[2].new_string`). */\n readonly field: string;\n /** Character offset within that field. */\n readonly index: number;\n readonly codePoint: number;\n}\n\n/** Control characters that are NEVER legitimate in tool-written text (tab/LF/CR excluded). */\nfunction isForbidden(codePoint: number): boolean {\n if (codePoint === 9 || codePoint === 10 || codePoint === 13) return false;\n return codePoint < 32 || codePoint === 127;\n}\n\n/** First forbidden control character in `text`, or null. */\nexport function findControlChar(text: string): { index: number; codePoint: number } | null {\n for (let i = 0; i < text.length; i++) {\n const cp = text.charCodeAt(i);\n if (isForbidden(cp)) return { index: i, codePoint: cp };\n }\n return null;\n}\n\nconst CONTROL_NAMES: Record<number, string> = {\n 0: \"NUL\",\n 7: \"BEL\",\n 8: \"BS\",\n 11: \"VT\",\n 12: \"FF\",\n 27: \"ESC\",\n 127: \"DEL\",\n};\n\nfunction describeCodePoint(cp: number): string {\n const hex = cp.toString(16).toUpperCase().padStart(4, \"0\");\n const name = CONTROL_NAMES[cp];\n return `U+${hex}${name ? ` (${name})` : \"\"}`;\n}\n\n/**\n * Scan a PreToolUse `tool_input` for forbidden control characters in every\n * NEW-text field. Returns the first finding, or null when clean.\n */\nexport function scanToolInput(toolInput: Record<string, unknown>): GuardFinding | null {\n const check = (field: string, value: unknown): GuardFinding | null => {\n if (typeof value !== \"string\") return null;\n const hit = findControlChar(value);\n return hit ? { field, index: hit.index, codePoint: hit.codePoint } : null;\n };\n\n const direct =\n check(\"content\", toolInput.content) ??\n check(\"new_string\", toolInput.new_string) ??\n check(\"new_source\", toolInput.new_source);\n if (direct) return direct;\n\n const edits = toolInput.edits;\n if (Array.isArray(edits)) {\n for (let i = 0; i < edits.length; i++) {\n const e = edits[i];\n if (e && typeof e === \"object\") {\n const hit = check(`edits[${i}].new_string`, (e as Record<string, unknown>).new_string);\n if (hit) return hit;\n }\n }\n }\n return null;\n}\n\n/** Build the PreToolUse deny JSON for a finding. */\nexport function buildDenyDecision(finding: GuardFinding): string {\n const reason =\n `VortEX guard: literal control character ${describeCodePoint(finding.codePoint)} at offset ${finding.index} of ${finding.field} — ` +\n `control bytes are never written literally into text files. Re-issue the write using escape notation for that character ` +\n `(e.g. \\\\x1b / \\\\u001b in code or regexes, the two-character sequence backslash+x, not the raw byte). ` +\n `If you are CLEANING an already-polluted file: old_string is not scanned — keep the raw byte only on the old_string side and put escape notation in the new text.`;\n return JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n permissionDecisionReason: reason,\n },\n });\n}\n\n/** The tools this guard understands. Anything else is out of scope → silent. */\nconst GUARDED_TOOLS = new Set([\"Write\", \"Edit\", \"MultiEdit\", \"NotebookEdit\"]);\n\n/** Out-of-scope tool? (A named tool that is not one of ours — e.g. a custom matcher routed `Task` here.) */\nfunction toolOutOfScope(payload: Record<string, unknown>): boolean {\n const name = payload.tool_name;\n return typeof name === \"string\" && name.length > 0 && !GUARDED_TOOLS.has(name);\n}\n\n/**\n * Decide on a raw PreToolUse payload. Returns the hook's stdout JSON (a deny\n * decision) when a forbidden character is found, or null to stay silent\n * (allow / not applicable / out-of-scope tool / unparseable — fail-open by design).\n */\nexport function guardWriteDecision(payloadText: string): string | null {\n try {\n const payload = JSON.parse(payloadText) as Record<string, unknown>;\n if (toolOutOfScope(payload)) return null;\n const toolInput = payload.tool_input;\n if (!toolInput || typeof toolInput !== \"object\" || Array.isArray(toolInput)) return null;\n const finding = scanToolInput(toolInput as Record<string, unknown>);\n return finding ? buildDenyDecision(finding) : null;\n } catch {\n return null; // fail-open: a guard bug must never block a write\n }\n}\n\n/**\n * CLI entry — `vortex guard write` (stdin: PreToolUse payload JSON).\n * Always exits 0: a deny is expressed via the JSON decision on stdout, and\n * everything else stays silent so the hook's `|| exit 0` wrapper never fires.\n */\nexport async function runGuardCli(\n argv: readonly string[],\n out: (s: string) => void,\n err: (s: string) => void,\n): Promise<number> {\n if (argv[0] !== \"write\") {\n err(\"[vortex] guard subcommands: write — PreToolUse hook body, reads the payload JSON from stdin.\\n\");\n return 0;\n }\n if (process.stdin.isTTY) {\n err(\"[vortex] guard write reads a Claude Code PreToolUse payload from stdin — it is wired by `vortex init` as a hook, not run by hand.\\n\");\n return 0;\n }\n let raw = \"\";\n try {\n raw = readFileSync(0, \"utf8\");\n } catch {\n return 0;\n }\n\n // Parse once: the decision needs tool_input, the self-recording needs cwd +\n // file_path for an honest ledger line. Any parse problem → fail-open silent.\n let payload: Record<string, unknown>;\n try {\n payload = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return 0;\n }\n if (toolOutOfScope(payload)) return 0;\n const toolInput = payload.tool_input;\n if (!toolInput || typeof toolInput !== \"object\" || Array.isArray(toolInput)) return 0;\n const finding = scanToolInput(toolInput as Record<string, unknown>);\n if (!finding) return 0;\n out(buildDenyDecision(finding) + \"\\n\");\n\n // Self-record the denial into the failure ledger (VortEX instances only) so\n // recurrence counting never depends on the agent noticing its own miss.\n // Best-effort: a ledger problem must never affect the deny above. The\n // instance root is resolved upward from the hook's cwd (Claude may have\n // `cd`-ed into a subfolder), and `autoRecord.failures: false` turns\n // self-recording off along with the rest of the loop.\n try {\n const cwd = typeof payload.cwd === \"string\" && payload.cwd ? payload.cwd : process.cwd();\n const root = resolveInstanceRoot(cwd);\n if (root) {\n const ctx = makeContext(root);\n if (loadVortexConfig(ctx).autoRecord.failures) {\n const filePath = (toolInput as Record<string, unknown>).file_path;\n const fileNote = typeof filePath === \"string\" && filePath ? ` (target: ${filePath})` : \"\";\n const { recordGuardDenial } = await import(\"./failures.js\");\n await recordGuardDenial(\n ctx.dataDir,\n `write guard denied a literal control character in ${finding.field}${fileNote}`,\n );\n }\n }\n } catch {\n // best-effort only\n }\n return 0;\n}\n\n/**\n * Nearest VortEX instance root at or above `startDir` (marker:\n * `.agent/vortex.json`), preferring an explicit `VORTEX_REPO_ROOT`, then the\n * host-provided `CLAUDE_PROJECT_DIR` — each honored only when it actually IS\n * an instance. Bounded walk; null when nothing matches (not an instance —\n * the guard still denies, it just has no ledger to count into).\n */\nexport function resolveInstanceRoot(startDir: string): string | null {\n const isInstance = (d: string): boolean => existsSync(join(d, \".agent\", \"vortex.json\"));\n const envRoot = process.env.VORTEX_REPO_ROOT?.trim();\n if (envRoot && isInstance(envRoot)) return envRoot;\n const projectDir = process.env.CLAUDE_PROJECT_DIR?.trim();\n if (projectDir && isInstance(projectDir)) return projectDir;\n let dir = resolve(startDir);\n for (let i = 0; i < 16; i++) {\n if (isInstance(dir)) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n return null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GUARD_WRITE_COMMAND,
|
|
3
|
+
GUARD_WRITE_MATCHER
|
|
4
|
+
} from "./chunk-2FVNWW77.js";
|
|
5
|
+
|
|
1
6
|
// ../plugins/session-rituals/dist/statusline.js
|
|
2
7
|
import { execFileSync } from "child_process";
|
|
3
8
|
import { closeSync, existsSync, openSync, readSync, readdirSync, readFileSync, statSync } from "fs";
|
|
@@ -31,7 +36,7 @@ function parseSettings(text) {
|
|
|
31
36
|
}
|
|
32
37
|
return parsed;
|
|
33
38
|
}
|
|
34
|
-
function ensureVortexHooks(existing) {
|
|
39
|
+
function ensureVortexHooks(existing, opts) {
|
|
35
40
|
const base = existing && typeof existing === "object" ? existing : {};
|
|
36
41
|
const hooks = { ...base.hooks ?? {} };
|
|
37
42
|
const added = [];
|
|
@@ -74,6 +79,17 @@ function ensureVortexHooks(existing) {
|
|
|
74
79
|
};
|
|
75
80
|
wire("SessionStart", SESSION_START_COMMAND);
|
|
76
81
|
wire("SessionEnd", SESSION_END_COMMAND);
|
|
82
|
+
if (opts?.guard) {
|
|
83
|
+
const src = hooks.PreToolUse ?? [];
|
|
84
|
+
const wired = src.some((g) => (g.hooks ?? []).some((h) => h.command === GUARD_WRITE_COMMAND));
|
|
85
|
+
if (!wired) {
|
|
86
|
+
hooks.PreToolUse = [
|
|
87
|
+
...src,
|
|
88
|
+
{ matcher: GUARD_WRITE_MATCHER, hooks: [{ type: "command", command: GUARD_WRITE_COMMAND }] }
|
|
89
|
+
];
|
|
90
|
+
added.push("PreToolUse");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
77
93
|
const settings = { ...base, hooks };
|
|
78
94
|
return { settings, added, alreadyWired: added.length === 0 };
|
|
79
95
|
}
|
|
@@ -131,7 +147,7 @@ function parseStatuslineInput(text) {
|
|
|
131
147
|
modelName: rawName.replace(/^Claude\s+/, ""),
|
|
132
148
|
effortLevel: str(effort.level),
|
|
133
149
|
transcriptPath: str(root.transcript_path),
|
|
134
|
-
dir: str(workspace.current_dir) ?? str(root.cwd),
|
|
150
|
+
dir: str(workspace.project_dir) ?? str(workspace.current_dir) ?? str(root.cwd),
|
|
135
151
|
contextWindowSize: windowSize > 0 ? windowSize : 2e5,
|
|
136
152
|
usedPercentage: clampPct(num(ctx.used_percentage)),
|
|
137
153
|
cacheReadTokens: nonneg(usage.cache_read_input_tokens),
|
|
@@ -381,14 +397,26 @@ function collectStatuslineProbes(d, now = /* @__PURE__ */ new Date()) {
|
|
|
381
397
|
now
|
|
382
398
|
};
|
|
383
399
|
}
|
|
384
|
-
function statuslineCommand(lite) {
|
|
385
|
-
|
|
400
|
+
function statuslineCommand(lite, directRoot) {
|
|
401
|
+
const sub = `statusline${lite ? " lite" : ""}`;
|
|
402
|
+
const npxForm = `npx --no-install vortex ${sub}`;
|
|
403
|
+
if (directRoot) {
|
|
404
|
+
const bin = `${directRoot.replace(/\\/g, "/").replace(/\/+$/, "")}/node_modules/@vortex-os/base/bin/vortex.mjs`;
|
|
405
|
+
return `node "${bin}" ${sub} || ${npxForm} || exit 0`;
|
|
406
|
+
}
|
|
407
|
+
return `${npxForm} || exit 0`;
|
|
408
|
+
}
|
|
409
|
+
var LOCAL_BIN_RELPATH = ["node_modules", "@vortex-os", "base", "bin", "vortex.mjs"];
|
|
410
|
+
function isOurStatuslineCommand(cmd) {
|
|
411
|
+
if (typeof cmd !== "string")
|
|
412
|
+
return false;
|
|
413
|
+
return /^(?:node ".*\/node_modules\/@vortex-os\/base\/bin\/vortex\.mjs" statusline(?: lite)? \|\| )?npx --no-install vortex statusline(?: lite)? \|\| exit 0$/.test(cmd);
|
|
386
414
|
}
|
|
387
|
-
function ensureStatusline(existing, lite, force = false) {
|
|
388
|
-
const command = statuslineCommand(lite);
|
|
415
|
+
function ensureStatusline(existing, lite, force = false, directRoot) {
|
|
416
|
+
const command = statuslineCommand(lite, directRoot);
|
|
389
417
|
const current = existing.statusLine;
|
|
390
418
|
const currentCmd = typeof current?.command === "string" ? current.command : void 0;
|
|
391
|
-
const isOurs =
|
|
419
|
+
const isOurs = isOurStatuslineCommand(currentCmd);
|
|
392
420
|
if (currentCmd === command && current?.type === "command") {
|
|
393
421
|
return { settings: existing, status: "already-ours" };
|
|
394
422
|
}
|
|
@@ -404,10 +432,11 @@ async function runStatuslineCli(argv, repoRoot, out, err) {
|
|
|
404
432
|
if (argv[0] === "install") {
|
|
405
433
|
const lite = argv.includes("--lite");
|
|
406
434
|
const force = argv.includes("--force");
|
|
435
|
+
const directRoot = existsSync(join(repoRoot, ...LOCAL_BIN_RELPATH)) ? repoRoot : void 0;
|
|
407
436
|
const settingsPath = join(repoRoot, ".claude", "settings.json");
|
|
408
437
|
const existingText = existsSync(settingsPath) ? readFileSync(settingsPath, "utf8") : null;
|
|
409
438
|
const parsed = parseSettings(existingText);
|
|
410
|
-
const result = ensureStatusline(parsed, lite, force);
|
|
439
|
+
const result = ensureStatusline(parsed, lite, force, directRoot);
|
|
411
440
|
if (result.status === "installed") {
|
|
412
441
|
const { mkdirSync, writeFileSync } = await import("fs");
|
|
413
442
|
mkdirSync(join(repoRoot, ".claude"), { recursive: true });
|
|
@@ -416,7 +445,7 @@ async function runStatuslineCli(argv, repoRoot, out, err) {
|
|
|
416
445
|
const payload = {
|
|
417
446
|
status: result.status,
|
|
418
447
|
settingsPath,
|
|
419
|
-
...result.status !== "kept-existing" ? { command: statuslineCommand(lite) } : {},
|
|
448
|
+
...result.status !== "kept-existing" ? { command: statuslineCommand(lite, directRoot) } : {},
|
|
420
449
|
...result.existing !== void 0 ? { existing: result.existing } : {}
|
|
421
450
|
};
|
|
422
451
|
out(JSON.stringify(payload, null, 2) + "\n");
|
|
@@ -464,7 +493,9 @@ export {
|
|
|
464
493
|
sniffEffortFromTranscript,
|
|
465
494
|
collectStatuslineProbes,
|
|
466
495
|
statuslineCommand,
|
|
496
|
+
LOCAL_BIN_RELPATH,
|
|
497
|
+
isOurStatuslineCommand,
|
|
467
498
|
ensureStatusline,
|
|
468
499
|
runStatuslineCli
|
|
469
500
|
};
|
|
470
|
-
//# sourceMappingURL=chunk-
|
|
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"]}
|