enigma-cli 1.31.4 → 1.31.6
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/assets/skills/anti-overengineering-policy/skill.json +1 -1
- package/assets/skills/anti-overengineering-review/skill.json +1 -1
- package/assets/skills/backend-policy/skill.json +1 -1
- package/assets/skills/ciphera-style-policy/skill.json +1 -1
- package/assets/skills/code-review-policy/skill.json +1 -1
- package/assets/skills/core-engineering-policy/skill.json +1 -1
- package/assets/skills/database-expert/skill.json +1 -1
- package/assets/skills/debugging-policy/skill.json +1 -1
- package/assets/skills/dependency-policy/skill.json +1 -1
- package/assets/skills/frontend-design/skill.json +1 -1
- package/assets/skills/frontend-policy/skill.json +1 -1
- package/assets/skills/git-policy/skill.json +1 -1
- package/assets/skills/logo-sourcing-policy/skill.json +1 -1
- package/assets/skills/security-policy/skill.json +1 -1
- package/assets/skills/skill-creator/skill.json +1 -1
- package/assets/skills/task-completion-policy/skill.json +1 -1
- package/assets/skills/technical-writing-policy/skill.json +1 -1
- package/assets/skills/testing-policy/skill.json +1 -1
- package/assets/skills/validation-policy/skill.json +1 -1
- package/bin/checksums.json +4 -4
- package/bin/enigma.mjs +9 -22
- package/bin/statusline.mjs +323 -0
- package/package.json +1 -1
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "On-demand over-engineering review - diff review, whole-repo audit, and enigma: debt-marker ledger (tags delete/stdlib/native/yagni/shrink, line/dep scoring); lists cuts, applies nothing.",
|
|
6
6
|
"updated": "2026-06-16T11:24:30+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "f742a2be3f328b9ea1ff9a35a449177c2cbec35ad16e46f7054b7a873a2ab017"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Backend/API architecture: controller-service-repository layering, API and request optimization (batching, avoiding redundant calls, skipping no-op writes), server-side caching (Redis), and Zod boundary validation.",
|
|
6
6
|
"updated": "2026-07-23T04:41:39+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "d091e8708888d23ee14cdd6c0e71f386861f2252d95360895a31c73db090a689"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Ciphera code style conventions (formatting, naming, imports, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
|
|
6
6
|
"updated": "2026-06-26T13:40:52+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "dc9ceb784004b05a0117c464e4bed05946835a04acea282586c0b735ee7c2284"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Pre-delivery self-review gate, prioritized review dimensions, and change-quality criteria.",
|
|
6
6
|
"updated": "2026-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "3d3bbe0602d5bbb4afe37648fe3c2fa39376b1bcbac5d8c441f01fad1e866ed0"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Core engineering execution policy and harness orchestration (highest-authority rules).",
|
|
6
6
|
"updated": "2026-07-16T22:43:53+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "d132f20db08806054d95e58d3d56c2aa7ebc8e6dd24d902b0a0ed9ddfae216c5"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Senior database architecture policy: query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
|
|
6
6
|
"updated": "2026-06-03T14:19:50+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "2883bcecb3202683ae6f81b073c3d6a9cec9c55029e011bdd06ba7ac3537297e"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Reproduce-isolate-fix debugging methodology with root-cause discipline and regression verification.",
|
|
6
6
|
"updated": "2026-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "14b0064c8b33a0dc85e51464b05005cf5801c756b1101789a6924b9548420f6b"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Dependency and supply-chain security: lockfiles and reproducible installs, version pinning, vulnerability auditing, vetting/minimizing packages, vendoring, and SBOM/provenance.",
|
|
6
6
|
"updated": "2026-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one.",
|
|
6
6
|
"updated": "2026-07-29T01:18:36+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "9e30ee7d8a1a1e8c6e7f4e043857cd01841c68a427752e45bc0cad9ec5cfa279"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Frontend architecture: reusable components, abstraction thresholds, state management, no-op detection (skip any operation whose result equals current state, not just form saves; dirty means different from the loaded snapshot, not touched), instant first paint (render the shell, load data async, skeletons), perceived performance (prefetch on intent, debounce/throttle, cancel stale requests, avoid waterfalls, lazy widgets), large-list rendering (infinite scroll/pagination, virtualization, skeletons, progressive loading), optimistic UI with rollback, visual restraint (one card level, spacing before borders, one elevation scale), icon actions (repeated row/card actions are icon-only buttons with aria-label plus title, not text labels), responsive/adaptive layout (fluid units, breakpoints, no overlap/overflow, viewport meta, touch targets), variable-length text (min-width:0 in flex/grid, wrap vs truncate, long unbroken strings, worst-case content checks), and AI chat/agent interfaces via Vercel's AI Elements registry instead of hand-rolled message threads.",
|
|
6
6
|
"updated": "2026-07-29T01:26:14+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "71ebc71510c8aa54d7925445a7dffc55dbce9eba209fb6a63bae05d119217d79"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Git & contribution policy (senior engineering standards).",
|
|
6
6
|
"updated": "2026-07-16T22:44:02+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "e6dfbc33884000d9d25841bd9c5a84d6558ffd374882cb7b34451eb2cebc2161"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Application and AI-agent security: secrets, authn/authz (least privilege), OWASP Top 10, transport/crypto baseline, secure logging, and agent/MCP/tool-use safety.",
|
|
6
6
|
"updated": "2026-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "9971e9d9127397d0152e89d24aad3191e2935e55a8483db7fd15f5d4d7a60e7a"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Create new skills, modify and improve existing skills, and measure skill performance with evals and benchmarks.",
|
|
6
6
|
"updated": "2026-07-29T01:18:36+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "13d24c217bdb6fe83fe16835d8f5c3d397a9f3338f16876c61ef96e97f34c90a"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Exhaustive completion discipline for long/multi-item tasks - inventory, coverage ledger, verified done.",
|
|
6
6
|
"updated": "2026-07-27T17:22:17+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "b9046c15fd636057a8e42e199d2eeb47297477f38a0a26a07a7b0bf71d61fcb4"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Concise, realistic technical copy - UI microcopy, descriptions, hints, empty/error states, and README/doc prose that informs without over-explaining or restating the obvious, and never uses a typographic dash.",
|
|
6
6
|
"updated": "2026-07-28T20:30:23+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "a4b792103eb1f9dad93b9d70ea79dc18fe9cbbc318facf5adb47ae5907d842f9"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Test strategy, coverage gates, deterministic tests, mocking discipline, regression-first bug fixing, and test-suite organization (layout by type/domain, mirrored paths, file naming, fixture/helper placement).",
|
|
6
6
|
"updated": "2026-06-16T17:11:49+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "3bdf591057b760f674fb2b1425f63acb426cda2c4f042e1a74c5a5d3807df664"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Strict frontend + backend schema validation, schema consistency, and safe client-facing error handling.",
|
|
6
6
|
"updated": "2026-07-22T01:41:06+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.6",
|
|
8
8
|
"sha": "d937df0052d1ec151728a28f6567744d9e7ca0a65d84b4d5c9a697f1e40d704f"
|
|
9
9
|
}
|
package/bin/checksums.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"enigma-darwin-arm64": "
|
|
3
|
-
"enigma-linux-arm64": "
|
|
4
|
-
"enigma-linux-x64": "
|
|
5
|
-
"enigma-win32-x64.exe": "
|
|
2
|
+
"enigma-darwin-arm64": "e7e35075aee9b4286ad4c1f9695f6dc3337acc220519d25418696723fb16b031",
|
|
3
|
+
"enigma-linux-arm64": "b174eac608664b7216e33c48e138272394fa5601ac6c9250136254e3da0c394e",
|
|
4
|
+
"enigma-linux-x64": "123a2965e0e2c5e6098d6de9530fe5700c98e3fed208e6e65f00fb5536120ed7",
|
|
5
|
+
"enigma-win32-x64.exe": "eca732e61512ce190d42c25eeac83d182a1154c0d405337dd2c49ffe4cc480d4"
|
|
6
6
|
}
|
package/bin/enigma.mjs
CHANGED
|
@@ -15,36 +15,23 @@
|
|
|
15
15
|
* here, since its own __dirname lives in Bun's virtual filesystem.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { spawn } from "node:child_process";
|
|
19
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
-
import { join } from "node:path";
|
|
21
18
|
import os from "node:os";
|
|
22
|
-
import {
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
import { spawn } from "node:child_process";
|
|
23
22
|
import { downloadBinary, installedBinary } from "./download.mjs";
|
|
23
|
+
import { ARCH, PLATFORM, packageVersion, pkgRoot } from "./platform.mjs";
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Fast path: an agent status bar (e.g. Claude Code's statusLine) calls `enigma
|
|
27
|
-
* statusline` on every refresh. Handle it here in the lightweight Node launcher
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* the level, e.g. `[ENIGMA:FULL]`.
|
|
27
|
+
* statusline` on every refresh. Handle it here in the lightweight Node launcher so
|
|
28
|
+
* it never resolves or spawns the Bun binary. The renderer is imported lazily, so
|
|
29
|
+
* every other command pays nothing for it.
|
|
31
30
|
*/
|
|
32
|
-
function readOutputStyle() {
|
|
33
|
-
let style = "off";
|
|
34
|
-
// Mirror config.ts precedence: global (~/.enigma.json) then local (cwd), nearest wins.
|
|
35
|
-
for (const dir of [os.homedir(), process.cwd()]) {
|
|
36
|
-
try {
|
|
37
|
-
const raw = JSON.parse(readFileSync(join(dir, ".enigma.json"), "utf8"));
|
|
38
|
-
if (raw && typeof raw.outputStyle === "string") style = raw.outputStyle;
|
|
39
|
-
} catch { /* missing/invalid .enigma.json - ignore */ }
|
|
40
|
-
}
|
|
41
|
-
return style;
|
|
42
|
-
};
|
|
43
31
|
if (process.argv[2] === "statusline") {
|
|
44
32
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
process.stdout.write(process.env.NO_COLOR ? `[${label}]` : `\x1b[38;5;172m[${label}]\x1b[0m`);
|
|
33
|
+
const { printStatusline } = await import("./statusline.mjs");
|
|
34
|
+
printStatusline();
|
|
48
35
|
} catch { /* a status bar must never error */ }
|
|
49
36
|
process.exit(0);
|
|
50
37
|
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Status bar renderer for agent status lines (Claude Code's `statusLine`).
|
|
4
|
+
*
|
|
5
|
+
* Lives on the launcher's fast path (bin/enigma.mjs), so it stays dependency-free
|
|
6
|
+
* and never resolves the Bun binary: a status bar re-runs on every refresh, and
|
|
7
|
+
* cold-starting a ~260 MB binary each time is not an option. That constraint is
|
|
8
|
+
* also why gate progress arrives through a small snapshot file the daemon writes
|
|
9
|
+
* instead of the gate's SQLite database, which only the Bun runtime can open.
|
|
10
|
+
*
|
|
11
|
+
* Both inputs are optional: the session JSON the agent pipes on stdin, and the
|
|
12
|
+
* gate snapshot. Output is one line at rest, two while a gate run is active.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here may throw. A status bar that errors displays the error until the
|
|
15
|
+
* next refresh, so every reader is guarded and every field is treated as absent
|
|
16
|
+
* until proven otherwise.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
|
|
23
|
+
/** Snapshot schema this renderer understands; a newer file is ignored, not guessed at. */
|
|
24
|
+
const SNAPSHOT_VERSION = 1;
|
|
25
|
+
|
|
26
|
+
/** Spinner frames, advanced once per second (the status bar's fastest refresh). */
|
|
27
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
28
|
+
|
|
29
|
+
const BAR_FILLED = "▰";
|
|
30
|
+
const BAR_EMPTY = "▱";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 256-color SGR codes. The status/state colors use the low indices so they follow
|
|
34
|
+
* the terminal's own palette, but greys are pinned to the greyscale ramp: index 8
|
|
35
|
+
* ("bright black") is rendered near-black by many terminals, which makes secondary
|
|
36
|
+
* text vanish on a dark background. 245 matches the weight of the harness's own
|
|
37
|
+
* hint text; `track` is a step quieter, since an empty meter cell is a background
|
|
38
|
+
* rail rather than something to read.
|
|
39
|
+
*/
|
|
40
|
+
const COLORS = { red: 1, green: 2, yellow: 3, blue: 4, cyan: 6, dim: 245, track: 240, amber: 172 };
|
|
41
|
+
|
|
42
|
+
/** Built at runtime so no raw control byte ever lives in this source file. */
|
|
43
|
+
const ESC = String.fromCharCode(27);
|
|
44
|
+
|
|
45
|
+
/** Matches any SGR sequence, so styled text can be measured by visible width. */
|
|
46
|
+
const SGR = new RegExp(`${ESC}\\[[0-9;]*m`, "g");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How a step status paints its cell in the pipeline bar. `pulse` marks the
|
|
50
|
+
* states that animate, so a parked or working run is visibly alive.
|
|
51
|
+
*/
|
|
52
|
+
const STEP_STYLES = {
|
|
53
|
+
completed: { color: "green", glyph: BAR_FILLED },
|
|
54
|
+
running: { color: "blue", glyph: BAR_FILLED },
|
|
55
|
+
fixing: { color: "cyan", glyph: BAR_FILLED },
|
|
56
|
+
fix_review: { color: "yellow", glyph: BAR_FILLED, pulse: true },
|
|
57
|
+
awaiting_approval: { color: "yellow", glyph: BAR_FILLED, pulse: true },
|
|
58
|
+
failed: { color: "red", glyph: BAR_FILLED },
|
|
59
|
+
skipped: { color: "dim", glyph: BAR_FILLED },
|
|
60
|
+
pending: { color: "track", glyph: BAR_EMPTY }
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Step statuses that mean the pipeline is working or parked rather than settled. */
|
|
64
|
+
const ACTIVE_STEP_STATUSES = new Set(["running", "fixing", "fix_review", "awaiting_approval"]);
|
|
65
|
+
|
|
66
|
+
/** Run statuses worth showing; a settled run leaves the status bar alone. */
|
|
67
|
+
const ACTIVE_RUN_STATUSES = new Set(["pending", "running"]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parses JSON, tolerating a leading UTF-8 BOM. Windows tooling emits one freely,
|
|
71
|
+
* and every reader here fails silently by design, so a BOM would otherwise show
|
|
72
|
+
* up as an inexplicably blank status bar.
|
|
73
|
+
*/
|
|
74
|
+
function parseJson(text) {
|
|
75
|
+
return JSON.parse(text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Wraps text in a 256-color SGR sequence, or returns it unchanged without color. */
|
|
79
|
+
function paint(text, color, color256) {
|
|
80
|
+
if (!color256) return text;
|
|
81
|
+
return `${ESC}[38;5;${COLORS[color] ?? COLORS.dim}m${text}${ESC}[0m`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Dims a string to the secondary palette entry. */
|
|
85
|
+
function dim(text, color256) {
|
|
86
|
+
return paint(text, "dim", color256);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Visible width of a string, ignoring SGR sequences. */
|
|
90
|
+
function width(text) {
|
|
91
|
+
return text.replace(SGR, "").length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Joins parts with a separator, dropping the lowest-priority parts (those latest
|
|
96
|
+
* in the list with `optional: true`) until the line fits `max` columns. Required
|
|
97
|
+
* parts are never dropped; an over-long line is truncated as a last resort.
|
|
98
|
+
*/
|
|
99
|
+
function fit(parts, sep, max) {
|
|
100
|
+
const kept = parts.filter((p) => p && p.text);
|
|
101
|
+
while (kept.length > 1) {
|
|
102
|
+
const line = kept.map((p) => p.text).join(sep);
|
|
103
|
+
if (width(line) <= max) return line;
|
|
104
|
+
const victim = kept.map((p, i) => (p.optional ? i : -1)).filter((i) => i !== -1).pop();
|
|
105
|
+
if (victim === undefined) break;
|
|
106
|
+
kept.splice(victim, 1);
|
|
107
|
+
}
|
|
108
|
+
const line = kept.map((p) => p.text).join(sep);
|
|
109
|
+
return width(line) <= max ? line : `${line.slice(0, Math.max(0, max - 1))}…`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Renders an n-cell meter, e.g. context window usage. */
|
|
113
|
+
function meter(fraction, cells, color, color256) {
|
|
114
|
+
const filled = Math.max(0, Math.min(cells, Math.round(fraction * cells)));
|
|
115
|
+
return paint(BAR_FILLED.repeat(filled), color, color256)
|
|
116
|
+
+ paint(BAR_EMPTY.repeat(cells - filled), "track", color256);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Colors a context percentage by pressure: green under 70, yellow under 90, red beyond. */
|
|
120
|
+
function pressureColor(pct) {
|
|
121
|
+
if (pct >= 90) return "red";
|
|
122
|
+
if (pct >= 70) return "yellow";
|
|
123
|
+
return "green";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Compact elapsed time: "42s", "7m12s", "2h05m". */
|
|
127
|
+
function formatElapsed(seconds) {
|
|
128
|
+
const s = Math.max(0, Math.floor(seconds));
|
|
129
|
+
if (s < 60) return `${s}s`;
|
|
130
|
+
const m = Math.floor(s / 60);
|
|
131
|
+
if (m < 60) return `${m}m${String(s % 60).padStart(2, "0")}s`;
|
|
132
|
+
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Reads the token-efficient output level from .enigma.json, mirroring config.ts
|
|
137
|
+
* precedence: global (~/.enigma.json) then local (cwd), nearest wins.
|
|
138
|
+
*/
|
|
139
|
+
function readOutputStyle() {
|
|
140
|
+
let style = "off";
|
|
141
|
+
for (const dir of [os.homedir(), process.cwd()]) {
|
|
142
|
+
try {
|
|
143
|
+
const raw = parseJson(readFileSync(join(dir, ".enigma.json"), "utf8"));
|
|
144
|
+
if (raw && typeof raw.outputStyle === "string") style = raw.outputStyle;
|
|
145
|
+
} catch { /* missing/invalid .enigma.json - ignore */ }
|
|
146
|
+
}
|
|
147
|
+
return style;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The [ENIGMA] badge, carrying the active token-efficient level when one is set. */
|
|
151
|
+
function badge(color256) {
|
|
152
|
+
const style = readOutputStyle();
|
|
153
|
+
const label = (!style || style === "off") ? "ENIGMA" : `ENIGMA:${style.toUpperCase()}`;
|
|
154
|
+
return paint(`[${label}]`, "amber", color256);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Path to the snapshot the gate daemon writes, mirroring gate/paths.ts. */
|
|
158
|
+
export function snapshotPath() {
|
|
159
|
+
const root = process.env.ENIGMA_GATE_HOME || join(os.homedir(), ".enigma", "gate");
|
|
160
|
+
return join(root, "statusline.json");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Reports whether a process is alive, used to discard a snapshot from a dead daemon. */
|
|
164
|
+
function pidAlive(pid) {
|
|
165
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
166
|
+
try {
|
|
167
|
+
process.kill(pid, 0);
|
|
168
|
+
return true;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
// EPERM means the process exists but belongs to another user.
|
|
171
|
+
return err && err.code === "EPERM";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Loads the gate snapshot for `cwd`, or null when there is nothing to show.
|
|
177
|
+
* Rejects a snapshot that is for another repository, from an unknown schema
|
|
178
|
+
* version, already settled, or left behind by a daemon that is no longer running.
|
|
179
|
+
*/
|
|
180
|
+
export function readSnapshot(cwd, path = snapshotPath()) {
|
|
181
|
+
let snap;
|
|
182
|
+
try {
|
|
183
|
+
snap = parseJson(readFileSync(path, "utf8"));
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (!snap || snap.version !== SNAPSHOT_VERSION) return null;
|
|
188
|
+
if (!ACTIVE_RUN_STATUSES.has(snap.status)) return null;
|
|
189
|
+
if (!Array.isArray(snap.steps) || snap.steps.length === 0) return null;
|
|
190
|
+
if (typeof snap.repoPath !== "string" || !isInside(cwd, snap.repoPath)) return null;
|
|
191
|
+
if (!pidAlive(snap.pid)) return null;
|
|
192
|
+
return snap;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Reports whether `cwd` is the repo root or a directory beneath it. */
|
|
196
|
+
function isInside(cwd, root) {
|
|
197
|
+
const a = String(cwd || "").replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
198
|
+
const b = root.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
199
|
+
return a === b || a.startsWith(`${b}/`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Renders the base line: the enigma badge, model, project, context meter and
|
|
204
|
+
* session cost. Everything after the badge is optional and drops away on a
|
|
205
|
+
* narrow terminal, in reverse order of usefulness.
|
|
206
|
+
*/
|
|
207
|
+
function renderBase(session, columns, color256) {
|
|
208
|
+
const model = session?.model?.display_name;
|
|
209
|
+
const dir = session?.workspace?.current_dir || session?.cwd;
|
|
210
|
+
const project = typeof dir === "string" ? dir.replace(/\\/g, "/").split("/").filter(Boolean).pop() : null;
|
|
211
|
+
const pct = session?.context_window?.used_percentage;
|
|
212
|
+
const cost = session?.cost?.total_cost_usd;
|
|
213
|
+
|
|
214
|
+
const parts = [{ text: badge(color256) }];
|
|
215
|
+
if (model) parts.push({ text: paint(model, "cyan", color256), optional: true });
|
|
216
|
+
if (project) parts.push({ text: dim(project, color256), optional: true });
|
|
217
|
+
if (typeof pct === "number") {
|
|
218
|
+
const rounded = Math.round(pct);
|
|
219
|
+
parts.push({
|
|
220
|
+
text: `${meter(rounded / 100, 8, pressureColor(rounded), color256)} ${dim(`${rounded}%`, color256)}`,
|
|
221
|
+
optional: true
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (typeof cost === "number" && cost > 0) {
|
|
225
|
+
parts.push({ text: dim(`$${cost.toFixed(2)}`, color256), optional: true });
|
|
226
|
+
}
|
|
227
|
+
return fit(parts, " ", columns);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Renders the gate line: a spinner, one bar cell per pipeline step, the step
|
|
232
|
+
* count, the active step with its elapsed time, and the PR once one exists.
|
|
233
|
+
*
|
|
234
|
+
* The bar doubles as the legend - cell N is step N - so the whole pipeline fits
|
|
235
|
+
* one line. The active cell alternates glyph each frame, which is what makes a
|
|
236
|
+
* long step visibly still working rather than hung.
|
|
237
|
+
*/
|
|
238
|
+
function renderGate(snap, columns, frame, nowSec, color256) {
|
|
239
|
+
const steps = snap.steps.filter((s) => s && typeof s.name === "string");
|
|
240
|
+
const done = steps.filter((s) => s.status === "completed" || s.status === "skipped").length;
|
|
241
|
+
const active = steps.find((s) => ACTIVE_STEP_STATUSES.has(s.status));
|
|
242
|
+
const failed = steps.find((s) => s.status === "failed");
|
|
243
|
+
const current = active ?? failed;
|
|
244
|
+
|
|
245
|
+
const cells = steps.map((s) => {
|
|
246
|
+
const style = STEP_STYLES[s.status] ?? STEP_STYLES.pending;
|
|
247
|
+
const glyph = style.pulse && frame % 2 === 1 ? BAR_EMPTY : style.glyph;
|
|
248
|
+
return paint(glyph, style.color, color256);
|
|
249
|
+
}).join("");
|
|
250
|
+
|
|
251
|
+
// The spinner turns only while a step is actually working; a failed run holds
|
|
252
|
+
// a red marker instead, so a stalled pipeline never looks like a busy one.
|
|
253
|
+
const spinner = active
|
|
254
|
+
? paint(SPINNER[frame % SPINNER.length], snap.awaiting ? "yellow" : "blue", color256)
|
|
255
|
+
: paint(BAR_FILLED, failed ? "red" : "dim", color256);
|
|
256
|
+
|
|
257
|
+
let labelColor = "blue";
|
|
258
|
+
if (snap.awaiting) labelColor = "yellow";
|
|
259
|
+
else if (!active && failed) labelColor = "red";
|
|
260
|
+
const label = snap.awaiting ? "needs you" : (current ? current.name : snap.status);
|
|
261
|
+
// A negative age means the snapshot and this process disagree about the clock.
|
|
262
|
+
// Dropping the segment is honest; a confident "0s" is not.
|
|
263
|
+
const started = current?.startedAt ?? snap.startedAt;
|
|
264
|
+
const age = typeof started === "number" ? nowSec - started : -1;
|
|
265
|
+
const elapsed = age >= 0 ? formatElapsed(age) : null;
|
|
266
|
+
|
|
267
|
+
const parts = [
|
|
268
|
+
{ text: `${spinner} ${dim("gate", color256)}` },
|
|
269
|
+
{ text: cells },
|
|
270
|
+
{ text: dim(`${done}/${steps.length}`, color256) },
|
|
271
|
+
{ text: paint(label, labelColor, color256) }
|
|
272
|
+
];
|
|
273
|
+
if (elapsed) parts.push({ text: dim(elapsed, color256), optional: true });
|
|
274
|
+
if (typeof snap.branch === "string" && snap.branch) {
|
|
275
|
+
parts.push({ text: dim(snap.branch, color256), optional: true });
|
|
276
|
+
}
|
|
277
|
+
if (typeof snap.prUrl === "string") {
|
|
278
|
+
const num = snap.prUrl.match(/\/(\d+)(?:\/)?$/);
|
|
279
|
+
if (num) parts.push({ text: dim(`PR #${num[1]}`, color256), optional: true });
|
|
280
|
+
}
|
|
281
|
+
return fit(parts, " ", columns);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Builds the full status bar. `frame` drives the animation and is derived from
|
|
286
|
+
* wall-clock seconds by the caller, so consecutive refreshes advance it without
|
|
287
|
+
* this module holding any state.
|
|
288
|
+
*/
|
|
289
|
+
export function render({ session, snapshot, columns = 80, frame = 0, nowSec = 0, color256 = true }) {
|
|
290
|
+
const lines = [renderBase(session, columns, color256)];
|
|
291
|
+
if (snapshot) lines.push(renderGate(snapshot, columns, frame, nowSec, color256));
|
|
292
|
+
return lines.join("\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Reads the piped session JSON, or null when nothing is piped in. */
|
|
296
|
+
function readSession() {
|
|
297
|
+
if (process.stdin.isTTY) return null;
|
|
298
|
+
try {
|
|
299
|
+
return parseJson(readFileSync(0, "utf8"));
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Entry point for `enigma statusline`. Prints the bar and never throws. */
|
|
306
|
+
export function printStatusline() {
|
|
307
|
+
try {
|
|
308
|
+
const session = readSession();
|
|
309
|
+
const cwd = session?.workspace?.current_dir || session?.cwd || process.cwd();
|
|
310
|
+
const columns = Number(process.env.COLUMNS) || 80;
|
|
311
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
312
|
+
process.stdout.write(render({
|
|
313
|
+
session,
|
|
314
|
+
snapshot: readSnapshot(cwd),
|
|
315
|
+
columns,
|
|
316
|
+
frame: nowSec,
|
|
317
|
+
nowSec,
|
|
318
|
+
color256: !process.env.NO_COLOR
|
|
319
|
+
}));
|
|
320
|
+
} catch {
|
|
321
|
+
// A status bar must never error or emit noise.
|
|
322
|
+
}
|
|
323
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enigma-cli",
|
|
3
|
-
"version": "1.31.
|
|
3
|
+
"version": "1.31.6",
|
|
4
4
|
"description": "Everything you need to work with a coding agent: install shared policy skills for Claude Code, OpenAI Codex and opencode, and set up portable git security hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|