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.
@@ -1,12 +1,21 @@
1
- const MAX_DETAIL = 500;
2
- function truncate(text) {
3
- return text.length > MAX_DETAIL ? text.slice(0, MAX_DETAIL) + "…" : text;
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
- "Architectural, product and tooling decisions, most recent first.",
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(`## ${d.title}`);
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 rule = "─".repeat(36);
33
+ const title = color ? accent(BLOCK) : BLOCK;
34
+ const rule = "─".repeat(50);
30
35
  return [
31
- `${whale}`,
32
- `${wordmarkWhale} ${wordmarkIgniter} ${version}`,
36
+ title,
37
+ `${wordmarkIgniter} · ${version}`,
33
38
  tag,
34
- rule
39
+ rule,
35
40
  ].join("\n");
36
41
  }
@@ -1,9 +1,60 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
- import { randomUUID } from "node:crypto";
4
- const STORE = "intelligence/decisions.json";
5
- export async function loadDecisions(target) {
6
- const file = path.join(target, STORE);
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 saveDecisions(target, decisions) {
17
- const file = path.join(target, STORE);
18
- await fs.ensureDir(path.dirname(file));
19
- await fs.writeJson(file, decisions, { spaces: 2 });
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 decisions = await loadDecisions(target);
100
+ const existing = await loadDecisions(target);
23
101
  const decision = {
24
- id: randomUUID(),
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
- decisions.push(decision);
30
- await saveDecisions(target, decisions);
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
- const STORE_FILENAME = "refinements.json";
4
- const STORE_DIR = "intelligence";
5
- export async function loadRefinements(target) {
6
- const file = path.join(target, STORE_DIR, STORE_FILENAME);
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 saveRefinements(target, refinements) {
17
- const dir = path.join(target, STORE_DIR);
18
- await fs.ensureDir(dir);
19
- const file = path.join(dir, STORE_FILENAME);
20
- await fs.writeJson(file, refinements, { spaces: 2 });
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
- export async function appendRefinement(target, refinement) {
23
- const current = await loadRefinements(target);
24
- current.push(refinement);
25
- await saveRefinements(target, current);
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.4.0";
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.4.0",
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": "^10.4.5",
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",