whale-igniter 1.4.0 → 1.5.2
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/CHANGELOG.md +35 -0
- package/README.md +68 -15
- package/dist/commands/adopt.js +7 -5
- package/dist/commands/atlas.js +196 -0
- package/dist/commands/changes.js +53 -39
- package/dist/commands/createComponent.js +3 -0
- package/dist/commands/createLanding.js +581 -52
- package/dist/commands/decision.js +1 -1
- package/dist/commands/forge.js +159 -0
- package/dist/commands/init.js +3 -2
- package/dist/commands/insights.js +0 -2
- package/dist/commands/refine.js +1 -8
- package/dist/commands/scribe.js +189 -0
- package/dist/commands/selene.js +0 -3
- package/dist/commands/sync.js +2 -2
- package/dist/commands/watch.js +1 -1
- package/dist/index.js +52 -2
- package/dist/mcp/server.js +3 -9
- package/dist/selene/promptBuilder.js +2 -2
- package/dist/templates/claude.js +4 -4
- package/dist/templates/conventions.js +4 -2
- package/dist/templates/decisions.js +17 -16
- package/dist/ui/splash.js +12 -7
- package/dist/utils/decisions.js +93 -13
- package/dist/utils/refinements.js +86 -13
- package/dist/version.js +1 -1
- package/package.json +4 -2
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
const
|
|
2
|
-
function
|
|
3
|
-
|
|
1
|
+
const MAX_SUMMARY = 120;
|
|
2
|
+
function summarize(d) {
|
|
3
|
+
const text = (d.context ?? d.decision).replace(/\s+/g, " ").trim();
|
|
4
|
+
const oneLine = text.length > MAX_SUMMARY ? text.slice(0, MAX_SUMMARY) + "…" : text;
|
|
5
|
+
return oneLine.replace(/\|/g, "\\|");
|
|
6
|
+
}
|
|
7
|
+
function linkFor(d) {
|
|
8
|
+
// Wiki lives in llm-wiki/; decisions live in intelligence/decisions/.
|
|
9
|
+
if (!d.sourcePath)
|
|
10
|
+
return d.title;
|
|
11
|
+
return `[${d.title}](../${d.sourcePath})`;
|
|
4
12
|
}
|
|
5
13
|
export function renderDecisions(decisions) {
|
|
6
14
|
const lines = [
|
|
7
15
|
"# Decision Log",
|
|
8
16
|
"",
|
|
9
|
-
"
|
|
17
|
+
"Compact index of architectural, product and tooling decisions, most recent first.",
|
|
18
|
+
"Open the linked file for full Context / Decision / Consequences.",
|
|
10
19
|
""
|
|
11
20
|
];
|
|
12
21
|
if (decisions.length === 0) {
|
|
@@ -14,20 +23,12 @@ export function renderDecisions(decisions) {
|
|
|
14
23
|
}
|
|
15
24
|
else {
|
|
16
25
|
const sorted = [...decisions].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
26
|
+
lines.push("| ID | Decision | Category | Status | Summary |");
|
|
27
|
+
lines.push("| --- | --- | --- | --- | --- |");
|
|
17
28
|
for (const d of sorted) {
|
|
18
|
-
lines.push(
|
|
19
|
-
lines.push("");
|
|
20
|
-
lines.push(`_${d.timestamp.slice(0, 10)} · ${d.category} · ${d.status}_`);
|
|
21
|
-
lines.push("");
|
|
22
|
-
if (d.context) {
|
|
23
|
-
lines.push("**Context**", "", truncate(d.context), "");
|
|
24
|
-
}
|
|
25
|
-
lines.push("**Decision**", "", truncate(d.decision));
|
|
26
|
-
if (d.consequences) {
|
|
27
|
-
lines.push("", "**Consequences**", "", truncate(d.consequences));
|
|
28
|
-
}
|
|
29
|
-
lines.push("", "---", "");
|
|
29
|
+
lines.push(`| ${d.id} | ${linkFor(d)} | ${d.category} | ${d.status} | ${summarize(d)} |`);
|
|
30
30
|
}
|
|
31
|
+
lines.push("");
|
|
31
32
|
}
|
|
32
33
|
lines.push("_Generated by Whale Igniter._");
|
|
33
34
|
return lines.join("\n");
|
package/dist/ui/splash.js
CHANGED
|
@@ -13,6 +13,13 @@ import { capabilities } from "./capabilities.js";
|
|
|
13
13
|
import { accent, muted, emphasis } from "./atoms.js";
|
|
14
14
|
import { PACKAGE_VERSION } from "../version.js";
|
|
15
15
|
const TAGLINE = "map the project before the agent moves";
|
|
16
|
+
const BLOCK = [
|
|
17
|
+
` _| _| _| _| _|_| _| _|_|_|_| `,
|
|
18
|
+
` _| _| _| _| _| _| _| _| `,
|
|
19
|
+
` _| _| _| _|_|_|_| _|_|_|_| _| _|_|_| `,
|
|
20
|
+
` _| _| _| _| _| _| _| _| _| `,
|
|
21
|
+
` _| _| _| _| _| _| _|_|_|_| _|_|_|_| `,
|
|
22
|
+
].join("\n");
|
|
16
23
|
export function splash() {
|
|
17
24
|
const { color, unicode } = capabilities();
|
|
18
25
|
const isPlain = !unicode;
|
|
@@ -20,17 +27,15 @@ export function splash() {
|
|
|
20
27
|
if (isPlain) {
|
|
21
28
|
return `WHALE IGNITER v${PACKAGE_VERSION} — ${TAGLINE}`;
|
|
22
29
|
}
|
|
23
|
-
// Tier 1 & 2 — unicode layout (color applied via atom functions when available)
|
|
24
|
-
const whale = "◆ ~";
|
|
25
|
-
const wordmarkWhale = color ? accent("WHALE") : "WHALE";
|
|
26
30
|
const wordmarkIgniter = color ? emphasis("IGNITER") : "IGNITER";
|
|
27
31
|
const version = color ? muted(`v${PACKAGE_VERSION}`) : `v${PACKAGE_VERSION}`;
|
|
28
32
|
const tag = color ? muted(TAGLINE) : TAGLINE;
|
|
29
|
-
const
|
|
33
|
+
const title = color ? accent(BLOCK) : BLOCK;
|
|
34
|
+
const rule = "─".repeat(50);
|
|
30
35
|
return [
|
|
31
|
-
|
|
32
|
-
`${
|
|
36
|
+
title,
|
|
37
|
+
`${wordmarkIgniter} · ${version}`,
|
|
33
38
|
tag,
|
|
34
|
-
rule
|
|
39
|
+
rule,
|
|
35
40
|
].join("\n");
|
|
36
41
|
}
|
package/dist/utils/decisions.js
CHANGED
|
@@ -1,9 +1,60 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
|
-
import
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
const DIR = "intelligence/decisions";
|
|
5
|
+
const LEGACY_STORE = "intelligence/decisions.json";
|
|
6
|
+
function slugify(title) {
|
|
7
|
+
return title
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
10
|
+
.replace(/^-+|-+$/g, "")
|
|
11
|
+
.slice(0, 60);
|
|
12
|
+
}
|
|
13
|
+
function section(body, heading) {
|
|
14
|
+
const re = new RegExp(`^##\\s+${heading}\\s*$([\\s\\S]*?)(?=^##\\s+|(?![\\s\\S]))`, "im");
|
|
15
|
+
const match = body.match(re);
|
|
16
|
+
const text = match?.[1]?.trim();
|
|
17
|
+
return text && text.length > 0 ? text : undefined;
|
|
18
|
+
}
|
|
19
|
+
function parseFile(raw) {
|
|
20
|
+
const { data, content } = matter(raw);
|
|
21
|
+
const decision = section(content, "Decision");
|
|
22
|
+
if (!data.id || !data.title || !decision)
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
id: String(data.id),
|
|
26
|
+
timestamp: String(data.createdAt ?? data.timestamp ?? ""),
|
|
27
|
+
title: String(data.title),
|
|
28
|
+
category: (data.category ?? "architecture"),
|
|
29
|
+
context: section(content, "Context"),
|
|
30
|
+
decision,
|
|
31
|
+
consequences: section(content, "Consequences"),
|
|
32
|
+
status: (data.status ?? "active"),
|
|
33
|
+
supersedes: data.supersedes ? String(data.supersedes) : undefined
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function serialize(d) {
|
|
37
|
+
const fm = matter.stringify([
|
|
38
|
+
"## Context",
|
|
39
|
+
d.context ?? "",
|
|
40
|
+
"",
|
|
41
|
+
"## Decision",
|
|
42
|
+
d.decision,
|
|
43
|
+
"",
|
|
44
|
+
"## Consequences",
|
|
45
|
+
d.consequences ?? ""
|
|
46
|
+
].join("\n"), {
|
|
47
|
+
id: d.id,
|
|
48
|
+
title: d.title,
|
|
49
|
+
category: d.category,
|
|
50
|
+
status: d.status,
|
|
51
|
+
createdAt: d.timestamp,
|
|
52
|
+
supersedes: d.supersedes ?? null
|
|
53
|
+
});
|
|
54
|
+
return fm;
|
|
55
|
+
}
|
|
56
|
+
async function loadLegacy(target) {
|
|
57
|
+
const file = path.join(target, LEGACY_STORE);
|
|
7
58
|
if (!(await fs.pathExists(file)))
|
|
8
59
|
return [];
|
|
9
60
|
try {
|
|
@@ -13,20 +64,49 @@ export async function loadDecisions(target) {
|
|
|
13
64
|
return [];
|
|
14
65
|
}
|
|
15
66
|
}
|
|
16
|
-
export async function
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
await fs.
|
|
67
|
+
export async function loadDecisions(target) {
|
|
68
|
+
const dir = path.join(target, DIR);
|
|
69
|
+
const fromFiles = [];
|
|
70
|
+
if (await fs.pathExists(dir)) {
|
|
71
|
+
const entries = (await fs.readdir(dir)).filter((f) => f.endsWith(".md")).sort();
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const raw = await fs.readFile(path.join(dir, entry), "utf8");
|
|
74
|
+
const parsed = parseFile(raw);
|
|
75
|
+
if (parsed) {
|
|
76
|
+
parsed.sourcePath = `${DIR}/${entry}`;
|
|
77
|
+
fromFiles.push(parsed);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Dual read: surface legacy JSON entries that haven't been migrated yet.
|
|
82
|
+
const legacy = await loadLegacy(target);
|
|
83
|
+
const seen = new Set(fromFiles.map((d) => d.id));
|
|
84
|
+
for (const d of legacy) {
|
|
85
|
+
if (!seen.has(d.id))
|
|
86
|
+
fromFiles.push(d);
|
|
87
|
+
}
|
|
88
|
+
return fromFiles;
|
|
89
|
+
}
|
|
90
|
+
function nextId(existing) {
|
|
91
|
+
let max = 0;
|
|
92
|
+
for (const d of existing) {
|
|
93
|
+
const m = /^ADR-(\d+)$/.exec(d.id);
|
|
94
|
+
if (m)
|
|
95
|
+
max = Math.max(max, Number(m[1]));
|
|
96
|
+
}
|
|
97
|
+
return `ADR-${String(max + 1).padStart(4, "0")}`;
|
|
20
98
|
}
|
|
21
99
|
export async function appendDecision(target, input) {
|
|
22
|
-
const
|
|
100
|
+
const existing = await loadDecisions(target);
|
|
23
101
|
const decision = {
|
|
24
|
-
id:
|
|
25
|
-
timestamp: new Date().toISOString(),
|
|
102
|
+
id: nextId(existing),
|
|
103
|
+
timestamp: new Date().toISOString().slice(0, 10),
|
|
26
104
|
status: "active",
|
|
27
105
|
...input
|
|
28
106
|
};
|
|
29
|
-
|
|
30
|
-
await
|
|
107
|
+
const dir = path.join(target, DIR);
|
|
108
|
+
await fs.ensureDir(dir);
|
|
109
|
+
const file = path.join(dir, `${decision.id}-${slugify(decision.title)}.md`);
|
|
110
|
+
await fs.writeFile(file, serialize(decision));
|
|
31
111
|
return decision;
|
|
32
112
|
}
|
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
const DIR = "intelligence/refinements";
|
|
5
|
+
const LEGACY_STORE = "intelligence/refinements.json";
|
|
6
|
+
function slugify(text) {
|
|
7
|
+
return text
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
10
|
+
.replace(/^-+|-+$/g, "")
|
|
11
|
+
.slice(0, 60);
|
|
12
|
+
}
|
|
13
|
+
function parseFile(raw) {
|
|
14
|
+
const { data, content } = matter(raw);
|
|
15
|
+
const note = content.trim();
|
|
16
|
+
if (!data.id || !note)
|
|
17
|
+
return null;
|
|
18
|
+
const scope = {};
|
|
19
|
+
if (data.issueType)
|
|
20
|
+
scope.issueType = String(data.issueType);
|
|
21
|
+
if (data.selector)
|
|
22
|
+
scope.selector = String(data.selector);
|
|
23
|
+
if (data.file)
|
|
24
|
+
scope.file = String(data.file);
|
|
25
|
+
return {
|
|
26
|
+
id: String(data.id),
|
|
27
|
+
timestamp: String(data.createdAt ?? data.timestamp ?? ""),
|
|
28
|
+
note,
|
|
29
|
+
scope: Object.keys(scope).length > 0 ? scope : undefined
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function serialize(r) {
|
|
33
|
+
const fm = {
|
|
34
|
+
id: r.id,
|
|
35
|
+
createdAt: r.timestamp
|
|
36
|
+
};
|
|
37
|
+
if (r.scope?.issueType)
|
|
38
|
+
fm.issueType = r.scope.issueType;
|
|
39
|
+
if (r.scope?.selector)
|
|
40
|
+
fm.selector = r.scope.selector;
|
|
41
|
+
if (r.scope?.file)
|
|
42
|
+
fm.file = r.scope.file;
|
|
43
|
+
return matter.stringify(r.note, fm);
|
|
44
|
+
}
|
|
45
|
+
async function loadLegacy(target) {
|
|
46
|
+
const file = path.join(target, LEGACY_STORE);
|
|
7
47
|
if (!(await fs.pathExists(file)))
|
|
8
48
|
return [];
|
|
9
49
|
try {
|
|
@@ -13,16 +53,49 @@ export async function loadRefinements(target) {
|
|
|
13
53
|
return [];
|
|
14
54
|
}
|
|
15
55
|
}
|
|
16
|
-
export async function
|
|
17
|
-
const dir = path.join(target,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
56
|
+
export async function loadRefinements(target) {
|
|
57
|
+
const dir = path.join(target, DIR);
|
|
58
|
+
const fromFiles = [];
|
|
59
|
+
if (await fs.pathExists(dir)) {
|
|
60
|
+
const entries = (await fs.readdir(dir)).filter((f) => f.endsWith(".md")).sort();
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const raw = await fs.readFile(path.join(dir, entry), "utf8");
|
|
63
|
+
const parsed = parseFile(raw);
|
|
64
|
+
if (parsed)
|
|
65
|
+
fromFiles.push({ ...parsed, sourcePath: `${DIR}/${entry}` });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Dual read: surface legacy JSON entries that haven't been migrated yet.
|
|
69
|
+
const legacy = await loadLegacy(target);
|
|
70
|
+
const seen = new Set(fromFiles.map((r) => r.id));
|
|
71
|
+
for (const r of legacy) {
|
|
72
|
+
if (!seen.has(r.id))
|
|
73
|
+
fromFiles.push(r);
|
|
74
|
+
}
|
|
75
|
+
return fromFiles;
|
|
21
76
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
77
|
+
function nextId(existing) {
|
|
78
|
+
let max = 0;
|
|
79
|
+
for (const r of existing) {
|
|
80
|
+
const m = /^REF-(\d+)$/.exec(r.id);
|
|
81
|
+
if (m)
|
|
82
|
+
max = Math.max(max, Number(m[1]));
|
|
83
|
+
}
|
|
84
|
+
return `REF-${String(max + 1).padStart(4, "0")}`;
|
|
85
|
+
}
|
|
86
|
+
export async function appendRefinement(target, input) {
|
|
87
|
+
const existing = await loadRefinements(target);
|
|
88
|
+
const refinement = {
|
|
89
|
+
id: nextId(existing),
|
|
90
|
+
timestamp: new Date().toISOString().slice(0, 10),
|
|
91
|
+
note: input.note,
|
|
92
|
+
scope: input.scope
|
|
93
|
+
};
|
|
94
|
+
const dir = path.join(target, DIR);
|
|
95
|
+
await fs.ensureDir(dir);
|
|
96
|
+
const file = path.join(dir, `${refinement.id}-${slugify(refinement.note)}.md`);
|
|
97
|
+
await fs.writeFile(file, serialize(refinement));
|
|
98
|
+
return refinement;
|
|
26
99
|
}
|
|
27
100
|
// Lightweight natural-language extractor. Recognises hints like:
|
|
28
101
|
// "Button radius should remain 2px for controls" -> issueType: radius-consistency
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = "1.
|
|
1
|
+
export const PACKAGE_VERSION = "1.5.2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whale-igniter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "CLI-first operational intelligence. Bootstraps and adopts AI-readable project context so agents like Claude Code, Codex and Cursor understand your design system from the first commit. Includes an MCP server, a file watcher, deterministic insights, and an opt-in AI bridge (Selene).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"dev": "tsx src/index.ts",
|
|
23
23
|
"build": "tsc",
|
|
24
|
+
"postbuild": "node -e \"require('node:fs').chmodSync('dist/index.js', 0o755)\"",
|
|
24
25
|
"start": "node dist/index.js",
|
|
25
26
|
"test": "tsx tests/run.ts && tsx tests/mcp-smoke.ts && tsx tests/extractors.ts && tsx tests/driftAnalyzers.ts",
|
|
26
27
|
"prepublishOnly": "npm run build && npm test"
|
|
@@ -64,7 +65,8 @@
|
|
|
64
65
|
"chalk": "^5.3.0",
|
|
65
66
|
"commander": "^12.1.0",
|
|
66
67
|
"fs-extra": "^11.2.0",
|
|
67
|
-
"glob": "^
|
|
68
|
+
"glob": "^13.0.6",
|
|
69
|
+
"gray-matter": "^4.0.3",
|
|
68
70
|
"ora": "^8.1.0",
|
|
69
71
|
"postcss": "^8.4.47",
|
|
70
72
|
"prompts": "^2.4.2",
|