@askviraj/ai-plugins 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Viraj Patel
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.
@@ -0,0 +1,397 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * @askviraj/ai-plugins — installs the powerline statusline into Claude Code.
6
+ *
7
+ * Single-command oclif CLI, plain JS (no build step). Run via:
8
+ * npx @askviraj/ai-plugins --statusline --subagentstatusline [--yes]
9
+ */
10
+
11
+ const { Command, Flags, execute, settings } = require("@oclif/core");
12
+ const { existsSync } = require("node:fs");
13
+ const { spawnSync } = require("node:child_process");
14
+
15
+ // This is a plain-JS CLI (no TypeScript / ts-node). Tell oclif so it never tries
16
+ // to auto-transpile, which otherwise warns "Could not find typescript".
17
+ settings.enableAutoTranspile = false;
18
+ const { chmod, copyFile, mkdir, readFile, writeFile } = require(
19
+ "node:fs/promises",
20
+ );
21
+ const { homedir } = require("node:os");
22
+ const { delimiter, dirname, join } = require("node:path");
23
+ const readline = require("node:readline/promises");
24
+
25
+ const SCRIPTS_DIR = join(homedir(), ".claude", "scripts");
26
+ const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
27
+ const INSTALLED_SCRIPT = join(SCRIPTS_DIR, "statusline");
28
+ const USER_CONFIG_PATH = join(homedir(), ".config", "statusline.json");
29
+ const ASSETS_DIR = join(__dirname, "..", "tools", "statusline");
30
+
31
+ // Written verbatim per docs/statusline.md. ${HOME} is expanded by the shell when
32
+ // Claude Code runs the statusLine command.
33
+ const COMMAND = "${HOME}/.claude/scripts/statusline";
34
+
35
+ const STATUS_LINE = {
36
+ type: "command",
37
+ command: COMMAND,
38
+ padding: 0,
39
+ refreshInterval: 4,
40
+ };
41
+
42
+ const SUBAGENT_STATUS_LINE = {
43
+ type: "command",
44
+ command: COMMAND,
45
+ };
46
+
47
+ // GitHub shorthand passed to `claude plugin marketplace add`, and the marketplace
48
+ // name it resolves to (used as `<plugin>@<name>` when installing).
49
+ const MARKETPLACE_REF = "virajp/ai-plugins";
50
+ const MARKETPLACE_NAME = "virajp-plugins";
51
+
52
+ // All plugins published by the virajp-plugins marketplace.
53
+ const PLUGINS = ["vwf", "typescript-lsp", "dart-lsp", "context7", "mempalace"];
54
+
55
+ // Plugins that install at project scope by default; everything else is
56
+ // user-scoped. The marketplace itself is always user-scoped.
57
+ const PROJECT_SCOPED = new Set(["dart-lsp"]);
58
+ const scopeFor = name => (PROJECT_SCOPED.has(name) ? "project" : "user");
59
+
60
+ // How to install each external tool we depend on (matches this toolchain:
61
+ // brew + mise drive the rest; rtk ships as a brew formula).
62
+ const DEP_HINTS = {
63
+ brew:
64
+ "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"",
65
+ mise: "brew install mise",
66
+ claude: "mise use -g claude-code@latest",
67
+ rtk: "brew install --formulae rtk",
68
+ pnpm: "mise use -g pnpm@latest",
69
+ node: "mise use -g node@latest",
70
+ graphify: "mise use -g pipx:graphifyy@latest",
71
+ };
72
+
73
+ // Tools required whenever any plugin/marketplace install runs.
74
+ const CORE_DEPS = ["brew", "mise", "claude"];
75
+
76
+ // Extra tools specific plugins need: vwf ships an `rtk hook claude` Bash hook,
77
+ // uses graphify, and pulls in context7, which runs its MCP server via pnpx (pnpm).
78
+ const PLUGIN_EXTRA_DEPS = {
79
+ vwf: ["rtk", "pnpm", "graphify"],
80
+ context7: ["pnpm"],
81
+ };
82
+
83
+ class Installer extends Command {
84
+ async run() {
85
+ const { flags } = await this.parse(Installer);
86
+ const plan = this.resolvePlan(flags);
87
+
88
+ if (!plan.plugins.length && !plan.statusLine && !plan.subagentStatusLine) {
89
+ this.error(
90
+ "Nothing to do. Pass --all, --plugins, --plugin <name>, --statusline, and/or --subagentstatusline.",
91
+ );
92
+ }
93
+
94
+ this.checkDeps(plan);
95
+
96
+ if (plan.plugins.length) {
97
+ this.installPlugins(plan.plugins);
98
+ }
99
+
100
+ if (plan.statusLine || plan.subagentStatusLine) {
101
+ await this.installStatusline(plan, flags.yes);
102
+ }
103
+ }
104
+
105
+ // Turn the parsed flags into a concrete plan: which plugins to install and
106
+ // which statusline keys to set. --all is the superset of everything.
107
+ resolvePlan(flags) {
108
+ const all = flags.all;
109
+ let plugins;
110
+ if (all || flags.plugins) {
111
+ plugins = [...PLUGINS];
112
+ }
113
+ else if (flags.plugin?.length) {
114
+ const invalid = flags.plugin.filter(n => !PLUGINS.includes(n));
115
+ if (invalid.length) {
116
+ this.error(
117
+ `Unknown plugin(s): ${invalid.join(", ")}. Valid: ${
118
+ PLUGINS.join(", ")
119
+ }`,
120
+ );
121
+ }
122
+ plugins = [...new Set(flags.plugin)];
123
+ }
124
+ else {
125
+ plugins = [];
126
+ }
127
+ return {
128
+ all,
129
+ plugins,
130
+ statusLine: all || flags.statusline,
131
+ subagentStatusLine: all || flags.subagentstatusline,
132
+ };
133
+ }
134
+
135
+ // Verify every tool the plan needs is on PATH. If any are missing, print the
136
+ // install command for each and exit so the user installs them and re-runs.
137
+ checkDeps(plan) {
138
+ const missing = requiredTools(plan).filter(t => !hasBin(t));
139
+ if (!missing.length) {
140
+ return;
141
+ }
142
+ this.log("\nMissing required dependencies — install these, then re-run:\n");
143
+ for (const t of missing) {
144
+ this.log(` ${t}`);
145
+ this.log(` ${DEP_HINTS[t] || "see project docs"}`);
146
+ }
147
+ this.exit(1);
148
+ }
149
+
150
+ // Add the marketplace (user scope), then install each plugin at its scope.
151
+ installPlugins(plugins) {
152
+ this.log(`\nAdding marketplace ${MARKETPLACE_NAME} (user scope)…`);
153
+ if (
154
+ !this.runClaude([
155
+ "plugin",
156
+ "marketplace",
157
+ "add",
158
+ "--scope",
159
+ "user",
160
+ MARKETPLACE_REF,
161
+ ])
162
+ ) {
163
+ this.log(
164
+ "Marketplace add returned non-zero (may already exist) — continuing.",
165
+ );
166
+ }
167
+ for (const name of plugins) {
168
+ const scope = scopeFor(name);
169
+ this.log(`\nInstalling ${name} (${scope} scope)…`);
170
+ if (
171
+ !this.runClaude([
172
+ "plugin",
173
+ "install",
174
+ "--scope",
175
+ scope,
176
+ `${name}@${MARKETPLACE_NAME}`,
177
+ ])
178
+ ) {
179
+ this.log(`Failed to install ${name}.`);
180
+ }
181
+ }
182
+ }
183
+
184
+ // Run a `claude` subcommand, streaming its output. Returns true on exit 0.
185
+ runClaude(args) {
186
+ this.log(`$ claude ${args.join(" ")}`);
187
+ const res = spawnSync("claude", args, { stdio: "inherit" });
188
+ if (res.error) {
189
+ this.error(`Failed to run claude: ${res.error.message}`);
190
+ }
191
+ return res.status === 0;
192
+ }
193
+
194
+ // Copy the script, seed the user config, and set the requested statusline keys.
195
+ async installStatusline(plan, yes) {
196
+ await this.installScript();
197
+ await this.seedUserConfig();
198
+
199
+ const settings = await this.readSettings();
200
+ if (plan.statusLine) {
201
+ await this.applyKey(settings, "statusLine", STATUS_LINE, yes);
202
+ }
203
+ if (plan.subagentStatusLine) {
204
+ await this.applyKey(
205
+ settings,
206
+ "subagentStatusLine",
207
+ SUBAGENT_STATUS_LINE,
208
+ yes,
209
+ );
210
+ }
211
+ await this.writeSettings(settings);
212
+ }
213
+
214
+ // Copy the statusline script into ~/.claude/scripts/ and make it executable.
215
+ async installScript() {
216
+ await mkdir(SCRIPTS_DIR, { recursive: true });
217
+ await copyFile(join(ASSETS_DIR, "statusline"), INSTALLED_SCRIPT);
218
+ await chmod(INSTALLED_SCRIPT, 0o755);
219
+ this.log(`Installed script → ${INSTALLED_SCRIPT}`);
220
+ }
221
+
222
+ // Seed ~/.config/statusline.json with the bundled defaults, or deep-merge any
223
+ // missing settings into an existing file (existing user values are preserved).
224
+ async seedUserConfig() {
225
+ const defaults = JSON.parse(
226
+ await readFile(join(ASSETS_DIR, "statusline.json"), "utf8"),
227
+ );
228
+ await mkdir(dirname(USER_CONFIG_PATH), { recursive: true });
229
+
230
+ if (existsSync(USER_CONFIG_PATH)) {
231
+ const raw = await readFile(USER_CONFIG_PATH, "utf8");
232
+ if (raw.trim()) {
233
+ let existing;
234
+ try {
235
+ existing = JSON.parse(raw);
236
+ }
237
+ catch {
238
+ this.error(
239
+ `${USER_CONFIG_PATH} is not valid JSON. Fix or remove it, then retry.`,
240
+ );
241
+ }
242
+ await writeFile(
243
+ USER_CONFIG_PATH,
244
+ `${JSON.stringify(deepMerge(defaults, existing), null, 2)}\n`,
245
+ );
246
+ this.log(`Filled missing settings → ${USER_CONFIG_PATH}`);
247
+ return;
248
+ }
249
+ }
250
+ await writeFile(USER_CONFIG_PATH, `${JSON.stringify(defaults, null, 2)}\n`);
251
+ this.log(`Seeded defaults → ${USER_CONFIG_PATH}`);
252
+ }
253
+
254
+ async readSettings() {
255
+ if (!existsSync(SETTINGS_PATH)) {
256
+ return {};
257
+ }
258
+ const raw = await readFile(SETTINGS_PATH, "utf8");
259
+ if (!raw.trim()) {
260
+ return {};
261
+ }
262
+ try {
263
+ return JSON.parse(raw);
264
+ }
265
+ catch {
266
+ this.error(
267
+ `${SETTINGS_PATH} is not valid JSON. Fix or remove it, then retry.`,
268
+ );
269
+ }
270
+ }
271
+
272
+ // Set `key`, prompting before overwriting an existing value unless `yes`.
273
+ async applyKey(settings, key, value, yes) {
274
+ if (!yes && settings[key] !== undefined) {
275
+ this.log(`\nExisting ${key} in ${SETTINGS_PATH}:`);
276
+ this.log(JSON.stringify(settings[key], null, 2));
277
+ if (!(await confirm(`Overwrite ${key}?`))) {
278
+ this.log(`Skipped ${key}.`);
279
+ return;
280
+ }
281
+ }
282
+ settings[key] = value;
283
+ this.log(`Set ${key}.`);
284
+ }
285
+
286
+ async writeSettings(settings) {
287
+ await mkdir(dirname(SETTINGS_PATH), { recursive: true });
288
+ await writeFile(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\n`);
289
+ this.log(`Updated ${SETTINGS_PATH}`);
290
+ }
291
+ }
292
+
293
+ Installer.description =
294
+ "Install Viraj Patel's Claude Code toolkit: marketplace plugins (via the `claude` CLI) and the powerline statusline. Checks required tools (brew/mise/claude/rtk/pnpm/…) first and prints install hints for any that are missing.";
295
+
296
+ Installer.examples = [
297
+ "<%= config.bin %> --all",
298
+ "<%= config.bin %> --plugins",
299
+ "<%= config.bin %> --plugin vwf --plugin dart-lsp",
300
+ "<%= config.bin %> --statusline --subagentstatusline --yes",
301
+ ];
302
+
303
+ Installer.flags = {
304
+ all: Flags.boolean({
305
+ description:
306
+ "Install everything: every marketplace plugin plus both statusline keys",
307
+ }),
308
+ plugins: Flags.boolean({
309
+ description: `Install all marketplace plugins (${PLUGINS.join(", ")})`,
310
+ }),
311
+ plugin: Flags.string({
312
+ multiple: true,
313
+ description: `Install a specific plugin by name (repeatable). One of: ${
314
+ PLUGINS.join(", ")
315
+ }`,
316
+ }),
317
+ statusline: Flags.boolean({
318
+ description:
319
+ "Install `statusLine` (the main status bar) in ~/.claude/settings.json",
320
+ }),
321
+ subagentstatusline: Flags.boolean({
322
+ description:
323
+ "Install `subagentStatusLine` (the subagent panel) in ~/.claude/settings.json",
324
+ }),
325
+ yes: Flags.boolean({
326
+ char: "y",
327
+ description: "Overwrite existing config without prompting",
328
+ }),
329
+ };
330
+
331
+ // True if `bin` is an executable found on PATH.
332
+ function hasBin(bin) {
333
+ return (process.env.PATH || "")
334
+ .split(delimiter)
335
+ .some(dir => dir && existsSync(join(dir, bin)));
336
+ }
337
+
338
+ // The tools that must be present for the resolved install plan.
339
+ function requiredTools({ plugins, statusLine, subagentStatusLine }) {
340
+ const tools = new Set();
341
+ if (plugins.length) {
342
+ for (const d of CORE_DEPS) {
343
+ tools.add(d);
344
+ }
345
+ }
346
+ for (const p of plugins) {
347
+ for (const d of PLUGIN_EXTRA_DEPS[p] || []) {
348
+ tools.add(d);
349
+ }
350
+ }
351
+ if (statusLine || subagentStatusLine) {
352
+ tools.add("node");
353
+ }
354
+ return [...tools];
355
+ }
356
+
357
+ const isObject = v => v != null && typeof v === "object" && !Array.isArray(v);
358
+
359
+ // Deep-merge: objects merge key-by-key; arrays and scalars from `override` win.
360
+ // Called as deepMerge(defaults, existing) so existing user values are preserved
361
+ // and only missing keys are filled from the defaults.
362
+ function deepMerge(base, override) {
363
+ if (!isObject(base) || !isObject(override)) {
364
+ return override === undefined ? base : override;
365
+ }
366
+ const out = { ...base };
367
+ for (const key of Object.keys(override)) {
368
+ out[key] = isObject(base[key]) && isObject(override[key])
369
+ ? deepMerge(base[key], override[key])
370
+ : override[key];
371
+ }
372
+ return out;
373
+ }
374
+
375
+ async function confirm(message) {
376
+ const rl = readline.createInterface({
377
+ input: process.stdin,
378
+ output: process.stdout,
379
+ });
380
+ try {
381
+ const answer = (await rl.question(`${message} [y/N] `))
382
+ .trim()
383
+ .toLowerCase();
384
+ return answer === "y" || answer === "yes";
385
+ }
386
+ finally {
387
+ rl.close();
388
+ }
389
+ }
390
+
391
+ module.exports = Installer;
392
+
393
+ // oclif loads this file as the single command (see `oclif.commands` in
394
+ // package.json); when invoked directly it also bootstraps the CLI.
395
+ if (require.main === module) {
396
+ execute({ dir: __dirname });
397
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "author": "Viraj Patel",
3
+ "bin": {
4
+ "ai-plugins": "./bin/installer.js"
5
+ },
6
+ "dependencies": {
7
+ "@oclif/core": "latest"
8
+ },
9
+ "description": "CLI to install Viraj Patel's Claude Code toolkit: marketplace plugins (via the claude CLI) and the powerline statusline",
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "tools"
16
+ ],
17
+ "license": "MIT",
18
+ "name": "@askviraj/ai-plugins",
19
+ "oclif": {
20
+ "bin": "ai-plugins",
21
+ "commands": {
22
+ "strategy": "single",
23
+ "target": "./bin/installer.js"
24
+ }
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "github:virajp/ai-plugins"
29
+ },
30
+ "type": "commonjs",
31
+ "version": "1.0.0"
32
+ }
package/readme.md ADDED
@@ -0,0 +1,163 @@
1
+ # Plugins for Claude Code
2
+
3
+ A curated collection of opinionated Claude Code plugins by Viraj Patel — LSP
4
+ servers, MCP servers, a full Spec → Plan → Execute workflow plugin (`vwf`), and
5
+ a config-driven powerline `statusline` — for use with the Claude Code CLI.
6
+
7
+ ## Prerequisites
8
+
9
+ - [Node.js](https://nodejs.org/) — runs the installer via `pnpx` (or `npx`).
10
+ - [Claude Code CLI](https://claude.ai/code) — the installer drives it to add the
11
+ marketplace and install plugins.
12
+ - [Mise](https://mise.jdx.dev/) — the LSP plugins and the `vwf` npm→pnpm hook
13
+ resolve their tools through it.
14
+
15
+ > The installer **checks every required tool** for what you're installing
16
+ > (including `rtk` and `pnpm` for `vwf`) and prints the exact install command
17
+ > for anything missing — it never installs a dependency for you. Run it first to
18
+ > see what you need.
19
+
20
+ ## Installation
21
+
22
+ Everything installs through one CLI —
23
+ [`@askviraj/ai-plugins`](https://www.npmjs.com/package/@askviraj/ai-plugins). It
24
+ adds the `virajp-plugins` marketplace (user-scoped) and drives the Claude Code
25
+ CLI for you, installing each plugin at its default scope (`dart-lsp` is
26
+ project-scoped, every other plugin user-scoped).
27
+
28
+ > The examples below use `pnpx`; if you don't use `pnpm`, swap in `npx` — the
29
+ > commands are otherwise identical.
30
+
31
+ **Install everything — all plugins + statusline:**
32
+
33
+ ```sh
34
+ pnpx @askviraj/ai-plugins --all
35
+ ```
36
+
37
+ **Install all plugins (no statusline):**
38
+
39
+ ```sh
40
+ pnpx @askviraj/ai-plugins --plugins
41
+ ```
42
+
43
+ **Install specific plugins (`--plugin` is repeatable):**
44
+
45
+ ```sh
46
+ pnpx @askviraj/ai-plugins --plugin vwf --plugin dart-lsp
47
+ ```
48
+
49
+ Available plugin names: `vwf`, `mempalace`, `context7`, `typescript-lsp`,
50
+ `dart-lsp`.
51
+
52
+ > The **statusline** also installs through this CLI — see
53
+ > [Statusline](#statusline) below.
54
+
55
+ ## Plugins
56
+
57
+ ### vwf
58
+
59
+ The flagship plugin — a highly opinionated Spec → Plan → Execute workflow for
60
+ solo developers and small teams. Ships slash commands, subagents, skills, and
61
+ two `PreToolUse` / `Bash` hooks: one that transparently rewrites `npm`/`npx`
62
+ commands to `pnpm`, and an `rtk hook claude` hook (requires `rtk` — see
63
+ Prerequisites).
64
+
65
+ ```sh
66
+ pnpx @askviraj/ai-plugins --plugin vwf
67
+ ```
68
+
69
+ **Slash commands** (`/vwf:<name>`):
70
+
71
+ | Command | Description |
72
+ | -------------- | ---------------------------------------------------------------------------------------------- |
73
+ | `spec` | Maintain the always-current, full-product blueprint under `docs/specs/` (one doc per entity) |
74
+ | `plan` | Pick one slice of the spec, diff desired vs actual, write a reviewable cycle plan |
75
+ | `execute` | Implement an approved plan under TDD, then code review + security review |
76
+ | `archive` | Move completed plans aside (never deletes) |
77
+ | `architecture` | Bootstrap or correct `docs/specs/architecture.md` — system shape and machine-readable registry |
78
+ | `git-workflow` | Internal — worktree isolation, commits, merges, pushes (used by plan/execute) |
79
+
80
+ **Skills:**
81
+
82
+ | Skill | Description |
83
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
84
+ | `karpathy-guidelines` | Behavioral guidelines to reduce common LLM coding mistakes: avoid overcomplication, make surgical changes, surface assumptions, define verifiable success criteria |
85
+ | `rest-api-design` | Technology-agnostic principles and best practices for designing REST APIs |
86
+
87
+ #### vwf dependencies
88
+
89
+ All of vwf's dependencies live in the **same `virajp-plugins` marketplace**
90
+ (`mempalace` is re-listed here), so a clean install needs no other marketplace
91
+ registered:
92
+
93
+ ```sh
94
+ pnpx @askviraj/ai-plugins --plugin vwf # adds the marketplace, installs vwf + deps
95
+ ```
96
+
97
+ > Auto-enable is **event-driven** — it fires when you enable `vwf`, not
98
+ > continuously. If a dependency later gets disabled on its own, re-enable it
99
+ > directly or toggle `vwf` off and on again.
100
+
101
+ `vwf` depends on two other plugins, which Claude Code **auto-installs and
102
+ auto-enables** when you enable `vwf` (requires Claude Code ≥ 2.1.143):
103
+
104
+ - `context7@virajp-plugins` — Context7 MCP docs server
105
+ - `mempalace@virajp-plugins` — AI memory system
106
+
107
+ ### statusline
108
+
109
+ A standalone, powerline-style statusline (main two-line bar + subagent panel),
110
+ fully data-driven from JSON and themeable across three config layers (defaults →
111
+ `~/.config/statusline.json` → `<repo-root>/.config/statusline.json`). It
112
+ installs via a small CLI rather than the plugin marketplace — the installer
113
+ copies the script to `~/.claude/scripts/` and writes the chosen key(s) into
114
+ `~/.claude/settings.json`. Requires a [Nerd Font](https://www.nerdfonts.com/).
115
+
116
+ ```sh
117
+ # install both surfaces (use --statusline / --subagentstatusline individually too)
118
+ pnpx @askviraj/ai-plugins --statusline --subagentstatusline
119
+
120
+ # overwrite existing config without prompting
121
+ pnpx @askviraj/ai-plugins --statusline --subagentstatusline --yes
122
+ ```
123
+
124
+ See **[docs/statusline.md](./docs/statusline.md)** for setup and the full
125
+ configuration reference.
126
+
127
+ ### mempalace
128
+
129
+ AI memory system — mine projects and conversations into a searchable palace. 33
130
+ MCP tools, auto-save hooks, and guided setup. Maintained externally
131
+ ([MemPalace/mempalace](https://github.com/MemPalace/mempalace)) and re-listed
132
+ here; it is also a `vwf` dependency.
133
+
134
+ > When you install `vwf`, `mempalace` is pulled in and enabled automatically —
135
+ > you only need these steps to install `mempalace` on its own.
136
+
137
+ ```sh
138
+ pnpx @askviraj/ai-plugins --plugin mempalace
139
+ ```
140
+
141
+ ### context7
142
+
143
+ Context7 MCP server — fetches up-to-date library/framework documentation.
144
+
145
+ ```sh
146
+ pnpx @askviraj/ai-plugins --plugin context7
147
+ ```
148
+
149
+ ### typescript-lsp
150
+
151
+ TypeScript/JavaScript language server (via `typescript-language-server`).
152
+
153
+ ```sh
154
+ pnpx @askviraj/ai-plugins --plugin typescript-lsp
155
+ ```
156
+
157
+ ### dart-lsp
158
+
159
+ Dart language server.
160
+
161
+ ```sh
162
+ pnpx @askviraj/ai-plugins --plugin dart-lsp
163
+ ```
@@ -0,0 +1,515 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * Claude Code statusline — powerline style, config-driven, with subagent mode.
6
+ *
7
+ * ONE file serves both surfaces. Wire both keys to it in ~/.claude/settings.json:
8
+ * {
9
+ * "statusLine": { "type": "command", "command": "node ~/.claude/statusline", "padding": 0 },
10
+ * "subagentStatusLine": { "type": "command", "command": "node ~/.claude/statusline" }
11
+ * }
12
+ *
13
+ * Mode is detected from stdin: a payload with a `tasks` array is the subagent
14
+ * panel (one NDJSON {id,content} row per subagent); anything else is the main
15
+ * two-line status bar. Debug goes to stderr so it never corrupts the line.
16
+ *
17
+ * Configuration is fully data-driven and layered. Two files are deep-merged,
18
+ * low → high precedence (a higher layer overrides the same key in a lower one;
19
+ * objects merge key-by-key, arrays replace wholesale). Either layer may be absent:
20
+ * 2. ~/.config/statusline.json — the per-USER config, applied to every repo.
21
+ * Seeded with the full defaults by the installer (palette, symbols, line
22
+ * layout, subagent panel, …).
23
+ * 1. <repo-root>/.config/statusline.json — per-REPO overrides (highest).
24
+ * So a repo can set just `projectName` and inherit everything else, or override
25
+ * any nested value (a single segment's bg, one symbol, the gauge width, a status
26
+ * colour, …).
27
+ *
28
+ * Colours accept three forms anywhere a colour is expected: a palette name
29
+ * ("blue"), a hex string ("#458588" or "#abc"), or an RGB triple ([69,133,136]).
30
+ *
31
+ * A line is a list of segment entries. An entry is either a segment id string
32
+ * ("model") or an object {name, bg?, fg?, bold?} that overrides that segment's
33
+ * styling inline — both resolve their defaults from the `segments` map.
34
+ *
35
+ * Main test:
36
+ * echo '{"model":{"display_name":"Opus 4.8"},"effort":{"level":"high"},"session_name":"users-and-groups","workspace":{"current_dir":"/Users/virajpatel/Projects/github.com/95octane/workspace/.worktrees/users-and-groups/backend"},"cost":{"total_cost_usd":46.51,"total_duration_ms":33540000},"context_window":{"used_percentage":26,"context_window_size":1000000,"total_input_tokens":259000},"rate_limits":{"five_hour":{"used_percentage":7,"resets_at":1774200000},"seven_day":{"used_percentage":1.0,"resets_at":1774600000}}}' | node statusline
37
+ * Subagent test:
38
+ * echo '{"columns":120,"tasks":[{"id":"t1","name":"reviewer","status":"running","description":"Auditing auth flow","tokenCount":18234,"startTime":1774200000000}]}' | node statusline
39
+ */
40
+
41
+ const fs = require("fs");
42
+ const path = require("path");
43
+ const os = require("os");
44
+
45
+ const RESET = "\x1b[0m";
46
+ const BOLD = "\x1b[1m";
47
+
48
+ // Config filename, shared across both layers.
49
+ const CONFIG_FILE = "statusline.json";
50
+ // Relative location under a home/repo root for the user & repo layers.
51
+ const CONFIG_REL = [".config", CONFIG_FILE];
52
+
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+ // Config — resolved once per invocation, then exposed as module state so the
55
+ // segment builders and renderers can stay terse.
56
+ // ─────────────────────────────────────────────────────────────────────────────
57
+ let CFG = {}; // the fully merged config object
58
+ let PALETTE = {}; // name -> [r,g,b]
59
+ let SYM = {}; // data-type glyphs
60
+ let TYPE_SYM = {}; // subagent type -> glyph
61
+ let GAUGE = {}; // { width, filled, empty }
62
+ let SUB = {}; // subagent panel config
63
+ let SEP, SEP_THIN, CAP, THIN_FG, DEFAULT_FG, WORKTREE_RE;
64
+
65
+ const isObj = (v) => v != null && typeof v === "object" && !Array.isArray(v);
66
+
67
+ // Deep-merge `override` onto `base`: objects merge recursively, everything else
68
+ // (scalars AND arrays) is replaced wholesale by the override.
69
+ function deepMerge(base, override) {
70
+ if (!isObj(base) || !isObj(override)) return override === undefined ? base : override;
71
+ const out = { ...base };
72
+ for (const k of Object.keys(override)) {
73
+ out[k] = isObj(base[k]) && isObj(override[k]) ? deepMerge(base[k], override[k]) : override[k];
74
+ }
75
+ return out;
76
+ }
77
+
78
+ function readJson(p) {
79
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch (_) { return null; }
80
+ }
81
+
82
+ // Merge the two config layers, low → high precedence — each present layer is
83
+ // deep-merged onto the accumulator so a higher layer wins:
84
+ // 2. ~/.config/statusline.json (per-user) -> 1. <root>/.config/statusline.json (per-repo)
85
+ // The per-user file is seeded with the full defaults by the installer.
86
+ function loadConfig(root) {
87
+ let cfg = readJson(path.join(os.homedir(), ...CONFIG_REL)) || {};
88
+ const repo = root ? readJson(path.join(root, ...CONFIG_REL)) : null;
89
+ if (repo) cfg = deepMerge(cfg, repo);
90
+ return cfg;
91
+ }
92
+
93
+ // Hoist the hot paths of the merged config into module state.
94
+ function applyConfig(cfg) {
95
+ CFG = cfg || {};
96
+ PALETTE = CFG.palette || {};
97
+ SYM = CFG.symbols || {};
98
+ TYPE_SYM = CFG.typeSymbols || {};
99
+ GAUGE = CFG.gauge || {};
100
+ SUB = CFG.subagent || {};
101
+ const pl = CFG.powerline || {};
102
+ SEP = pl.sep ?? "";
103
+ SEP_THIN = pl.sepThin ?? "";
104
+ CAP = pl.cap ?? "";
105
+ THIN_FG = color(pl.thinFg || "grey");
106
+ DEFAULT_FG = color(CFG.defaultFg || "white");
107
+ WORKTREE_RE = new RegExp(CFG.worktreePattern || "worktree", "i");
108
+ }
109
+
110
+ // Resolve a colour spec to an [r,g,b] triple. Accepts a palette name, a hex
111
+ // string ("#rgb" or "#rrggbb"), or a literal triple. Falls back to white.
112
+ function color(v) {
113
+ if (Array.isArray(v) && v.length === 3) return v;
114
+ if (typeof v === "string") {
115
+ if (PALETTE[v]) return PALETTE[v];
116
+ if (v[0] === "#") {
117
+ let h = v.slice(1);
118
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
119
+ if (h.length === 6) {
120
+ const n = parseInt(h, 16);
121
+ if (!isNaN(n)) return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
122
+ }
123
+ }
124
+ }
125
+ return PALETTE.white || [251, 241, 199];
126
+ }
127
+
128
+ // ─────────────────────────────────────────────────────────────────────────────
129
+ // ANSI + powerline rendering.
130
+ // ─────────────────────────────────────────────────────────────────────────────
131
+ const fg = ([r, g, b]) => `\x1b[38;2;${r};${g};${b}m`;
132
+ const bg = ([r, g, b]) => `\x1b[48;2;${r};${g};${b}m`;
133
+ const sameColor = (a, b) => a && b && a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
134
+
135
+ /**
136
+ * Render one powerline row from an ordered list of segments {text, bg, fg?, bold?}.
137
+ * Opens with a rounded CAP colored as the first bg, joins neighbours with the
138
+ * fill triangle (fg = prev bg, bg = next bg), and when two neighbours share a
139
+ * background uses a thin divider so the seam stays visible. Closes with a
140
+ * triangle that dissolves into the terminal's own background.
141
+ */
142
+ function renderLine(segments) {
143
+ let out = "";
144
+ if (!segments.length) return out;
145
+ out += fg(segments[0].bg) + CAP + RESET;
146
+ for (let i = 0; i < segments.length; i++) {
147
+ const seg = segments[i];
148
+ out += bg(seg.bg) + fg(seg.fg || DEFAULT_FG) + (seg.bold ? BOLD : "");
149
+ out += ` ${seg.text} ` + RESET;
150
+
151
+ const next = segments[i + 1];
152
+ if (next && sameColor(seg.bg, next.bg)) {
153
+ out += bg(next.bg) + fg(THIN_FG) + SEP_THIN + RESET; // shared bg: thin seam
154
+ } else if (next) {
155
+ out += fg(seg.bg) + bg(next.bg) + SEP + RESET; // colored transition
156
+ } else {
157
+ out += fg(seg.bg) + SEP + RESET; // closing cap
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+
163
+ // ─────────────────────────────────────────────────────────────────────────────
164
+ // Formatting utilities.
165
+ // ─────────────────────────────────────────────────────────────────────────────
166
+
167
+ // Normalize a model field (object {display_name|id} or plain string) to a label.
168
+ function modelName(m) {
169
+ if (!m) return "";
170
+ const s = m.display_name || m.id || (typeof m === "string" ? m : "");
171
+ return s.replace(/\s*\([^)]*\)\s*$/, "");
172
+ }
173
+
174
+ // Normalize an effort field (object {level} or plain string) to a level string.
175
+ function effortLevel(e) {
176
+ if (!e) return "";
177
+ return e.level || (typeof e === "string" ? e : "");
178
+ }
179
+
180
+ // 1234567 -> "1.2M", 259000 -> "259k"
181
+ function humanTokens(n) {
182
+ if (n == null || isNaN(n)) return "?";
183
+ if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
184
+ if (n >= 1e3) return Math.round(n / 1e3) + "k";
185
+ return String(n);
186
+ }
187
+
188
+ // ms -> "9hr 19m" / "4m 12s"
189
+ function humanDuration(ms) {
190
+ if (ms == null || isNaN(ms)) return "?";
191
+ let s = Math.floor(ms / 1000);
192
+ const h = Math.floor(s / 3600); s -= h * 3600;
193
+ const m = Math.floor(s / 60); s -= m * 60;
194
+ if (h > 0) return `${h}hr ${m}m`;
195
+ if (m > 0) return `${m}m ${s}s`;
196
+ return `${s}s`;
197
+ }
198
+
199
+ // Normalize a timestamp to epoch milliseconds. Accepts:
200
+ // - epoch seconds (10-digit number, e.g. 1774200000)
201
+ // - epoch millis (13-digit number)
202
+ // - ISO 8601 string ("2026-03-28T15:00:00Z")
203
+ // Returns null if unparseable.
204
+ function toEpochMs(v) {
205
+ if (v == null) return null;
206
+ if (typeof v === "number") return v > 1e12 ? v : v * 1000;
207
+ const t = Date.parse(String(v));
208
+ return isNaN(t) ? null : t;
209
+ }
210
+
211
+ // Time remaining until a reset timestamp -> "4h36m" / "5d2h" / "now".
212
+ // Hardened to accept epoch seconds, epoch millis, or an ISO string.
213
+ function humanResetIn(resetsAt) {
214
+ const ms = toEpochMs(resetsAt);
215
+ if (ms == null) return null;
216
+ let s = Math.floor((ms - Date.now()) / 1000);
217
+ if (s <= 0) return "now";
218
+ const d = Math.floor(s / 86400); s -= d * 86400;
219
+ const h = Math.floor(s / 3600); s -= h * 3600;
220
+ const m = Math.floor(s / 60);
221
+ if (d > 0) return `${d}d${h}h`;
222
+ return h > 0 ? `${h}h${String(m).padStart(2, "0")}m` : `${m}m`;
223
+ }
224
+
225
+ // Fixed-width gauge: filled vs empty blocks (glyphs + width from config).
226
+ function gauge(pct) {
227
+ const width = GAUGE.width || 10;
228
+ const p = Math.max(0, Math.min(100, pct || 0));
229
+ const filled = Math.round((p / 100) * width);
230
+ return (GAUGE.filled || "▰").repeat(filled) + (GAUGE.empty || "▱").repeat(width - filled);
231
+ }
232
+
233
+ // ─────────────────────────────────────────────────────────────────────────────
234
+ // Git (filesystem-only branch/root; one bounded git call for dirtiness).
235
+ // ─────────────────────────────────────────────────────────────────────────────
236
+
237
+ // Walk up from startDir resolving repo root (dir containing .git) and branch
238
+ // from .git/HEAD. Handles worktrees/submodules (.git as a gitdir pointer file).
239
+ function resolveGit(startDir) {
240
+ let dir = startDir;
241
+ for (let i = 0; i < 40 && dir; i++) {
242
+ try {
243
+ const gitPath = path.join(dir, ".git");
244
+ const st = fs.statSync(gitPath);
245
+ let headFile;
246
+ if (st.isDirectory()) {
247
+ headFile = path.join(gitPath, "HEAD");
248
+ } else {
249
+ const ref = fs.readFileSync(gitPath, "utf8").trim();
250
+ const m = ref.match(/^gitdir:\s*(.+)$/);
251
+ if (!m) return { root: dir, branch: null };
252
+ const gd = path.isAbsolute(m[1]) ? m[1] : path.join(dir, m[1]);
253
+ headFile = path.join(gd, "HEAD");
254
+ }
255
+ const head = fs.readFileSync(headFile, "utf8").trim();
256
+ const rm = head.match(/^ref:\s*refs\/heads\/(.+)$/);
257
+ return { root: dir, branch: rm ? rm[1] : head.slice(0, 7) };
258
+ } catch (_) {
259
+ const parent = path.dirname(dir);
260
+ if (parent === dir) break;
261
+ dir = parent;
262
+ }
263
+ }
264
+ return { root: null, branch: null };
265
+ }
266
+
267
+ // Path after a "worktree" keyword component, else null.
268
+ function worktreeSubpath(cwd) {
269
+ const parts = cwd.split("/").filter(Boolean);
270
+ let idx = -1;
271
+ for (let i = 0; i < parts.length; i++) if (WORKTREE_RE.test(parts[i])) idx = i;
272
+ if (idx === -1 || idx === parts.length - 1) return null;
273
+ return parts.slice(idx + 1).join("/");
274
+ }
275
+
276
+ // Single dirty marker: "+" net additions, "-" net deletions, "±" both, "" clean.
277
+ // Shells out to git once (twice on a fresh repo), bounded by a 250ms timeout.
278
+ function gitDirtyMark(cwd) {
279
+ const { execFileSync } = require("child_process");
280
+ const opts = { cwd, encoding: "utf8", timeout: 250, stdio: ["ignore", "pipe", "ignore"] };
281
+ const run = (args) => execFileSync("git", args, opts);
282
+ let added = 0, removed = 0;
283
+ try {
284
+ let numstat;
285
+ try { numstat = run(["diff", "--numstat", "HEAD"]); }
286
+ catch (_) { numstat = run(["diff", "--numstat", "--cached"]); }
287
+ for (const line of numstat.split("\n")) {
288
+ const m = line.match(/^(\d+|-)\t(\d+|-)\t/);
289
+ if (!m) continue;
290
+ if (m[1] !== "-") added += parseInt(m[1], 10);
291
+ if (m[2] !== "-") removed += parseInt(m[2], 10);
292
+ }
293
+ if (run(["ls-files", "--others", "--exclude-standard"]).trim()) added += 1;
294
+ } catch (_) {
295
+ return "";
296
+ }
297
+ if (added && removed) return SYM.dirtyMix;
298
+ if (added) return SYM.dirtyAdd;
299
+ if (removed) return SYM.dirtyDel;
300
+ return "";
301
+ }
302
+
303
+ // ─────────────────────────────────────────────────────────────────────────────
304
+ // Segment registry — each builder takes the resolved context `c` and returns the
305
+ // segment TEXT (a string), or null to omit (when its data is absent). All styling
306
+ // (bg/fg/bold) is resolved separately from the `segments` config + inline entry
307
+ // overrides, so this map carries only the text logic. Add an entry here plus a
308
+ // `segments.<id>` default style to introduce a new segment.
309
+ // ─────────────────────────────────────────────────────────────────────────────
310
+ const SEGMENTS = {
311
+ model: (c) => `${SYM.model} ${c.model}${c.effort ? ` [${c.effort}]` : ""}`,
312
+ context: (c) =>
313
+ `${SYM.context} ${gauge(c.ctxPct)} ${humanTokens(c.ctxUsed)}/${humanTokens(c.ctxSize)} (${Math.round(c.ctxPct)}%)`,
314
+ rl5h: (c) => c.sessPct == null ? null
315
+ : `${SYM.win5h} ${c.sessPct.toFixed(1)}%${c.reset5h ? ` ${SYM.reset} ${c.reset5h}` : ""}`,
316
+ rl7d: (c) => c.weekPct == null ? null
317
+ : `${SYM.win7d} ${c.weekPct.toFixed(1)}%${c.reset7d ? ` ${SYM.reset} ${c.reset7d}` : ""}`,
318
+ session: (c) => c.sessionName ? `${SYM.session} ${c.sessionName}` : null,
319
+ cost: (c) => `${SYM.cost} ${c.cost.toFixed(2)}`,
320
+ duration: (c) => c.dur == null ? null : `${SYM.duration} ${humanDuration(c.dur)}`,
321
+ project: (c) => c.projectName ? `${c.projectSym} ${c.projectName}` : null,
322
+ worktree: (c) => c.wt ? `${SYM.worktree} ${SYM.folder} ${c.wt}` : null,
323
+ branch: (c) => {
324
+ if (!c.branch) return null;
325
+ let t = c.wt ? `${SYM.worktree} ${SYM.branch} ${c.branch}` : `${SYM.branch} ${c.branch}`;
326
+ if (c.dirty) t += ` ${c.dirty}`;
327
+ return t;
328
+ },
329
+ };
330
+
331
+ // Build one rendered segment from a line entry (id string or {name,bg,fg,bold}),
332
+ // resolving styling as: inline override → segments.<id> default → hard fallback.
333
+ function buildSegment(entry, c) {
334
+ const id = typeof entry === "string" ? entry : (entry.name || entry.id);
335
+ const build = SEGMENTS[id];
336
+ if (!build) { process.stderr.write(`statusline: unknown segment "${id}"\n`); return null; }
337
+
338
+ let text;
339
+ try { text = build(c); } catch (_) { return null; }
340
+ if (text == null) return null;
341
+
342
+ const base = (CFG.segments && CFG.segments[id]) || {};
343
+ const inline = typeof entry === "string" ? {} : entry;
344
+ const bgSpec = inline.bg ?? base.bg ?? "blue";
345
+ const fgSpec = inline.fg ?? base.fg;
346
+ const bold = inline.bold ?? base.bold ?? false;
347
+ return { text, bg: color(bgSpec), fg: fgSpec ? color(fgSpec) : undefined, bold: !!bold };
348
+ }
349
+
350
+ // ─────────────────────────────────────────────────────────────────────────────
351
+ // Main status bar.
352
+ // ─────────────────────────────────────────────────────────────────────────────
353
+ function renderMain(d) {
354
+ const cwd = d.workspace?.current_dir || d.cwd || process.cwd();
355
+ const git = resolveGit(cwd);
356
+ applyConfig(loadConfig(git.root));
357
+
358
+ const ctx = d.context_window || {};
359
+ const usage = ctx.current_usage || {};
360
+ const ctxSize = ctx.context_window_size;
361
+ const ctxPct = ctx.used_percentage ?? 0;
362
+ // Prefer the input-only total (matches used_percentage); fall back gracefully.
363
+ const ctxUsed =
364
+ ctx.total_input_tokens ??
365
+ ([usage.input_tokens, usage.cache_creation_input_tokens, usage.cache_read_input_tokens]
366
+ .reduce((a, v) => a + (v || 0), 0) || null) ??
367
+ (ctxSize ? Math.round((ctxPct / 100) * ctxSize) : null);
368
+
369
+ const rl = d.rate_limits || {};
370
+ const weekly = rl.seven_day || rl.weekly || {};
371
+
372
+ const c = {
373
+ model: modelName(d.model) || "Claude",
374
+ effort: effortLevel(d.effort),
375
+ cost: d.cost?.total_cost_usd ?? 0,
376
+ dur: d.cost?.total_duration_ms,
377
+ ctxPct, ctxUsed, ctxSize,
378
+ sessPct: rl.five_hour?.used_percentage,
379
+ weekPct: weekly.used_percentage,
380
+ reset5h: humanResetIn(rl.five_hour?.resets_at),
381
+ reset7d: humanResetIn(weekly.resets_at),
382
+ sessionName: d.session_name,
383
+ projectName: CFG.projectName,
384
+ projectSym: SYM.project,
385
+ wt: worktreeSubpath(cwd),
386
+ branch: git.branch,
387
+ dirty: git.branch ? gitDirtyMark(cwd) : "",
388
+ };
389
+
390
+ const lines = Array.isArray(CFG.lines) ? CFG.lines : [];
391
+ return lines
392
+ .map((line) => {
393
+ const segs = (Array.isArray(line) ? line : [])
394
+ .map((entry) => buildSegment(entry, c))
395
+ .filter(Boolean);
396
+ return segs.length ? renderLine(segs) : null;
397
+ })
398
+ .filter((l) => l != null)
399
+ .join("\n");
400
+ }
401
+
402
+ // ─────────────────────────────────────────────────────────────────────────────
403
+ // Subagent panel — one NDJSON {id, content} row per running subagent.
404
+ // Input shape: { columns, tasks: [{ id, name, type, status, description,
405
+ // label, startTime, tokenCount, cwd }, ...] }.
406
+ // ─────────────────────────────────────────────────────────────────────────────
407
+
408
+ // Map a status string to its configured glyph + colour. Statuses are tried in
409
+ // config order; the first whose `match` regex hits wins. An entry with an empty
410
+ // `match` is the fallback for anything unmatched (pending / queued / unknown).
411
+ function taskMark(status) {
412
+ const s = String(status || "").toLowerCase();
413
+ const statuses = SUB.statuses || {};
414
+ let fallback = null;
415
+ for (const def of Object.values(statuses)) {
416
+ if (!def || !def.match) { if (def) fallback = def; continue; }
417
+ try { if (new RegExp(def.match, "i").test(s)) return { sym: def.symbol, bg: color(def.bg) }; }
418
+ catch (_) { /* bad regex in config: skip */ }
419
+ }
420
+ if (fallback) return { sym: fallback.symbol, bg: color(fallback.bg) };
421
+ return { sym: "", bg: color("bg3") };
422
+ }
423
+
424
+ // Resolve a subagent segment's styling from subagent.segments.<key>.
425
+ function subStyle(key, fallbackBg) {
426
+ const s = (SUB.segments && SUB.segments[key]) || {};
427
+ return { bg: color(s.bg || fallbackBg), fg: s.fg ? color(s.fg) : undefined, bold: !!s.bold };
428
+ }
429
+
430
+ function renderSubagents(d) {
431
+ const cwd = d.cwd || (Array.isArray(d.tasks) && d.tasks[0] && d.tasks[0].cwd) || process.cwd();
432
+ applyConfig(loadConfig(resolveGit(cwd).root));
433
+
434
+ const tasks = Array.isArray(d.tasks) ? d.tasks : [];
435
+ const cols = Number(d.columns) || Number(process.env.COLUMNS) || 80;
436
+ const budgetFraction = SUB.descBudgetFraction ?? 0.45;
437
+
438
+ // Panel-wide fallbacks. Per the documented schema neither model nor effort is
439
+ // a per-task field, so these are usually the only source — and may be absent
440
+ // entirely. We read them defensively and only render when something exists.
441
+ const topModel = modelName(d.model);
442
+ const topEffort = effortLevel(d.effort);
443
+
444
+ const headBold = !!(SUB.segments && SUB.segments.head && SUB.segments.head.bold);
445
+ const headFg = SUB.segments && SUB.segments.head && SUB.segments.head.fg
446
+ ? color(SUB.segments.head.fg) : undefined;
447
+
448
+ const out = [];
449
+ for (const t of tasks) {
450
+ if (!t || t.id == null) continue;
451
+ const mk = taskMark(t.status);
452
+
453
+ // Head: status glyph · type glyph (replaces the raw type string).
454
+ const typeSym = TYPE_SYM[String(t.type || "").toLowerCase()] || TYPE_SYM._default;
455
+ const segs = [{ text: `${mk.sym} ${typeSym}`, bg: mk.bg, fg: headFg, bold: headBold }];
456
+
457
+ // Name: the subagent's identity as its own configurable segment. Only the
458
+ // explicit name — Claude Code reports `type` as the generic "local_agent"
459
+ // regardless of subagent_type, and the type glyph already conveys that, so
460
+ // a fallback to type would just be redundant noise. Omitted when unnamed.
461
+ const agentName = t.name;
462
+ if (agentName) {
463
+ const st = subStyle("name", "orange");
464
+ segs.push({ text: `${SYM.agent} ${agentName}`, bg: st.bg, fg: st.fg, bold: st.bold });
465
+ }
466
+
467
+ // Model / effort: prefer a per-task value (in case a future build adds one),
468
+ // fall back to the panel-wide value, omit when neither is present.
469
+ const model = modelName(t.model) || topModel;
470
+ const effort = effortLevel(t.effort) || topEffort;
471
+ if (model || effort) {
472
+ const label = [model, effort ? `[${effort}]` : ""].filter(Boolean).join(" ");
473
+ const st = subStyle("model", "blue");
474
+ segs.push({ text: `${SYM.model} ${label}`, bg: st.bg, fg: st.fg, bold: st.bold });
475
+ }
476
+
477
+ const desc = (t.description || t.label || "").replace(/\s+/g, " ").trim();
478
+ if (desc) {
479
+ const budget = Math.max(12, Math.floor(cols * budgetFraction));
480
+ const st = subStyle("desc", "bg3");
481
+ segs.push({ text: desc.length > budget ? desc.slice(0, budget - 1) + "…" : desc, bg: st.bg, fg: st.fg, bold: st.bold });
482
+ }
483
+ if (t.tokenCount != null) {
484
+ const st = subStyle("tokens", "aqua");
485
+ segs.push({ text: `${SYM.tokens} ${humanTokens(t.tokenCount)}`, bg: st.bg, fg: st.fg, bold: st.bold });
486
+ }
487
+
488
+ const startMs = toEpochMs(t.startTime);
489
+ if (startMs) {
490
+ const st = subStyle("duration", "purple");
491
+ segs.push({ text: `${SYM.duration} ${humanDuration(Date.now() - startMs)}`, bg: st.bg, fg: st.fg, bold: st.bold });
492
+ }
493
+
494
+ out.push(JSON.stringify({ id: t.id, content: renderLine(segs) }));
495
+ }
496
+ return out.join("\n");
497
+ }
498
+
499
+ // ─────────────────────────────────────────────────────────────────────────────
500
+ // stdin plumbing — route by payload shape.
501
+ // ─────────────────────────────────────────────────────────────────────────────
502
+ let buf = "";
503
+ process.stdin.setEncoding("utf8");
504
+ process.stdin.on("data", (c) => (buf += c));
505
+ process.stdin.on("end", () => {
506
+ try {
507
+ let d = {};
508
+ try { d = JSON.parse(buf); } catch (_) { /* keep defaults */ }
509
+ const output = Array.isArray(d.tasks) ? renderSubagents(d) : renderMain(d);
510
+ process.stdout.write(output);
511
+ } catch (e) {
512
+ process.stderr.write(`statusline error: ${e && e.stack ? e.stack : e}\n`);
513
+ process.stdout.write("⚡ Claude"); // never leave the line blank
514
+ }
515
+ });
@@ -0,0 +1,199 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/virajp/ai-plugins/main/schemas/statusline.schema.json",
3
+ "defaultFg": "white",
4
+ "gauge": {
5
+ "empty": "▱",
6
+ "filled": "▰",
7
+ "width": 10
8
+ },
9
+ "lines": [
10
+ [
11
+ "model",
12
+ "context",
13
+ "rl5h",
14
+ "rl7d",
15
+ "cost"
16
+ ],
17
+ [
18
+ "project",
19
+ "worktree",
20
+ "branch"
21
+ ]
22
+ ],
23
+ "palette": {
24
+ "aqua": [
25
+ 104,
26
+ 157,
27
+ 106
28
+ ],
29
+ "bg3": [
30
+ 102,
31
+ 92,
32
+ 84
33
+ ],
34
+ "blue": [
35
+ 69,
36
+ 133,
37
+ 136
38
+ ],
39
+ "green": [
40
+ 152,
41
+ 151,
42
+ 26
43
+ ],
44
+ "grey": [
45
+ 60,
46
+ 56,
47
+ 54
48
+ ],
49
+ "orange": [
50
+ 214,
51
+ 93,
52
+ 14
53
+ ],
54
+ "purple": [
55
+ 177,
56
+ 98,
57
+ 134
58
+ ],
59
+ "red": [
60
+ 204,
61
+ 36,
62
+ 29
63
+ ],
64
+ "white": [
65
+ 251,
66
+ 241,
67
+ 199
68
+ ],
69
+ "yellow": [
70
+ 215,
71
+ 153,
72
+ 33
73
+ ]
74
+ },
75
+ "powerline": {
76
+ "cap": "",
77
+ "sep": "",
78
+ "sepThin": "",
79
+ "thinFg": "grey"
80
+ },
81
+ "projectName": "Project-Name",
82
+ "segments": {
83
+ "branch": {
84
+ "bg": "aqua"
85
+ },
86
+ "context": {
87
+ "bg": "aqua"
88
+ },
89
+ "cost": {
90
+ "bg": "green",
91
+ "bold": true,
92
+ "fg": "white"
93
+ },
94
+ "duration": {
95
+ "bg": "aqua"
96
+ },
97
+ "model": {
98
+ "bg": "blue",
99
+ "bold": true,
100
+ "fg": "white"
101
+ },
102
+ "project": {
103
+ "bg": "green",
104
+ "bold": true,
105
+ "fg": "white"
106
+ },
107
+ "rl5h": {
108
+ "bg": "blue"
109
+ },
110
+ "rl7d": {
111
+ "bg": "purple"
112
+ },
113
+ "session": {
114
+ "bg": "orange"
115
+ },
116
+ "worktree": {
117
+ "bg": "yellow",
118
+ "fg": "grey"
119
+ }
120
+ },
121
+ "subagent": {
122
+ "descBudgetFraction": 0.45,
123
+ "segments": {
124
+ "desc": {
125
+ "bg": "bg3"
126
+ },
127
+ "duration": {
128
+ "bg": "purple"
129
+ },
130
+ "head": {
131
+ "bold": true
132
+ },
133
+ "model": {
134
+ "bg": "blue"
135
+ },
136
+ "name": {
137
+ "bg": "orange",
138
+ "bold": true
139
+ },
140
+ "tokens": {
141
+ "bg": "aqua"
142
+ }
143
+ },
144
+ "statuses": {
145
+ "done": {
146
+ "bg": "green",
147
+ "match": "done|complete|success|finish|ok",
148
+ "symbol": ""
149
+ },
150
+ "error": {
151
+ "bg": "red",
152
+ "match": "error|fail|cancel|abort",
153
+ "symbol": ""
154
+ },
155
+ "pending": {
156
+ "bg": "bg3",
157
+ "match": "",
158
+ "symbol": ""
159
+ },
160
+ "running": {
161
+ "bg": "blue",
162
+ "match": "run|active|progress|working|busy",
163
+ "symbol": ""
164
+ }
165
+ }
166
+ },
167
+ "symbols": {
168
+ "agent": "",
169
+ "branch": "",
170
+ "context": "",
171
+ "cost": "",
172
+ "dirtyAdd": "+",
173
+ "dirtyDel": "-",
174
+ "dirtyMix": "±",
175
+ "duration": "",
176
+ "folder": "",
177
+ "model": "⚡",
178
+ "project": "",
179
+ "repo": "",
180
+ "reset": "↻",
181
+ "session": "",
182
+ "tokens": "",
183
+ "win5h": "",
184
+ "win7d": "",
185
+ "worktree": "🌲"
186
+ },
187
+ "typeSymbols": {
188
+ "_default": "",
189
+ "background": "",
190
+ "cloud_agent": "",
191
+ "local_agent": "",
192
+ "mcp": "",
193
+ "remote_agent": "",
194
+ "review": "",
195
+ "task": "",
196
+ "test": ""
197
+ },
198
+ "worktreePattern": "worktree"
199
+ }