@vortex-os/base 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +19 -0
- package/bin/vortex.mjs +17 -17
- package/dist/{catch-up-KIHTAUPX.js → catch-up-GDDKPZHJ.js} +2 -2
- package/dist/{chunk-7SNLVGBO.js → chunk-3L5DLEGP.js} +1 -1
- package/dist/chunk-3L5DLEGP.js.map +1 -0
- package/dist/chunk-DWANI3LV.js +470 -0
- package/dist/chunk-DWANI3LV.js.map +1 -0
- package/dist/index.d.ts +140 -1
- package/dist/index.js +42 -83
- package/dist/index.js.map +1 -1
- package/dist/statusline-NQKJ3NWD.js +30 -0
- package/dist/statusline-NQKJ3NWD.js.map +1 -0
- package/dist/{vectorize-RBDBTSTW.js → vectorize-PN4Y7XMO.js} +1 -1
- package/dist/vectorize-PN4Y7XMO.js.map +1 -0
- package/package.json +1 -1
- package/templates/commands/agenda.md +15 -15
- package/templates/commands/handoff.md +26 -26
- package/templates/commands/resume.md +52 -52
- package/templates/config/vortex.json +13 -13
- package/templates/manifest.json +1 -1
- package/templates/routers/.cursorrules +14 -14
- package/templates/routers/AGENTS.md +27 -27
- package/templates/routers/GEMINI.md +16 -16
- package/dist/chunk-7SNLVGBO.js.map +0 -1
- package/dist/vectorize-RBDBTSTW.js.map +0 -1
- /package/dist/{catch-up-KIHTAUPX.js.map → catch-up-GDDKPZHJ.js.map} +0 -0
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 vortex-os-project
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vortex-os-project
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -61,6 +61,25 @@ npx vortex session-start # start-of-session boot report (git pull + counts +
|
|
|
61
61
|
npx vortex session-end # no-op (kept for hook compatibility; gap handling is at session-start)
|
|
62
62
|
npx vortex doctor # health diagnosis
|
|
63
63
|
npx vortex update # refresh framework templates (never clobbers your edits)
|
|
64
|
+
npx vortex statusline # Claude Code status-bar renderer (see below)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Claude Code status bar (optional)
|
|
68
|
+
|
|
69
|
+
`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:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
🧠 Fable 5 │ █ max │ █░░░░░░░░░ 12% · 120K/1M │ 🕐 22:25 │ 💰 $9.22
|
|
73
|
+
5h ██████░░ 75%(1h9m) │ 7d ███████░ 96%(7h30m) │ 📦 cache 99%
|
|
74
|
+
📁 my-project │ ⎇ main 2b0949d │ ⏱ 1h26m │ +90 -49 │ ⧉ 1
|
|
75
|
+
🌀 VortEX v0.11.0 │ last: 2026-06-10_0452-notes.md
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Wire it up once — this writes the `statusLine` entry into the **current folder's** `.claude/settings.json` and **keeps any bar you already configured** (replace it explicitly with `--force`):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx vortex statusline install # full bar (4 lines inside an instance, 3 outside)
|
|
82
|
+
npx vortex statusline install --lite # compact 1-line bar
|
|
64
83
|
```
|
|
65
84
|
|
|
66
85
|
## Library usage
|
package/bin/vortex.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// `vortex` — the CLI shipped by @vortex-os/base.
|
|
3
|
-
//
|
|
4
|
-
// `npm i @vortex-os/base` puts this on the instance's path (node_modules/.bin),
|
|
5
|
-
// so `npx vortex init` / `npx vortex session-start` / `npx vortex --list` work
|
|
6
|
-
// without any monorepo checkout. It is a thin wrapper over the canonical
|
|
7
|
-
// dispatch (`runVortexCli`), which is bundled into base from
|
|
8
|
-
// `@vortex-os/session-rituals` — exactly one source of truth for the CLI logic.
|
|
9
|
-
//
|
|
10
|
-
// The dispatch lazily probes the optional `@vortex-os/memory-extended` add-on:
|
|
11
|
-
// when it is installed alongside base, `/recall` lights up; on a lean base-only
|
|
12
|
-
// install the probe is caught and the CLI runs with every other command.
|
|
13
|
-
|
|
14
|
-
import { sessionRituals } from "../dist/index.js";
|
|
15
|
-
|
|
16
|
-
const code = await sessionRituals.runVortexCli(process.argv.slice(2));
|
|
17
|
-
process.exitCode = code;
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `vortex` — the CLI shipped by @vortex-os/base.
|
|
3
|
+
//
|
|
4
|
+
// `npm i @vortex-os/base` puts this on the instance's path (node_modules/.bin),
|
|
5
|
+
// so `npx vortex init` / `npx vortex session-start` / `npx vortex --list` work
|
|
6
|
+
// without any monorepo checkout. It is a thin wrapper over the canonical
|
|
7
|
+
// dispatch (`runVortexCli`), which is bundled into base from
|
|
8
|
+
// `@vortex-os/session-rituals` — exactly one source of truth for the CLI logic.
|
|
9
|
+
//
|
|
10
|
+
// The dispatch lazily probes the optional `@vortex-os/memory-extended` add-on:
|
|
11
|
+
// when it is installed alongside base, `/recall` lights up; on a lean base-only
|
|
12
|
+
// install the probe is caught and the CLI runs with every other command.
|
|
13
|
+
|
|
14
|
+
import { sessionRituals } from "../dist/index.js";
|
|
15
|
+
|
|
16
|
+
const code = await sessionRituals.runVortexCli(process.argv.slice(2));
|
|
17
|
+
process.exitCode = code;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../plugins/session-rituals/src/catch-up.ts"],"sourcesContent":["import type { ModuleContext } from \"@vortex-os/core\";\n// Type-only — erased at compile time, so importing it does NOT pull the\n// `@vortex-os/memory-extended` add-on (or its native sqlite/level deps) into\n// the module graph of consumers that only need the types. The runtime engine\n// is loaded lazily inside the function body via `await import(...)`.\nimport type { sessionArchive } from \"@vortex-os/memory-extended\";\n\n/**\n * Start-of-session \"catch-up\": fold conversation transcripts into the local\n * search archive without the user ever having to wrap up a session.\n *\n * Two sources, one pass:\n * - **local (a)** — this machine's own transcripts that are not archived yet,\n * read from every detected agent host's transcript store (Claude Code,\n * Codex, Gemini) and scoped to the current project. Because all hosts are\n * swept on every start, a single Claude Code session-start also folds in the\n * Codex/Gemini sessions you ran in the same project, into one archive.\n * - **pulled (b)** — transcripts created on another machine that arrived as\n * normalized text via git sync. Their text is present but this machine's\n * DB (local, derived, gitignored) has never indexed them.\n *\n * Text only — vectorization is deferred to recall/rebuild so session start\n * stays fast. The whole step is gated by `autoRecord.archive` at the call site\n * and is best-effort: callers should treat a thrown archive backend (e.g. the\n * native sqlite module not built) as \"skip\", never as a fatal start error.\n */\nexport interface CatchUpResult {\n /** Local transcripts newly archived this run (source a). */\n readonly ingestedLocal: number;\n /** Normalized transcripts from another machine newly indexed (source b). */\n readonly indexedPulled: number;\n /** Per-session ingest errors (source a). */\n readonly errors: number;\n}\n\nexport interface CatchUpOptions {\n /** Restrict local ingest to one project's transcripts. Default: `ctx.repoRoot`. */\n readonly cwd?: string;\n /**\n * Transcript adapters for local ingest. Default: all CLI hosts — Claude Code,\n * Codex, and Gemini. Each adapter's `detect()` returns false when its host is\n * absent, so registering all three is ~free on a machine that only uses one.\n * (Claude Desktop is opt-in — it needs the `classic-level` dependency — so it\n * is not in the default set; a caller can add it.) Tests inject fakes (or a\n * sandbox `env.home`) so the scan never touches the real home directory.\n */\n readonly adapters?: sessionArchive.IngestParams[\"adapters\"];\n /** Adapter environment override (e.g. a sandbox HOME). Tests use this. */\n readonly env?: sessionArchive.IngestParams[\"env\"];\n}\n\nexport async function catchUpSessions(\n ctx: ModuleContext,\n opts?: CatchUpOptions,\n): Promise<CatchUpResult> {\n // Lazy-load the optional add-on. Base ships without `memory-extended`; this\n // import resolves only when the add-on is installed alongside it. The call\n // site already gates this step on `autoRecord.archive` and treats a thrown\n // backend as \"skip\", so a missing add-on surfaces as a normal load error\n // the caller can catch.\n const { sessionArchive } = await import(\"@vortex-os/memory-extended\");\n\n const dataDir = ctx.dataDir;\n const cwd = opts?.cwd ?? ctx.repoRoot;\n const adapters = opts?.adapters ?? [\n sessionArchive.claudeCodeAdapter,\n sessionArchive.codexAdapter,\n sessionArchive.geminiAdapter,\n ];\n\n // (a) Ingest this machine's not-yet-archived transcripts for the current\n // project. Writes the normalized copy into the archive (which git syncs) and\n // a local DB row. Text only — no vectorization here.\n const ingestResult = await sessionArchive.ingest({ adapters, dataDir, cwd, env: opts?.env });\n\n // (b) Index normalized transcripts that arrived from another machine via git\n // pull — their text is on disk but this machine's DB has no row yet.\n const store = new sessionArchive.SessionArchiveStore(dataDir);\n let indexedPulled = 0;\n try {\n indexedPulled = store.reindexFromNormalized().indexed;\n } finally {\n store.close();\n }\n\n return {\n ingestedLocal: ingestResult.sessionsIngested,\n indexedPulled,\n errors: ingestResult.errors.length,\n };\n}\n"],"mappings":";AAmDA,eAAsB,gBACpB,KACA,MAAqB;AAOrB,QAAM,EAAE,eAAc,IAAK,MAAM,OAAO,4BAA4B;AAEpE,QAAM,UAAU,IAAI;AACpB,QAAM,MAAM,MAAM,OAAO,IAAI;AAC7B,QAAM,WAAW,MAAM,YAAY;IACjC,eAAe;IACf,eAAe;IACf,eAAe;;AAMjB,QAAM,eAAe,MAAM,eAAe,OAAO,EAAE,UAAU,SAAS,KAAK,KAAK,MAAM,IAAG,CAAE;AAI3F,QAAM,QAAQ,IAAI,eAAe,oBAAoB,OAAO;AAC5D,MAAI,gBAAgB;AACpB,MAAI;AACF,oBAAgB,MAAM,sBAAqB,EAAG;EAChD;AACE,UAAM,MAAK;EACb;AAEA,SAAO;IACL,eAAe,aAAa;IAC5B;IACA,QAAQ,aAAa,OAAO;;AAEhC;","names":[]}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
// ../plugins/session-rituals/dist/statusline.js
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import { closeSync, existsSync, openSync, readSync, readdirSync, readFileSync, statSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
// ../plugins/session-rituals/dist/ensure-hooks.js
|
|
7
|
+
var SESSION_START_COMMAND = "npx --no-install vortex session-start || exit 0";
|
|
8
|
+
var SESSION_END_COMMAND = "npx --no-install vortex session-end || exit 0";
|
|
9
|
+
var LEGACY_COMMANDS = {
|
|
10
|
+
SessionStart: [
|
|
11
|
+
"npx --no-install -p @vortex-os/base vortex session-start || exit 0",
|
|
12
|
+
"npx --no-install -p @vortex-os/base vortex session-start"
|
|
13
|
+
],
|
|
14
|
+
SessionEnd: [
|
|
15
|
+
"npx --no-install -p @vortex-os/base vortex session-end || exit 0",
|
|
16
|
+
"npx --no-install -p @vortex-os/base vortex session-end"
|
|
17
|
+
]
|
|
18
|
+
};
|
|
19
|
+
function parseSettings(text) {
|
|
20
|
+
const trimmed = (text ?? "").trim();
|
|
21
|
+
if (trimmed.length === 0)
|
|
22
|
+
return {};
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(trimmed);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
throw new Error(`.claude/settings.json is not valid JSON \u2014 refusing to overwrite. Fix or remove it first. (${e.message})`);
|
|
28
|
+
}
|
|
29
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
30
|
+
throw new Error(".claude/settings.json is not a JSON object \u2014 refusing to overwrite.");
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
function ensureVortexHooks(existing) {
|
|
35
|
+
const base = existing && typeof existing === "object" ? existing : {};
|
|
36
|
+
const hooks = { ...base.hooks ?? {} };
|
|
37
|
+
const added = [];
|
|
38
|
+
const wire = (event, command) => {
|
|
39
|
+
const legacy = LEGACY_COMMANDS[event];
|
|
40
|
+
const src = hooks[event] ?? [];
|
|
41
|
+
let changed = false;
|
|
42
|
+
let kept = false;
|
|
43
|
+
const groups = [];
|
|
44
|
+
for (const g of src) {
|
|
45
|
+
const hookList = [];
|
|
46
|
+
for (const h of g.hooks ?? []) {
|
|
47
|
+
const migrated = legacy.includes(h.command);
|
|
48
|
+
const cmd = migrated ? command : h.command;
|
|
49
|
+
if (cmd === command) {
|
|
50
|
+
if (kept) {
|
|
51
|
+
changed = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
kept = true;
|
|
55
|
+
if (migrated)
|
|
56
|
+
changed = true;
|
|
57
|
+
hookList.push(migrated ? { ...h, command } : h);
|
|
58
|
+
} else {
|
|
59
|
+
hookList.push(h);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (hookList.length > 0)
|
|
63
|
+
groups.push({ ...g, hooks: hookList });
|
|
64
|
+
else
|
|
65
|
+
changed = true;
|
|
66
|
+
}
|
|
67
|
+
if (!kept) {
|
|
68
|
+
groups.push({ hooks: [{ type: "command", command }] });
|
|
69
|
+
changed = true;
|
|
70
|
+
}
|
|
71
|
+
hooks[event] = groups;
|
|
72
|
+
if (changed)
|
|
73
|
+
added.push(event);
|
|
74
|
+
};
|
|
75
|
+
wire("SessionStart", SESSION_START_COMMAND);
|
|
76
|
+
wire("SessionEnd", SESSION_END_COMMAND);
|
|
77
|
+
const settings = { ...base, hooks };
|
|
78
|
+
return { settings, added, alreadyWired: added.length === 0 };
|
|
79
|
+
}
|
|
80
|
+
function serializeSettings(settings) {
|
|
81
|
+
return JSON.stringify(settings, null, 2) + "\n";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ../plugins/session-rituals/dist/statusline.js
|
|
85
|
+
var RST = "\x1B[0m";
|
|
86
|
+
var CYAN = "\x1B[36m";
|
|
87
|
+
var GREEN = "\x1B[32m";
|
|
88
|
+
var YELLOW = "\x1B[33m";
|
|
89
|
+
var RED = "\x1B[31m";
|
|
90
|
+
var DIM = "\x1B[2m";
|
|
91
|
+
var BOLD = "\x1B[1m";
|
|
92
|
+
var WHITE = "\x1B[37m";
|
|
93
|
+
var MAGENTA = "\x1B[35m";
|
|
94
|
+
var BLUE = "\x1B[34m";
|
|
95
|
+
var GREY = "\x1B[38;5;242m";
|
|
96
|
+
var ORANGE = "\x1B[38;5;208m";
|
|
97
|
+
var SEP = ` ${DIM}\u2502${RST} `;
|
|
98
|
+
function num(v, fallback = 0) {
|
|
99
|
+
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
100
|
+
}
|
|
101
|
+
function nonneg(v, fallback = 0) {
|
|
102
|
+
return Math.max(0, num(v, fallback));
|
|
103
|
+
}
|
|
104
|
+
function clampPct(v) {
|
|
105
|
+
return Math.min(100, Math.max(0, v));
|
|
106
|
+
}
|
|
107
|
+
function safeSegment(s, max = 60) {
|
|
108
|
+
const cleaned = s.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
109
|
+
return cleaned.length > max ? cleaned.slice(0, max - 1) + "\u2026" : cleaned;
|
|
110
|
+
}
|
|
111
|
+
function str(v) {
|
|
112
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
113
|
+
}
|
|
114
|
+
function obj(v) {
|
|
115
|
+
return typeof v === "object" && v !== null ? v : {};
|
|
116
|
+
}
|
|
117
|
+
function parseStatuslineInput(text) {
|
|
118
|
+
const root = obj(JSON.parse(text));
|
|
119
|
+
const model = obj(root.model);
|
|
120
|
+
const effort = obj(root.effort);
|
|
121
|
+
const workspace = obj(root.workspace);
|
|
122
|
+
const cost = obj(root.cost);
|
|
123
|
+
const ctx = obj(root.context_window);
|
|
124
|
+
const usage = obj(ctx.current_usage);
|
|
125
|
+
const limits = obj(root.rate_limits);
|
|
126
|
+
const five = obj(limits.five_hour);
|
|
127
|
+
const seven = obj(limits.seven_day);
|
|
128
|
+
const rawName = str(model.display_name) ?? "Claude";
|
|
129
|
+
const windowSize = nonneg(ctx.context_window_size, 2e5);
|
|
130
|
+
return {
|
|
131
|
+
modelName: rawName.replace(/^Claude\s+/, ""),
|
|
132
|
+
effortLevel: str(effort.level),
|
|
133
|
+
transcriptPath: str(root.transcript_path),
|
|
134
|
+
dir: str(workspace.current_dir) ?? str(root.cwd),
|
|
135
|
+
contextWindowSize: windowSize > 0 ? windowSize : 2e5,
|
|
136
|
+
usedPercentage: clampPct(num(ctx.used_percentage)),
|
|
137
|
+
cacheReadTokens: nonneg(usage.cache_read_input_tokens),
|
|
138
|
+
cacheCreationTokens: nonneg(usage.cache_creation_input_tokens),
|
|
139
|
+
costUsd: nonneg(cost.total_cost_usd),
|
|
140
|
+
durationMs: nonneg(cost.total_duration_ms),
|
|
141
|
+
linesAdded: nonneg(cost.total_lines_added),
|
|
142
|
+
linesRemoved: nonneg(cost.total_lines_removed),
|
|
143
|
+
fiveHourUsedPct: clampPct(num(five.used_percentage)),
|
|
144
|
+
fiveHourResetsAt: nonneg(five.resets_at),
|
|
145
|
+
sevenDayUsedPct: clampPct(num(seven.used_percentage)),
|
|
146
|
+
sevenDayResetsAt: nonneg(seven.resets_at)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function effortMeter(level) {
|
|
150
|
+
switch (level) {
|
|
151
|
+
case "low":
|
|
152
|
+
return { meter: "\u2582", color: GREY };
|
|
153
|
+
case "medium":
|
|
154
|
+
return { meter: "\u2584", color: GREEN };
|
|
155
|
+
case "high":
|
|
156
|
+
return { meter: "\u2586", color: YELLOW };
|
|
157
|
+
case "xhigh":
|
|
158
|
+
return { meter: "\u2587", color: ORANGE };
|
|
159
|
+
case "max":
|
|
160
|
+
return { meter: "\u2588", color: RED };
|
|
161
|
+
case "ultracode":
|
|
162
|
+
return { meter: "\u2588\u2726", color: MAGENTA };
|
|
163
|
+
default:
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function formatTokens(t) {
|
|
168
|
+
if (t >= 1e6)
|
|
169
|
+
return `${Math.floor(t / 1e6)}.${Math.floor(t % 1e6 / 1e5)}M`;
|
|
170
|
+
if (t >= 1e3)
|
|
171
|
+
return `${Math.floor(t / 1e3)}K`;
|
|
172
|
+
return String(Math.floor(t));
|
|
173
|
+
}
|
|
174
|
+
function formatWindow(t) {
|
|
175
|
+
if (t >= 1e6)
|
|
176
|
+
return `${Math.floor(t / 1e6)}M`;
|
|
177
|
+
if (t >= 1e3)
|
|
178
|
+
return `${Math.floor(t / 1e3)}K`;
|
|
179
|
+
return String(Math.floor(t));
|
|
180
|
+
}
|
|
181
|
+
function makeBar(pct, width) {
|
|
182
|
+
const filled = Math.min(width, Math.max(0, Math.floor(pct * width / 100)));
|
|
183
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
184
|
+
}
|
|
185
|
+
function usageColor(pct) {
|
|
186
|
+
if (pct >= 80)
|
|
187
|
+
return RED;
|
|
188
|
+
if (pct >= 50)
|
|
189
|
+
return YELLOW;
|
|
190
|
+
return GREEN;
|
|
191
|
+
}
|
|
192
|
+
function healthColor(remaining) {
|
|
193
|
+
if (remaining <= 30)
|
|
194
|
+
return RED;
|
|
195
|
+
if (remaining <= 60)
|
|
196
|
+
return YELLOW;
|
|
197
|
+
return GREEN;
|
|
198
|
+
}
|
|
199
|
+
function formatDuration(ms) {
|
|
200
|
+
const totalMin = Math.floor(ms / 6e4);
|
|
201
|
+
if (totalMin >= 60)
|
|
202
|
+
return `${Math.floor(totalMin / 60)}h${totalMin % 60}m`;
|
|
203
|
+
return `${totalMin}m${Math.floor(ms / 1e3) % 60}s`;
|
|
204
|
+
}
|
|
205
|
+
function untilReset(resetsAtSec, now) {
|
|
206
|
+
if (resetsAtSec <= 0)
|
|
207
|
+
return "";
|
|
208
|
+
const diffSec = resetsAtSec - Math.floor(now.getTime() / 1e3);
|
|
209
|
+
if (diffSec <= 0)
|
|
210
|
+
return "";
|
|
211
|
+
if (diffSec >= 86400)
|
|
212
|
+
return `${Math.floor(diffSec / 86400)}d${Math.floor(diffSec % 86400 / 3600)}h`;
|
|
213
|
+
return `${Math.floor(diffSec / 3600)}h${Math.floor(diffSec % 3600 / 60)}m`;
|
|
214
|
+
}
|
|
215
|
+
function pad2(n) {
|
|
216
|
+
return String(n).padStart(2, "0");
|
|
217
|
+
}
|
|
218
|
+
function renderStatusline(d, p, mode = "full") {
|
|
219
|
+
const usedPctDisplay = Math.round(d.usedPercentage);
|
|
220
|
+
const ctxColor = usageColor(usedPctDisplay);
|
|
221
|
+
const ctxUsedTokens = Math.round(d.usedPercentage * d.contextWindowSize / 100);
|
|
222
|
+
const ctxText = `${WHITE}${formatTokens(ctxUsedTokens)}/${formatWindow(d.contextWindowSize)}${RST}`;
|
|
223
|
+
const clock = `\u{1F550} ${WHITE}${pad2(p.now.getHours())}:${pad2(p.now.getMinutes())}${RST}`;
|
|
224
|
+
const level = p.effortLevel ?? d.effortLevel;
|
|
225
|
+
const effort = effortMeter(level);
|
|
226
|
+
const effortSeg = effort ? `${effort.color}${effort.meter}${RST} ${GREY}${safeSegment(level ?? "")}${RST}` : null;
|
|
227
|
+
const modelSeg = `\u{1F9E0} ${BOLD}${CYAN}${safeSegment(d.modelName, 30)}${RST}`;
|
|
228
|
+
const fiveRemain = Math.max(0, 100 - Math.round(d.fiveHourUsedPct));
|
|
229
|
+
const sevenRemain = Math.max(0, 100 - Math.round(d.sevenDayUsedPct));
|
|
230
|
+
const fiveColor = healthColor(fiveRemain);
|
|
231
|
+
const sevenColor = healthColor(sevenRemain);
|
|
232
|
+
const sessionSeg = `${WHITE}\u29C9 ${Math.max(1, Math.floor(p.sessionCount))}${RST}`;
|
|
233
|
+
const project = safeSegment(p.vortex || d.dir ? (d.dir ?? "").replace(/[\\/]+$/, "").split(/[\\/]/).pop() || "?" : "?", 40);
|
|
234
|
+
const gitBranch = safeSegment(p.gitBranch, 40);
|
|
235
|
+
const gitHash = safeSegment(p.gitHash, 16);
|
|
236
|
+
if (mode === "lite") {
|
|
237
|
+
const parts = [
|
|
238
|
+
`\u{1F4C1} ${BLUE}${project}${RST}`,
|
|
239
|
+
`${WHITE}\u2387${RST} ${YELLOW}${gitBranch}${RST}`,
|
|
240
|
+
effortSeg ? `${modelSeg}${SEP}${effortSeg}` : modelSeg,
|
|
241
|
+
`${ctxColor}${makeBar(usedPctDisplay, 8)} ${usedPctDisplay}%${RST} ${DIM}\xB7${RST} ${ctxText}`,
|
|
242
|
+
`${GREY}5h${RST} ${fiveColor}${fiveRemain}%${RST} ${DIM}\xB7${RST} ${GREY}7d${RST} ${sevenColor}${sevenRemain}%${RST}`,
|
|
243
|
+
sessionSeg,
|
|
244
|
+
...p.vortex?.version ? [`\u{1F300} ${MAGENTA}v${safeSegment(p.vortex.version, 20)}${RST}`] : [],
|
|
245
|
+
clock
|
|
246
|
+
];
|
|
247
|
+
return parts.join(SEP);
|
|
248
|
+
}
|
|
249
|
+
const l1 = [
|
|
250
|
+
effortSeg ? `${modelSeg}${SEP}${effortSeg}` : modelSeg,
|
|
251
|
+
`${ctxColor}${makeBar(usedPctDisplay, 10)} ${usedPctDisplay}%${RST} ${DIM}\xB7${RST} ${ctxText}`,
|
|
252
|
+
clock,
|
|
253
|
+
`\u{1F4B0} ${BOLD}${YELLOW}$${d.costUsd.toFixed(2)}${RST}`
|
|
254
|
+
].join(SEP);
|
|
255
|
+
const cacheTotal = d.cacheReadTokens + d.cacheCreationTokens;
|
|
256
|
+
const cachePct = cacheTotal > 0 ? Math.floor(d.cacheReadTokens * 100 / cacheTotal) : 0;
|
|
257
|
+
const fiveReset = untilReset(d.fiveHourResetsAt, p.now);
|
|
258
|
+
const sevenReset = untilReset(d.sevenDayResetsAt, p.now);
|
|
259
|
+
const l2 = [
|
|
260
|
+
`${GREY}5h${RST} ${fiveColor}${makeBar(fiveRemain, 8)} ${fiveRemain}%${RST}` + (fiveReset ? `${GREY}(${fiveReset})${RST}` : ""),
|
|
261
|
+
`${GREY}7d${RST} ${sevenColor}${makeBar(sevenRemain, 8)} ${sevenRemain}%${RST}` + (sevenReset ? `${GREY}(${sevenReset})${RST}` : ""),
|
|
262
|
+
`\u{1F4E6} ${GREEN}cache ${cachePct}%${RST}`
|
|
263
|
+
].join(SEP);
|
|
264
|
+
const l3 = [
|
|
265
|
+
`\u{1F4C1} ${BLUE}${project}${RST}`,
|
|
266
|
+
`${WHITE}\u2387${RST} ${YELLOW}${gitBranch}${RST} ${GREY}${gitHash}${RST}`,
|
|
267
|
+
`\u23F1 ${MAGENTA}${formatDuration(d.durationMs)}${RST}`,
|
|
268
|
+
`${GREEN}+${d.linesAdded}${RST} ${RED}-${d.linesRemoved}${RST}`,
|
|
269
|
+
sessionSeg
|
|
270
|
+
].join(SEP);
|
|
271
|
+
const lines = [l1, l2, l3];
|
|
272
|
+
if (p.vortex) {
|
|
273
|
+
let vx = `\u{1F300} ${BOLD}${MAGENTA}VortEX${RST}`;
|
|
274
|
+
if (p.vortex.version)
|
|
275
|
+
vx += ` ${GREY}v${safeSegment(p.vortex.version, 20)}${RST}`;
|
|
276
|
+
if (p.vortex.lastWorklog)
|
|
277
|
+
vx += `${SEP}${GREY}last:${RST} ${safeSegment(p.vortex.lastWorklog, 70)}`;
|
|
278
|
+
lines.push(vx);
|
|
279
|
+
}
|
|
280
|
+
return lines.join("\n");
|
|
281
|
+
}
|
|
282
|
+
function gitOut(dir, args) {
|
|
283
|
+
return execFileSync("git", ["-C", dir, ...args], {
|
|
284
|
+
encoding: "utf8",
|
|
285
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
286
|
+
}).trim();
|
|
287
|
+
}
|
|
288
|
+
function sniffEffortFromTranscript(transcriptPath, maxBytes = 4e6) {
|
|
289
|
+
try {
|
|
290
|
+
const size = statSync(transcriptPath).size;
|
|
291
|
+
const start = Math.max(0, size - maxBytes);
|
|
292
|
+
const length = size - start;
|
|
293
|
+
if (length <= 0)
|
|
294
|
+
return null;
|
|
295
|
+
const buf = Buffer.alloc(length);
|
|
296
|
+
const fd = openSync(transcriptPath, "r");
|
|
297
|
+
try {
|
|
298
|
+
readSync(fd, buf, 0, length, start);
|
|
299
|
+
} finally {
|
|
300
|
+
closeSync(fd);
|
|
301
|
+
}
|
|
302
|
+
const text = buf.toString("utf8");
|
|
303
|
+
const matches = text.match(/Set effort level to [a-z]+/g);
|
|
304
|
+
if (!matches || matches.length === 0)
|
|
305
|
+
return null;
|
|
306
|
+
return matches[matches.length - 1].slice("Set effort level to ".length);
|
|
307
|
+
} catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function countClaudeSessions() {
|
|
312
|
+
try {
|
|
313
|
+
if (process.platform === "win32") {
|
|
314
|
+
const out2 = execFileSync("tasklist", [], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
315
|
+
const n2 = (out2.match(/claude\.exe/gi) ?? []).length;
|
|
316
|
+
return n2 > 0 ? n2 : 1;
|
|
317
|
+
}
|
|
318
|
+
const out = execFileSync("pgrep", ["-x", "claude"], {
|
|
319
|
+
encoding: "utf8",
|
|
320
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
321
|
+
});
|
|
322
|
+
const n = out.split(/\r?\n/).filter(Boolean).length;
|
|
323
|
+
return n > 0 ? n : 1;
|
|
324
|
+
} catch {
|
|
325
|
+
return 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function newestWorklogName(repoRoot) {
|
|
329
|
+
try {
|
|
330
|
+
const dir = join(repoRoot, "data", "worklog");
|
|
331
|
+
const entries = readdirSync(dir, { recursive: true });
|
|
332
|
+
let best = null;
|
|
333
|
+
for (const rel of entries) {
|
|
334
|
+
const s = String(rel);
|
|
335
|
+
if (!s.endsWith(".md") || s.endsWith("_INDEX.md"))
|
|
336
|
+
continue;
|
|
337
|
+
if (best === null || s > best)
|
|
338
|
+
best = s;
|
|
339
|
+
}
|
|
340
|
+
return best ? best.replace(/\\/g, "/").split("/").pop() : null;
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function collectStatuslineProbes(d, now = /* @__PURE__ */ new Date()) {
|
|
346
|
+
const dir = d.dir ?? process.cwd();
|
|
347
|
+
let gitBranch = "-";
|
|
348
|
+
let gitHash = "-";
|
|
349
|
+
try {
|
|
350
|
+
const branch = gitOut(dir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
351
|
+
gitBranch = branch === "HEAD" ? "detached" : branch || "-";
|
|
352
|
+
gitHash = gitOut(dir, ["rev-parse", "--short", "HEAD"]) || "-";
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
let vortex = null;
|
|
356
|
+
try {
|
|
357
|
+
if (existsSync(join(dir, ".agent", "vortex.json"))) {
|
|
358
|
+
let version = null;
|
|
359
|
+
try {
|
|
360
|
+
const pkg = JSON.parse(readFileSync(join(dir, "node_modules", "@vortex-os", "base", "package.json"), "utf8"));
|
|
361
|
+
version = typeof pkg.version === "string" ? pkg.version : null;
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
vortex = { version, lastWorklog: newestWorklogName(dir) };
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
vortex = null;
|
|
368
|
+
}
|
|
369
|
+
let effortLevel = null;
|
|
370
|
+
if (d.effortLevel === "xhigh" && d.transcriptPath) {
|
|
371
|
+
const sniffed = sniffEffortFromTranscript(d.transcriptPath);
|
|
372
|
+
if (sniffed === "ultracode")
|
|
373
|
+
effortLevel = "ultracode";
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
gitBranch,
|
|
377
|
+
gitHash,
|
|
378
|
+
sessionCount: countClaudeSessions(),
|
|
379
|
+
vortex,
|
|
380
|
+
effortLevel,
|
|
381
|
+
now
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function statuslineCommand(lite) {
|
|
385
|
+
return `npx --no-install vortex statusline${lite ? " lite" : ""} || exit 0`;
|
|
386
|
+
}
|
|
387
|
+
function ensureStatusline(existing, lite, force = false) {
|
|
388
|
+
const command = statuslineCommand(lite);
|
|
389
|
+
const current = existing.statusLine;
|
|
390
|
+
const currentCmd = typeof current?.command === "string" ? current.command : void 0;
|
|
391
|
+
const isOurs = currentCmd === statuslineCommand(false) || currentCmd === statuslineCommand(true);
|
|
392
|
+
if (currentCmd === command && current?.type === "command") {
|
|
393
|
+
return { settings: existing, status: "already-ours" };
|
|
394
|
+
}
|
|
395
|
+
if (current && !isOurs && !force) {
|
|
396
|
+
return { settings: existing, status: "kept-existing", existing: currentCmd ?? JSON.stringify(current) };
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
settings: { ...existing, statusLine: { type: "command", command } },
|
|
400
|
+
status: "installed"
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
async function runStatuslineCli(argv, repoRoot, out, err) {
|
|
404
|
+
if (argv[0] === "install") {
|
|
405
|
+
const lite = argv.includes("--lite");
|
|
406
|
+
const force = argv.includes("--force");
|
|
407
|
+
const settingsPath = join(repoRoot, ".claude", "settings.json");
|
|
408
|
+
const existingText = existsSync(settingsPath) ? readFileSync(settingsPath, "utf8") : null;
|
|
409
|
+
const parsed = parseSettings(existingText);
|
|
410
|
+
const result = ensureStatusline(parsed, lite, force);
|
|
411
|
+
if (result.status === "installed") {
|
|
412
|
+
const { mkdirSync, writeFileSync } = await import("fs");
|
|
413
|
+
mkdirSync(join(repoRoot, ".claude"), { recursive: true });
|
|
414
|
+
writeFileSync(settingsPath, serializeSettings(result.settings), "utf8");
|
|
415
|
+
}
|
|
416
|
+
const payload = {
|
|
417
|
+
status: result.status,
|
|
418
|
+
settingsPath,
|
|
419
|
+
...result.status !== "kept-existing" ? { command: statuslineCommand(lite) } : {},
|
|
420
|
+
...result.existing !== void 0 ? { existing: result.existing } : {}
|
|
421
|
+
};
|
|
422
|
+
out(JSON.stringify(payload, null, 2) + "\n");
|
|
423
|
+
if (result.status === "kept-existing") {
|
|
424
|
+
err("[vortex] statusLine already configured with a different command \u2014 kept it. Re-run with --force to replace.\n");
|
|
425
|
+
}
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
428
|
+
const mode = argv[0] === "lite" ? "lite" : "full";
|
|
429
|
+
if (process.stdin.isTTY) {
|
|
430
|
+
err("[vortex] statusline renders Claude Code's statusLine stdin JSON \u2014 pipe it in, or wire it up with `vortex statusline install [--lite]`.\n");
|
|
431
|
+
return 0;
|
|
432
|
+
}
|
|
433
|
+
let raw = "";
|
|
434
|
+
try {
|
|
435
|
+
raw = readFileSync(0, "utf8");
|
|
436
|
+
} catch {
|
|
437
|
+
raw = "";
|
|
438
|
+
}
|
|
439
|
+
if (!raw.trim())
|
|
440
|
+
return 0;
|
|
441
|
+
let data;
|
|
442
|
+
try {
|
|
443
|
+
data = parseStatuslineInput(raw);
|
|
444
|
+
} catch {
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
out(renderStatusline(data, collectStatuslineProbes(data), mode));
|
|
448
|
+
return 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export {
|
|
452
|
+
SESSION_START_COMMAND,
|
|
453
|
+
SESSION_END_COMMAND,
|
|
454
|
+
parseSettings,
|
|
455
|
+
ensureVortexHooks,
|
|
456
|
+
serializeSettings,
|
|
457
|
+
safeSegment,
|
|
458
|
+
parseStatuslineInput,
|
|
459
|
+
effortMeter,
|
|
460
|
+
formatTokens,
|
|
461
|
+
formatWindow,
|
|
462
|
+
makeBar,
|
|
463
|
+
renderStatusline,
|
|
464
|
+
sniffEffortFromTranscript,
|
|
465
|
+
collectStatuslineProbes,
|
|
466
|
+
statuslineCommand,
|
|
467
|
+
ensureStatusline,
|
|
468
|
+
runStatuslineCli
|
|
469
|
+
};
|
|
470
|
+
//# sourceMappingURL=chunk-DWANI3LV.js.map
|