loki-mode 10.8.0 → 10.9.1
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/README.md +2 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +16 -31
- package/autonomy/tui.sh +5 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_phases.py +1 -1
- package/dashboard/api_start.py +0 -1
- package/dashboard/server.py +22 -138
- package/docs/COCKPIT-SPEC.md +3 -1
- package/docs/DASHBOARD-9.12-EVIDENCE.md +1 -1
- package/docs/DASHBOARD-ARCHITECTURE.md +13 -11
- package/docs/DASHBOARD_V2_CHECKLIST.md +2 -0
- package/docs/INSTALLATION.md +12 -11
- package/docs/MERGE-DEDUP-MAP.md +1 -1
- package/docs/MERGE-ROUTE-MAP.md +1 -1
- package/docs/MERGE3-PLAN.md +2 -2
- package/docs/R3-COST-OBSERVABILITY-DESIGN.md +2 -2
- package/docs/R4-TRUST-TRAJECTORY-DESIGN.md +1 -1
- package/docs/R5-AUTO-WIKI-DESIGN.md +1 -1
- package/docs/R6-ROLLBACK-CHECKPOINT-PLAN.md +1 -1
- package/docs/RARV-C-100X-PLAN.md +3 -3
- package/docs/SONNET5-DEFAULT-PLAN.md +2 -2
- package/docs/SYNERGY-TASKS.md +1 -1
- package/docs/TOOL-INTEGRATION.md +1 -1
- package/docs/TOP-100-BACKLOG.md +1 -1
- package/docs/alternative-installations.md +1 -1
- package/docs/architecture/ADR-001-runtime-migration.md +1 -1
- package/docs/architecture/DASHBOARD_V2_ARCHITECTURE.md +2 -0
- package/docs/audit-logging.md +2 -0
- package/docs/authentication.md +2 -0
- package/docs/authorization.md +2 -0
- package/docs/certification/02-enterprise-features/lab.md +2 -0
- package/docs/certification/02-enterprise-features/lesson.md +2 -0
- package/docs/certification/04-production-deployment/lab.md +2 -0
- package/docs/certification/04-production-deployment/lesson.md +2 -0
- package/docs/control-plane-migration.md +97 -0
- package/docs/dashboard-guide.md +6 -7
- package/docs/dev/release-checklist.md +9 -11
- package/docs/enterprise/integration-cookbook.md +2 -0
- package/docs/enterprise/migration.md +1 -1
- package/docs/enterprise/performance.md +3 -1
- package/docs/enterprise/sdk-guide.md +2 -0
- package/docs/enterprise/security.md +2 -0
- package/docs/plans/FORGE-AUTONOMOUS-QUEUE.md +2 -2
- package/docs/research-2026-07/OBSERVABILITY-TOOL-SPEC.md +1 -1
- package/docs/retrospectives/v7.5.15-honesty-audit.md +2 -2
- package/docs/test-scenarios/enterprise-scenarios.md +2 -2
- package/docs/v10/ARCHITECT-CUTS.md +9 -9
- package/docs/v10/BACKLOG.md +7 -7
- package/docs/v10/BOARD.md +25 -25
- package/docs/v10/CONTROL-PLANE.md +4 -4
- package/docs/v10/CP-ENTERPRISE-UI.md +66 -66
- package/docs/v10/D51-PHASE-B.md +1 -1
- package/docs/v10/DECISIONS.md +3 -3
- package/docs/v10/DEPS.md +10 -10
- package/docs/v10/FAILURE-CLASSES.md +45 -1
- package/docs/v10/LEGACY-REMOVAL.md +2 -2
- package/docs/v10/SWARM.md +2 -2
- package/loki-ts/dist/cockpit.js +11 -11
- package/loki-ts/dist/loki.js +596 -579
- package/magic/core/design_tokens.py +1 -2
- package/magic/core/generator.py +2 -2
- package/magic/tokens/README.md +1 -3
- package/mcp/__init__.py +1 -1
- package/package.json +2 -7
- package/packages/control-plane/dist/server.js +1683 -843
- package/packages/control-plane/src/server/legacy/routes-audit.ts +50 -0
- package/packages/control-plane/src/server/legacy/routes-control.ts +26 -0
- package/packages/control-plane/src/server/legacy/routes-memory.ts +30 -0
- package/packages/control-plane/src/server/legacy/routes.ts +58 -58
- package/packages/control-plane/src/server/legacy/shim.ts +10 -4
- package/packages/control-plane/src/server/routes/checkpoints.ts +171 -0
- package/packages/control-plane/src/server/routes/index.ts +4 -1
- package/packages/control-plane/src/server/routes/memory.ts +219 -0
- package/packages/control-plane/src/server/routes/session_control.ts +155 -0
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/magic-modules-patterns.md +2 -2
- package/skills/factory-operations.md +3 -3
- package/skills/magic-modules.md +1 -1
- package/skills/release-cadence.md +1 -1
- package/tools/audit-docs.py +1 -1
- package/tools/index-codebase.py +1 -1
- package/web-app/dist/assets/{AdminPage-DDDvUz0N.js → AdminPage-DqMnfZrH.js} +1 -1
- package/web-app/dist/assets/{Avatar-Dcdz46i6.js → Avatar-BEof29du.js} +1 -1
- package/web-app/dist/assets/{Badge-BB24cCpK.js → Badge-WD53OL5m.js} +1 -1
- package/web-app/dist/assets/{Button-D0-DmPZL.js → Button-Rzk5flhU.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-CdjpU2JW.js → CockpitPage-Bqz3PCMh.js} +1 -1
- package/web-app/dist/assets/{ComparePage-DT5U0y0d.js → ComparePage-Byo2NzAZ.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-lEHcl0EO.js → ErrorBoundary-BuE0kH3W.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-GpnWvdCr.js → EvidenceReceiptPanel-CKFHOPBx.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-C7VOh1B7.js → GitHubIssuesPanel-Df_kTMqI.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-G_kOuINv.js → GitHubPRsPanel-CouvcKrZ.js} +1 -1
- package/web-app/dist/assets/{HomePage-PqRisOny.js → HomePage-dEpLzj0x.js} +2 -2
- package/web-app/dist/assets/{LoginPage-DU7pFAkf.js → LoginPage-6DVjUND5.js} +1 -1
- package/web-app/dist/assets/{MagicPage-C5JWIWMk.js → MagicPage-BDQ9MOfY.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CbIGS_qo.js → MetricsPage-CXiJ04G_.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-vU1pvdSt.js → NotFoundPage-BaXhZvyj.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-SsixIPPv.js → ProjectPage-BzE4V7Ti.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-BfQKSR9k.js → ProjectsPage-ipf6TLMX.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-B8YfXAom.js → SettingsPage-BDxkUKN6.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-Ba407f2-.js → ShowcasePage-YQXD72SM.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-DzNQhhBS.js → SystemSettingsPage-Dou7zHS0.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-tqUnpZ15.js → TeamsPage-CQarfcYr.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-ClB5EpI3.js → TemplatesPage-CPi1R8Xj.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-CBvpYfEu.js → TerminalOutput-WiGhWdYD.js} +1 -1
- package/web-app/dist/assets/{activity-C1s0L_X9.js → activity-DOa7ejQA.js} +1 -1
- package/web-app/dist/assets/{bell-DMEiXr73.js → bell-CX-VAlYN.js} +1 -1
- package/web-app/dist/assets/{bot-BF_xIOxu.js → bot-EDXaZmHc.js} +1 -1
- package/web-app/dist/assets/{check-Dv1Ynvrs.js → check-CnBdKk3L.js} +1 -1
- package/web-app/dist/assets/{chevron-left-Ddhsod7S.js → chevron-left-BJ_ydRQW.js} +1 -1
- package/web-app/dist/assets/{circle-alert-OATHUkBZ.js → circle-alert-tz8ufhc_.js} +1 -1
- package/web-app/dist/assets/{clock-C1Ltva07.js → clock-DFBZPSsH.js} +1 -1
- package/web-app/dist/assets/{cloud-B-S4oOy9.js → cloud-uy3p_5Q7.js} +1 -1
- package/web-app/dist/assets/{code-xml-D3Femn_F.js → code-xml-3CbYD9YL.js} +1 -1
- package/web-app/dist/assets/{database-DBSeFgO3.js → database-YiNBXc9c.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-sgDgNuGE.js → dollar-sign-D0wBa4Cv.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-C-MbdZEc.js → file-code-corner-DDdZVZ5v.js} +1 -1
- package/web-app/dist/assets/{file-plus-CCB6yqXv.js → file-plus-D09LTBqt.js} +1 -1
- package/web-app/dist/assets/{globe-DlbrNAj1.js → globe-BirTPwff.js} +1 -1
- package/web-app/dist/assets/{hammer-B-DWm695.js → hammer-DaE5qgnd.js} +1 -1
- package/web-app/dist/assets/{index-Dvny-1SU.js → index-D8t7KnQf.js} +3 -3
- package/web-app/dist/assets/{layers-99qsKpOT.js → layers-DVVrckRY.js} +1 -1
- package/web-app/dist/assets/{loader-circle-ZvZyZl3J.js → loader-circle-2-nMGmcb.js} +1 -1
- package/web-app/dist/assets/{lock-DosGjcxP.js → lock-D0Xj43pC.js} +1 -1
- package/web-app/dist/assets/{package-C-PumesN.js → package-BB7S3egm.js} +1 -1
- package/web-app/dist/assets/{plus-CTYYskTB.js → plus-BLhoMrn3.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-qW2ztTFS.js → refresh-cw-DMc2uPLw.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-ZIKrf-vO.js → rotate-ccw-DM3IP9x7.js} +1 -1
- package/web-app/dist/assets/{scroll-text-BamlXjAT.js → scroll-text-lH2uW7rB.js} +1 -1
- package/web-app/dist/assets/{server-BljXb79I.js → server-BkerZcdR.js} +1 -1
- package/web-app/dist/assets/{shield-alert-Dr6eT9c_.js → shield-alert-B8ohcUfu.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-v3oBXZgU.js → thumbs-up-CTIxLzPD.js} +1 -1
- package/web-app/dist/assets/{trash-2-BOuIrCpi.js → trash-2-C3zIpsH5.js} +1 -1
- package/web-app/dist/assets/{trending-down-Dp7jMCgH.js → trending-down-BTT7wV4Y.js} +1 -1
- package/web-app/dist/assets/{trending-up-CDiur-nP.js → trending-up-BzHdCDvF.js} +1 -1
- package/web-app/dist/assets/{upload-DxIHXhNE.js → upload-HxR34-A8.js} +1 -1
- package/web-app/dist/assets/{usePolling-D2Uu-YZb.js → usePolling-DDj6Fupy.js} +1 -1
- package/web-app/dist/assets/{user-BVWCUAfF.js → user-qH5y9yNy.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/dashboard/static/assets/mermaid.min.js +0 -2030
- package/dashboard/static/cost.html +0 -344
- package/dashboard/static/favicon.svg +0 -5
- package/dashboard/static/index.html +0 -15577
- package/dashboard/static/proofs.html +0 -194
- package/dashboard/static/start.html +0 -131
- package/dashboard/static/trust.html +0 -325
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// CPE24-P3: memory, context, focus and tasks, ported from dashboard/server.py (list_tasks, set_focus, list_episodes, list_patterns, list_skills,
|
|
2
|
+
// get_token_economics, get_context). Same .loki/ flat-file sources; the legacy SQLite memory backend and the SQL task store have no CP equivalent, so
|
|
3
|
+
// those paths are not ported (see docs/v10/CP-ENTERPRISE-UI.md). Unmeasured values are null, never a fabricated zero.
|
|
4
|
+
// Every path read is derived from the focused project dir plus a fixed suffix; a request value is only ever compared to an id, never joined into a path.
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
6
|
+
import type { Dirent } from "node:fs";
|
|
7
|
+
import type { Context } from "hono";
|
|
8
|
+
import { join, sep } from "node:path";
|
|
9
|
+
import { isLoopbackHost, peerIsLoopback } from "../auth.ts";
|
|
10
|
+
import { probeAllowed, type RouteCtx } from "./index.ts";
|
|
11
|
+
import { lokiDirOf, readJson } from "./cost_ledger.ts";
|
|
12
|
+
import { registryFile } from "./fleet.ts";
|
|
13
|
+
|
|
14
|
+
type Rec = Record<string, unknown>;
|
|
15
|
+
const isObj = (v: unknown): v is Rec => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16
|
+
const nonEmpty = (v: unknown) => !(v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0) || (isObj(v) && Object.keys(v).length === 0));
|
|
17
|
+
|
|
18
|
+
/** The focused project dir (POST /v1/focus); null means the server's own checkout. Process-wide, like the legacy _active_project_dir. */
|
|
19
|
+
export const focus: { dir: string | null } = { dir: null };
|
|
20
|
+
export const lokiFor = (repoDir: string) => lokiDirOf(focus.dir ?? repoDir);
|
|
21
|
+
|
|
22
|
+
/** State-changing guard: loopback peer and Host, JSON content type, and a browser Origin (when sent) equal to the Host. */
|
|
23
|
+
export function mutationAllowed(c: Context): boolean {
|
|
24
|
+
if (!peerIsLoopback(c) || !isLoopbackHost(c.req.header("host"))) return false;
|
|
25
|
+
if (!(c.req.header("content-type") ?? "").toLowerCase().startsWith("application/json")) return false;
|
|
26
|
+
const origin = c.req.header("origin");
|
|
27
|
+
if (origin === undefined) return true;
|
|
28
|
+
try { return new URL(origin).host.toLowerCase() === (c.req.header("host") ?? "").toLowerCase(); } catch { return false; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Every .json file under dir (sorted), never following symlinks, so nothing resolves outside dir. */
|
|
32
|
+
function jsonFiles(dir: string, cap = Infinity): string[] {
|
|
33
|
+
const out: string[] = [];
|
|
34
|
+
const walk = (d: string) => {
|
|
35
|
+
let ents: Dirent[] = [];
|
|
36
|
+
try { ents = readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
37
|
+
for (const e of ents.sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
38
|
+
if (out.length >= cap) return;
|
|
39
|
+
const p = join(d, e.name);
|
|
40
|
+
if (e.isDirectory()) walk(p); else if (e.isFile() && e.name.endsWith(".json")) out.push(p);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
walk(dir);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function focusInfo(repoDir: string) { return { project_dir: focus.dir, loki_dir: lokiFor(repoDir) }; }
|
|
48
|
+
|
|
49
|
+
/** A focus target must be this checkout or a project in the machine registry, and hold a .loki dir; `..` and relative paths are refused. */
|
|
50
|
+
export function setFocus(repoDir: string, raw: unknown, registry = registryFile()): { ok: true } | { ok: false; error: string } {
|
|
51
|
+
const s = typeof raw === "string" ? raw.trim() : "";
|
|
52
|
+
if (!s) return { ok: false, error: "project_dir must not be empty" };
|
|
53
|
+
if (!s.startsWith("/") || s.split(/[\\/]/).includes("..")) return { ok: false, error: "project_dir must be an absolute path without .. segments" };
|
|
54
|
+
let real = "";
|
|
55
|
+
try { real = realpathSync(s); if (!statSync(real).isDirectory()) throw new Error("not a dir"); } catch { return { ok: false, error: `Directory does not exist: ${s}` }; }
|
|
56
|
+
if (!existsSync(join(real, ".loki")) || !statSync(join(real, ".loki")).isDirectory()) return { ok: false, error: `Directory does not contain a .loki/ subdirectory: ${s}` };
|
|
57
|
+
const allowed = new Set<string>();
|
|
58
|
+
try { allowed.add(realpathSync(repoDir)); } catch { /* unreadable */ }
|
|
59
|
+
const reg = readJson(registry);
|
|
60
|
+
if (isObj(reg) && isObj(reg.projects)) for (const p of Object.values(reg.projects)) {
|
|
61
|
+
if (isObj(p) && typeof p.path === "string") { try { allowed.add(realpathSync(p.path)); } catch { /* gone */ } }
|
|
62
|
+
}
|
|
63
|
+
if (!allowed.has(real)) return { ok: false, error: "project_dir is not this checkout or a registered project" };
|
|
64
|
+
focus.dir = real;
|
|
65
|
+
return { ok: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---- tasks (dashboard-state.json groups plus .loki/queue/*.json), deduped with the legacy terminal-wins rule ----
|
|
69
|
+
const GROUPS: [string, string][] = [["pending", "pending"], ["inProgress", "in_progress"], ["review", "review"], ["completed", "done"], ["failed", "done"]];
|
|
70
|
+
const QUEUES: [string, string][] = [["pending.json", "pending"], ["in-progress.json", "in_progress"], ["completed.json", "done"], ["failed.json", "done"], ["dead-letter.json", "done"]];
|
|
71
|
+
const RANK: Record<string, number> = { done: 3, in_progress: 2, review: 1, pending: 0 };
|
|
72
|
+
|
|
73
|
+
export function tasksList(loki: string, status?: string): Rec[] {
|
|
74
|
+
const all: Rec[] = [];
|
|
75
|
+
const st = readJson(join(loki, "dashboard-state.json"));
|
|
76
|
+
const groups = isObj(st) && isObj(st.tasks) ? st.tasks : {};
|
|
77
|
+
for (const [key, mapped] of GROUPS) {
|
|
78
|
+
const g = groups[key];
|
|
79
|
+
if (!Array.isArray(g)) continue;
|
|
80
|
+
g.forEach((t, i) => {
|
|
81
|
+
if (!isObj(t)) return;
|
|
82
|
+
const payload = isObj(t.payload) ? t.payload : {};
|
|
83
|
+
const e: Rec = { id: t.id ?? `${key}-${i}`, title: t.title ?? payload.action ?? t.type ?? "Task", description: t.description ?? payload.description ?? "", status: mapped, priority: t.priority ?? payload.priority ?? "medium", type: t.type ?? "task", position: i };
|
|
84
|
+
for (const f of ["acceptance_criteria", "notes", "logs", "user_story", "project", "source", "specification", "provider", "startedAt", "full_content", "completedAt", "phase", "exitCode"]) if (nonEmpty(t[f])) e[f] = t[f];
|
|
85
|
+
if (t.exitCode === 0) e.exitCode = 0;
|
|
86
|
+
if (key === "completed" || key === "failed") e._terminal_outcome = key;
|
|
87
|
+
all.push(e);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
for (const [file, qs] of QUEUES) {
|
|
91
|
+
const raw = readJson(join(loki, "queue", file));
|
|
92
|
+
const items = isObj(raw) ? raw.tasks : raw;
|
|
93
|
+
if (!Array.isArray(items)) continue;
|
|
94
|
+
const term = file === "completed.json" ? "completed" : file === "failed.json" || file === "dead-letter.json" ? "failed" : null;
|
|
95
|
+
items.forEach((it, i) => {
|
|
96
|
+
if (!isObj(it)) return;
|
|
97
|
+
const id = it.id ?? `q-${qs}-${i}`;
|
|
98
|
+
if (term === null && all.some((t) => t.id === id)) return;
|
|
99
|
+
const e: Rec = { id, title: it.title ?? it.action ?? "Task", description: it.description ?? "", status: qs, priority: it.priority ?? "medium", type: it.type ?? "task", position: i };
|
|
100
|
+
for (const f of ["acceptance_criteria", "user_story", "project", "source", "provider", "startedAt", "completedAt", "phase"]) if (nonEmpty(it[f])) e[f] = it[f];
|
|
101
|
+
for (const f of ["notes", "logs"]) if (Array.isArray(it[f])) e[f] = it[f];
|
|
102
|
+
if (Number.isInteger(it.exitCode)) e.exitCode = it.exitCode;
|
|
103
|
+
if (term) e._terminal_outcome = term;
|
|
104
|
+
all.push(e);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const auth = (t: Rec) => [RANK[t.status as string] ?? -1, t._terminal_outcome === "failed" ? 1 : 0] as const;
|
|
108
|
+
const better = (a: Rec, b: Rec) => { const x = auth(a), y = auth(b); return x[0] !== y[0] ? x[0] > y[0] : x[1] > y[1]; };
|
|
109
|
+
const byId = new Map<unknown, Rec>(), order: (Rec | { ref: unknown })[] = [];
|
|
110
|
+
for (const t of all) {
|
|
111
|
+
if (t.id === undefined || t.id === null) { order.push(t); continue; }
|
|
112
|
+
const prev = byId.get(t.id);
|
|
113
|
+
if (!prev) { byId.set(t.id, t); order.push({ ref: t.id }); } else if (better(t, prev)) byId.set(t.id, t);
|
|
114
|
+
}
|
|
115
|
+
const out = order.map((o) => ("ref" in o ? (byId.get(o.ref) as Rec) : o));
|
|
116
|
+
for (const t of out) {
|
|
117
|
+
if (t.type === "iteration" && !t.description) {
|
|
118
|
+
const ec = t.exitCode, failed = t._terminal_outcome === "failed";
|
|
119
|
+
const o = Number.isInteger(ec) && ec === 0 && !failed ? "completed cleanly (exit 0)" : Number.isInteger(ec) && ec !== 0 ? `failed (exit ${ec})` : failed ? "failed"
|
|
120
|
+
: t._terminal_outcome === "completed" ? "completed" : t.status === "done" ? "finished" : null;
|
|
121
|
+
if (o) t.description = `Iteration ${o}${t.provider ? `, built by ${t.provider}` : ""}.`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return status ? out.filter((t) => t.status === status) : out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---- memory (.loki/memory JSON store) ----
|
|
128
|
+
const memDir = (loki: string, sub: string) => join(loki, "memory", sub);
|
|
129
|
+
|
|
130
|
+
export function memoryEpisodes(loki: string, limit = 50): Rec[] {
|
|
131
|
+
const eps = jsonFiles(memDir(loki, "episodic")).map(readJson).filter(isObj);
|
|
132
|
+
eps.sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
|
|
133
|
+
return eps.slice(0, limit);
|
|
134
|
+
}
|
|
135
|
+
export function memoryEpisode(loki: string, id: string): Rec | null {
|
|
136
|
+
for (const f of jsonFiles(memDir(loki, "episodic"))) {
|
|
137
|
+
const d = readJson(f);
|
|
138
|
+
if (isObj(d) && (d.id === id || f.split(sep).pop() === `${id}.json`)) return d;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
export function memoryPatterns(loki: string): Rec[] {
|
|
143
|
+
const d = readJson(join(memDir(loki, "semantic"), "patterns.json"));
|
|
144
|
+
const list = Array.isArray(d) ? d : isObj(d) && Array.isArray(d.patterns) ? d.patterns : [];
|
|
145
|
+
return list.filter(isObj);
|
|
146
|
+
}
|
|
147
|
+
export const memorySkills = (loki: string): Rec[] => jsonFiles(memDir(loki, "skills")).map(readJson).filter(isObj);
|
|
148
|
+
|
|
149
|
+
export function memoryEconomics(loki: string) {
|
|
150
|
+
const rawV = readJson(join(loki, "memory", "token_economics.json"));
|
|
151
|
+
const raw: Rec = isObj(rawV) ? rawV : {};
|
|
152
|
+
const m = isObj(raw.metrics) ? raw.metrics : {};
|
|
153
|
+
const n = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : 0);
|
|
154
|
+
const hits = n(m.cache_hits), misses = n(m.cache_misses), total = hits + misses;
|
|
155
|
+
const recorded = Object.keys(m).length > 0;
|
|
156
|
+
const disc = recorded ? n(m.discovery_tokens) : null, read = recorded ? n(m.read_tokens) : null;
|
|
157
|
+
const cands: Rec[] = [];
|
|
158
|
+
for (const kind of ["episodic", "semantic"]) {
|
|
159
|
+
for (const f of jsonFiles(memDir(loki, kind), 300)) {
|
|
160
|
+
const d = readJson(f);
|
|
161
|
+
if (!isObj(d)) continue;
|
|
162
|
+
const ctx = isObj(d.context) ? d.context : {};
|
|
163
|
+
cands.push({ id: d.id ?? "", kind, access_count: n(d.access_count), importance: Number(d.importance) || 0, summary: String(d.summary || d.pattern || ctx.goal || "").slice(0, 160) });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
cands.sort((a, b) => (b.access_count as number) - (a.access_count as number) || (b.importance as number) - (a.importance as number));
|
|
167
|
+
return {
|
|
168
|
+
session_id: raw.session_id ?? null, discovery_tokens: disc, read_tokens: read, total_tokens: recorded ? (disc as number) + (read as number) : null,
|
|
169
|
+
cache_hits: hits, cache_misses: misses, hit_rate: total > 0 ? Math.round((hits / total) * 1e4) / 1e4 : null, ratio: raw.ratio ?? null,
|
|
170
|
+
savings_percent: raw.savings_percent ?? null, top_patterns: cands.slice(0, 10).filter((p) => (p.access_count as number) > 0),
|
|
171
|
+
discoveryTokens: disc, readTokens: read, savingsPercent: raw.savings_percent ?? null, raw,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** .loki/context/tracking.json; absent means the tracker never ran, so every reading is null. A file that exists but is unreadable is an error, not a zero. */
|
|
176
|
+
export function contextTracking(loki: string): { status: 200 | 500; body: unknown } {
|
|
177
|
+
const f = join(loki, "context", "tracking.json");
|
|
178
|
+
if (!existsSync(f)) {
|
|
179
|
+
return { status: 200, body: {
|
|
180
|
+
session_id: null, updated_at: null,
|
|
181
|
+
current: { input_tokens: null, output_tokens: null, cache_read_tokens: null, cache_creation_tokens: null, total_tokens: null, context_window_pct: null, estimated_cost_usd: null },
|
|
182
|
+
compactions: [], per_iteration: [],
|
|
183
|
+
totals: { total_input: null, total_output: null, total_cost_usd: null, compaction_count: null, iterations_tracked: null },
|
|
184
|
+
} };
|
|
185
|
+
}
|
|
186
|
+
try { return { status: 200, body: JSON.parse(readFileSync(f, "utf8")) }; } catch { return { status: 500, body: { error: "Failed to read context tracking data" } }; }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function mount(ctx: RouteCtx): void {
|
|
190
|
+
const deny = { error: "loopback only without a token" };
|
|
191
|
+
const read = (path: string, fn: (c: Context, loki: string) => Response) =>
|
|
192
|
+
ctx.app.get(path, (c) => (probeAllowed(ctx, c) ? fn(c, lokiFor(ctx.repoDir)) : c.json(deny, 403)));
|
|
193
|
+
read("/v1/tasks", (c, l) => {
|
|
194
|
+
if (c.req.query("project_id") !== undefined) return c.json({ error: "project_id filter is not supported by the Control Plane" }, 400);
|
|
195
|
+
return c.json(tasksList(l, c.req.query("status")));
|
|
196
|
+
});
|
|
197
|
+
read("/v1/memory/episodes", (c, l) => {
|
|
198
|
+
const lim = c.req.query("limit") === undefined ? 50 : Number(c.req.query("limit"));
|
|
199
|
+
return Number.isInteger(lim) && lim >= 1 && lim <= 1000 ? c.json(memoryEpisodes(l, lim)) : c.json({ error: "limit must be 1..1000" }, 422);
|
|
200
|
+
});
|
|
201
|
+
read("/v1/memory/episodes/:id", (c, l) => { const e = memoryEpisode(l, c.req.param("id")); return e ? c.json(e) : c.json({ error: "Episode not found" }, 404); });
|
|
202
|
+
read("/v1/memory/patterns", (c, l) => c.json(memoryPatterns(l)));
|
|
203
|
+
read("/v1/memory/patterns/:id", (c, l) => { const p = memoryPatterns(l).find((x) => x.id === c.req.param("id")); return p ? c.json(p) : c.json({ error: "Pattern not found" }, 404); });
|
|
204
|
+
read("/v1/memory/skills", (c, l) => c.json(memorySkills(l)));
|
|
205
|
+
read("/v1/memory/economics", (c, l) => c.json(memoryEconomics(l)));
|
|
206
|
+
read("/v1/context", (c, l) => { const r = contextTracking(l); return c.json(r.body, r.status); });
|
|
207
|
+
read("/v1/focus", (c) => c.json(focusInfo(ctx.repoDir)));
|
|
208
|
+
ctx.act.post("/v1/focus", async (c) => {
|
|
209
|
+
if (!mutationAllowed(c)) return c.json({ error: "loopback JSON requests only" }, 403);
|
|
210
|
+
const body = await c.req.json().catch(() => null);
|
|
211
|
+
const r = setFocus(ctx.repoDir, isObj(body) ? body.project_dir : undefined);
|
|
212
|
+
return r.ok ? c.json(focusInfo(ctx.repoDir)) : c.json({ error: r.error }, 400);
|
|
213
|
+
});
|
|
214
|
+
ctx.act.delete("/v1/focus", (c) => {
|
|
215
|
+
if (!mutationAllowed(c)) return c.json({ error: "loopback JSON requests only" }, 403);
|
|
216
|
+
focus.dir = null;
|
|
217
|
+
return c.json(focusInfo(ctx.repoDir));
|
|
218
|
+
});
|
|
219
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// CPE24-P4: session control and the Completion Council, ported from dashboard/server.py (/api/control/{pause,resume,stop}, /api/council/*).
|
|
2
|
+
// Trust (HIGH tier): control accepts only the fixed ACTIONS enum, never a command string, and writes only the signal files run.sh already
|
|
3
|
+
// reads (.loki/PAUSE, .loki/STOP, .loki/signals/COUNCIL_REVIEW_REQUESTED) holding a timestamp. It spawns and signals nothing.
|
|
4
|
+
// Council reads are read-only, take no path from the caller, and cap what they return.
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import type { Context } from "hono";
|
|
8
|
+
import { audit } from "../audit.ts";
|
|
9
|
+
import type { Db } from "../../db/migrate.ts";
|
|
10
|
+
import { probeAllowed, type RouteCtx } from "./index.ts";
|
|
11
|
+
import { pidAlive } from "./fleet.ts";
|
|
12
|
+
import { lokiDirOf } from "./cost_ledger.ts";
|
|
13
|
+
|
|
14
|
+
type Rec = Record<string, unknown>;
|
|
15
|
+
const isObj = (v: unknown): v is Rec => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16
|
+
|
|
17
|
+
export const ACTIONS = ["pause", "resume", "stop", "council-review"] as const;
|
|
18
|
+
export type ControlAction = (typeof ACTIONS)[number];
|
|
19
|
+
export const isAction = (v: string): v is ControlAction => (ACTIONS as readonly string[]).includes(v);
|
|
20
|
+
|
|
21
|
+
/** A browser Origin, when present, must be this server's own host; a cross-site fetch is refused. Absent Origin (curl, the CLI, the extension host) is allowed. */
|
|
22
|
+
export function sameOrigin(c: Context): boolean {
|
|
23
|
+
if (c.req.header("sec-fetch-site") === "cross-site") return false;
|
|
24
|
+
const origin = c.req.header("origin");
|
|
25
|
+
if (origin === undefined) return true;
|
|
26
|
+
try { return new URL(origin).host.toLowerCase() === (c.req.header("host") ?? "").toLowerCase(); } catch { return false; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const readJson = (p: string): unknown => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
|
|
30
|
+
|
|
31
|
+
function runnerPid(loki: string): number | null {
|
|
32
|
+
try { const pid = parseInt(readFileSync(join(loki, "loki.pid"), "utf8").trim(), 10); return pidAlive(pid) ? pid : null; } catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ControlResult { status: 200 | 503; body: Rec }
|
|
36
|
+
|
|
37
|
+
/** Applies one enum action to <repoDir>/.loki by writing or removing a signal file. Never signals a process. */
|
|
38
|
+
export function applyControl(repoDir: string, action: ControlAction): ControlResult {
|
|
39
|
+
const loki = lokiDirOf(repoDir);
|
|
40
|
+
const running = runnerPid(loki) !== null;
|
|
41
|
+
const put = (rel: string) => { const f = join(loki, rel); mkdirSync(dirname(f), { recursive: true }); writeFileSync(f, new Date().toISOString()); };
|
|
42
|
+
if (action === "council-review") { put(join("signals", "COUNCIL_REVIEW_REQUESTED")); return { status: 200, body: { success: true, message: "Council review requested" } }; }
|
|
43
|
+
if (action === "resume") {
|
|
44
|
+
for (const n of ["PAUSE", "STOP"]) { try { unlinkSync(join(loki, n)); } catch { /* absent */ } }
|
|
45
|
+
return running ? { status: 200, body: { success: true, message: "Session resumed", process_verified: true } }
|
|
46
|
+
: { status: 503, body: { success: false, message: "Session process is not running; the signal files were cleared" } };
|
|
47
|
+
}
|
|
48
|
+
put(action === "pause" ? "PAUSE" : "STOP");
|
|
49
|
+
if (!running) return { status: 503, body: { success: false, message: `Session process is not running; the ${action} signal may have no effect` } };
|
|
50
|
+
return { status: 200, body: { success: true, message: action === "pause" ? "Session paused" : "Stop requested; the runner exits at its next check", process_verified: true } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Runs the guarded, audited control action; shared by /v1/control/:action and the legacy mappings. */
|
|
54
|
+
export function runControl(db: Db, repoDir: string, action: ControlAction, c: Context, via: string): Response {
|
|
55
|
+
if (!sameOrigin(c)) { audit(db, { kind: `session.${action}`, target: via, result: "refused", detail: "origin not allowed" }); return c.json({ error: "origin not allowed" }, 403); }
|
|
56
|
+
const r = applyControl(repoDir, action);
|
|
57
|
+
audit(db, { kind: `session.${action}`, target: via, result: r.status === 200 ? "ok" : "unverified" });
|
|
58
|
+
return c.json(r.body, r.status);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function councilState(loki: string): Rec {
|
|
62
|
+
const f = join(loki, "council", "state.json");
|
|
63
|
+
if (!existsSync(f)) return { enabled: false, total_votes: 0, verdicts: [] };
|
|
64
|
+
const d = readJson(f);
|
|
65
|
+
return isObj(d) ? d : { enabled: null, total_votes: null, verdicts: null, error: "unreadable_state" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function councilVerdicts(loki: string, limit = 20): Rec {
|
|
69
|
+
const st = readJson(join(loki, "council", "state.json"));
|
|
70
|
+
const verdicts = isObj(st) && Array.isArray(st.verdicts) ? st.verdicts : [];
|
|
71
|
+
const votes = join(loki, "council", "votes");
|
|
72
|
+
const details: Rec[] = [];
|
|
73
|
+
let names: string[] = [];
|
|
74
|
+
try { names = readdirSync(votes, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse(); } catch { /* none */ }
|
|
75
|
+
const text = (p: string) => { try { return readFileSync(p, "utf8"); } catch { return null; } };
|
|
76
|
+
for (const n of names.slice(0, limit)) {
|
|
77
|
+
const dir = join(votes, n), d: Rec = { iteration: n };
|
|
78
|
+
const ev = text(join(dir, "evidence.md"));
|
|
79
|
+
if (ev !== null) d.evidence_preview = ev.slice(0, 500);
|
|
80
|
+
let files: string[] = [];
|
|
81
|
+
try { files = readdirSync(dir).filter((f) => /^member-.*\.txt$/.test(f)).sort(); } catch { /* none */ }
|
|
82
|
+
d.members = files.flatMap((f) => { const t = text(join(dir, f)); return t === null ? [] : [{ member: f.replace(/\.txt$/, ""), content: t.trim() }]; });
|
|
83
|
+
const con = text(join(dir, "contrarian.txt"));
|
|
84
|
+
if (con !== null) d.contrarian = con.trim();
|
|
85
|
+
details.push(d);
|
|
86
|
+
}
|
|
87
|
+
return { verdicts, details };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function councilConvergence(loki: string): Rec {
|
|
91
|
+
const dataPoints: Rec[] = [];
|
|
92
|
+
let raw = "";
|
|
93
|
+
try { raw = readFileSync(join(loki, "council", "convergence.log"), "utf8"); } catch { /* none */ }
|
|
94
|
+
for (const line of raw.trim().split("\n")) {
|
|
95
|
+
const p = line.split("|");
|
|
96
|
+
if (p.length < 5) continue;
|
|
97
|
+
const n = p.slice(1, 5).map((x) => (/^-?\d+$/.test(x.trim()) ? Number(x) : NaN));
|
|
98
|
+
if (n.some(Number.isNaN)) continue;
|
|
99
|
+
dataPoints.push({ timestamp: p[0], iteration: n[0], files_changed: n[1], no_change_streak: n[2], done_signals: n[3] });
|
|
100
|
+
}
|
|
101
|
+
return { dataPoints };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function councilReport(loki: string): Rec {
|
|
105
|
+
try { return { report: readFileSync(join(loki, "council", "report.md"), "utf8") }; } catch { return { report: null }; }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function councilTranscripts(loki: string, q: { limit?: number; since?: string; iter_min?: number }): Rec {
|
|
109
|
+
const limit = Math.min(Math.max(q.limit ?? 20, 1), 200);
|
|
110
|
+
let since: number | null = null;
|
|
111
|
+
if (q.since) { since = Date.parse(q.since); if (Number.isNaN(since)) return { error: "Invalid 'since' timestamp format; expected ISO8601" }; }
|
|
112
|
+
const dir = join(loki, "council", "transcripts");
|
|
113
|
+
let names: string[] = [];
|
|
114
|
+
try { names = readdirSync(dir).filter((f) => /^iter-.*\.json$/.test(f)).sort().reverse(); } catch { return { transcripts: [], total: 0, latest_id: null }; }
|
|
115
|
+
const out: Rec[] = [];
|
|
116
|
+
for (const n of names) {
|
|
117
|
+
const rec = readJson(join(dir, n));
|
|
118
|
+
if (!isObj(rec) || typeof rec.iteration_id !== "string") continue;
|
|
119
|
+
if (since !== null) { const t = Date.parse(String(rec.timestamp ?? "")); if (Number.isNaN(t) || t <= since) continue; }
|
|
120
|
+
if (q.iter_min !== undefined && Number(rec.iteration ?? 0) < q.iter_min) continue;
|
|
121
|
+
out.push(rec);
|
|
122
|
+
if (out.length >= limit) break;
|
|
123
|
+
}
|
|
124
|
+
return { transcripts: out, total: out.length, latest_id: out.length ? out[0]!.iteration_id : null };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Parses an optional non-negative integer query param: undefined when absent, null when malformed or above max. */
|
|
128
|
+
export const intQ = (v: string | undefined, max: number): number | undefined | null => (v === undefined ? undefined : /^\d+$/.test(v) && Number(v) <= max ? Number(v) : null);
|
|
129
|
+
|
|
130
|
+
export function mount(ctx: RouteCtx): void {
|
|
131
|
+
const deny = { error: "loopback only without a token" };
|
|
132
|
+
const loki = () => lokiDirOf(ctx.repoDir);
|
|
133
|
+
// Control: registered on `act` (loopback-bound servers only); real loopback peer plus JSON content type (ctx.local), the CP bearer via tokenGuard, and same-origin.
|
|
134
|
+
ctx.act.post("/v1/control/:action", (c) => {
|
|
135
|
+
if (!ctx.local(c)) return c.json({ error: "loopback JSON requests only" }, 403);
|
|
136
|
+
const action = c.req.param("action");
|
|
137
|
+
if (!isAction(action)) return c.json({ error: `action must be one of ${ACTIONS.join(", ")}` }, 400);
|
|
138
|
+
return runControl(ctx.db, ctx.repoDir, action, c, "v1");
|
|
139
|
+
});
|
|
140
|
+
const read = (path: string, f: (c: Context) => Response | Rec) => ctx.app.get(path, (c) => {
|
|
141
|
+
if (!probeAllowed(ctx, c)) return c.json(deny, 403);
|
|
142
|
+
const r = f(c);
|
|
143
|
+
return r instanceof Response ? r : c.json(r);
|
|
144
|
+
});
|
|
145
|
+
read("/v1/council/state", () => councilState(loki()));
|
|
146
|
+
read("/v1/council/verdicts", (c) => { const n = intQ(c.req.query("limit"), 1000); return n === null || n === 0 ? c.json({ error: "limit must be 1..1000" }, 422) : councilVerdicts(loki(), n); });
|
|
147
|
+
read("/v1/council/convergence", () => councilConvergence(loki()));
|
|
148
|
+
read("/v1/council/report", () => councilReport(loki()));
|
|
149
|
+
read("/v1/council/transcripts", (c) => {
|
|
150
|
+
const limit = intQ(c.req.query("limit"), 200), iterMin = intQ(c.req.query("iter_min"), Number.MAX_SAFE_INTEGER);
|
|
151
|
+
if (limit === null || limit === 0 || iterMin === null) return c.json({ error: "limit must be 1..200 and iter_min a non-negative integer" }, 422);
|
|
152
|
+
const r = councilTranscripts(loki(), { limit, since: c.req.query("since"), iter_min: iterMin });
|
|
153
|
+
return "error" in r ? c.json(r, 400) : r;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "10.
|
|
5
|
+
"version": "10.9.1",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|
|
@@ -393,7 +393,7 @@ Design tokens are the shared vocabulary that keeps generated components visually
|
|
|
393
393
|
|
|
394
394
|
`loki magic tokens extract` scans:
|
|
395
395
|
|
|
396
|
-
- `web-app/src/`
|
|
396
|
+
- `web-app/src/` for Tailwind config and CSS variables
|
|
397
397
|
- `web-app/src/**/*.css` for `:root` declarations
|
|
398
398
|
- Existing React components for recurring inline style values
|
|
399
399
|
|
|
@@ -491,7 +491,7 @@ Extract design tokens from the codebase.
|
|
|
491
491
|
|
|
492
492
|
```python
|
|
493
493
|
magic_tokens_extract(
|
|
494
|
-
sources: list[str] | None = None, # default: web-app/src
|
|
494
|
+
sources: list[str] | None = None, # default: web-app/src
|
|
495
495
|
overwrite: bool = False,
|
|
496
496
|
) -> {
|
|
497
497
|
"tokens_written": int,
|
|
@@ -410,7 +410,7 @@ harder.
|
|
|
410
410
|
|---|---|---|---|---|
|
|
411
411
|
| 1 | MCP registry staleness guard | drift, 7.34.1 vs 9.50.1 | registry was already current | iterated `servers[]`, took the FIRST name match; the registry keeps every published version as an `active` row |
|
|
412
412
|
| 2 | "any stale version string left after the bump" | OK on every file | measured nothing at all | shell arithmetic comparison errored on every file; the error text printed alongside the OK lines |
|
|
413
|
-
| 3 | `grep -c '9.50.1'
|
|
413
|
+
| 3 | `grep -c '9.50.1' legacy-ui-static/index.html` | no match, rc=1 | the string class IS present | file contains NUL bytes, so grep treats it as binary and silently reports nothing |
|
|
414
414
|
| 4 | competitor benchmark | 0/3 | harness never reached the tool | missing `--skip-git-repo-check` and `</dev/null` (2026-08-08) |
|
|
415
415
|
| 5 | `git push` wrapped in `timeout 180` | `[exited with code 0]` | origin never moved | the timeout killed the pre-push hook mid-pytest; the WRAPPER's exit code replaced the operation's |
|
|
416
416
|
| 6 | `grep -rn ... --include=*.sh` for a precedent | no output, `rc=0` | the search never ran | zsh expanded `--include=*.sh` as a glob, found no match, and aborted the command before grep started |
|
|
@@ -438,8 +438,8 @@ For 3, measured on this checkout, three tools give three answers for the same
|
|
|
438
438
|
question on the same file:
|
|
439
439
|
|
|
440
440
|
```
|
|
441
|
-
grep -c 'Loki'
|
|
442
|
-
grep -ac 'Loki'
|
|
441
|
+
grep -c 'Loki' legacy-ui-static/index.html -> (nothing), rc=1
|
|
442
|
+
grep -ac 'Loki' legacy-ui-static/index.html -> 27, rc=0
|
|
443
443
|
python3 ... .read().count('Loki') -> 91
|
|
444
444
|
```
|
|
445
445
|
|
package/skills/magic-modules.md
CHANGED
|
@@ -111,7 +111,7 @@ All files under `.loki/magic/generated/` carry a `LOKI-MAGIC-HASH: <sha>` header
|
|
|
111
111
|
|
|
112
112
|
## Design Tokens
|
|
113
113
|
|
|
114
|
-
All generated components use design tokens (colors, spacing, typography, motion) so they match Loki Mode's design language. Defaults come from observing existing components in `web-app/src/` and `
|
|
114
|
+
All generated components use design tokens (colors, spacing, typography, motion) so they match Loki Mode's design language. Defaults come from observing existing components in `web-app/src/` and `legacy-ui/`. Override with:
|
|
115
115
|
|
|
116
116
|
- `loki magic tokens extract` -- scan the codebase, update `.loki/magic/tokens.json`
|
|
117
117
|
- Manual edit of `.loki/magic/tokens.json` -- takes precedence over extraction
|
|
@@ -345,7 +345,7 @@ three other instances measured in the same session.
|
|
|
345
345
|
|
|
346
346
|
Applies to the artifact greps in this section too: a built artifact can contain
|
|
347
347
|
NUL bytes, which makes plain `grep` report no matches and exit 1 on a file that
|
|
348
|
-
demonstrably contains the string. Measured on `
|
|
348
|
+
demonstrably contains the string. Measured on `legacy-ui-static/index.html`:
|
|
349
349
|
`grep -c 'Loki'` finds nothing, `grep -ac 'Loki'` finds 27, Python finds 91.
|
|
350
350
|
Always pair a dist grep with a control string known to be present.
|
|
351
351
|
|
package/tools/audit-docs.py
CHANGED
|
@@ -96,7 +96,7 @@ _ROOT = os.path.dirname(_HERE)
|
|
|
96
96
|
# evidence some runtime reads it. Counting it can only turn a finding into
|
|
97
97
|
# UNCHECKABLE, never the reverse.
|
|
98
98
|
SOURCE_DIRS = ("autonomy", "loki-ts/src", "loki-ts/test", "loki-ts/tests",
|
|
99
|
-
"src", "tools", "dashboard", "
|
|
99
|
+
"src", "tools", "dashboard", "mcp",
|
|
100
100
|
"memory", "events", "bin", "providers", "scripts", "deploy",
|
|
101
101
|
"tests", "benchmarks", "web-app/src", ".github")
|
|
102
102
|
|
package/tools/index-codebase.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as L,r as x,j as e,w as re,Z as ne,m as le,h as ie,p as P,F as ye,U as I,X as ce,L as de,x as oe,M as Ae,T as Z,S as we,l as xe,k as me,y as Ce,E as Fe,a0 as Se,a as U,a2 as he}from"./index-
|
|
1
|
+
import{c as L,r as x,j as e,w as re,Z as ne,m as le,h as ie,p as P,F as ye,U as I,X as ce,L as de,x as oe,M as Ae,T as Z,S as we,l as xe,k as me,y as Ce,E as Fe,a0 as Se,a as U,a2 as he}from"./index-D8t7KnQf.js";import{B as C}from"./Button-Rzk5flhU.js";import{T as $e}from"./trending-up-BzHdCDvF.js";import{C as z}from"./clock-DFBZPSsH.js";import{A as Be,U as ue,M as De}from"./Avatar-BEof29du.js";import{A as J}from"./activity-DOa7ejQA.js";import{R as pe}from"./refresh-cw-DMc2uPLw.js";import{G as ze}from"./globe-BirTPwff.js";import{C as Le}from"./chevron-left-BJ_ydRQW.js";import{C as je}from"./check-CnBdKk3L.js";import{D as Y}from"./dollar-sign-D0wBa4Cv.js";import{U as Te}from"./user-qH5y9yNy.js";import{D as Me}from"./database-YiNBXc9c.js";import{L as Ue}from"./lock-D0Xj43pC.js";import{S as Re}from"./scroll-text-lH2uW7rB.js";import{S as Ie}from"./shield-alert-B8ohcUfu.js";/**
|
|
2
2
|
* @license lucide-react v0.577.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as e}from"./index-
|
|
1
|
+
import{j as e}from"./index-D8t7KnQf.js";import{C as r}from"./clock-DFBZPSsH.js";import{C as a}from"./circle-alert-tz8ufhc_.js";import{C as l}from"./check-CnBdKk3L.js";const i={completed:"bg-[#1FC5A8]/10 text-[#1FC5A8]",running:"bg-[#553DE9]/10 text-[#553DE9]",failed:"bg-[#C45B5B]/10 text-[#C45B5B]",started:"bg-[#D4A03C]/10 text-[#D4A03C]",empty:"bg-[#F8F4F0] text-[#6B6960]",version:"bg-[#553DE9]/10 text-[#553DE9]"};function o({status:t}){switch(t){case"completed":return e.jsx(l,{size:12});case"running":return e.jsxs("span",{className:"relative flex h-2 w-2",children:[e.jsx("span",{className:"animate-ping motion-reduce:animate-none absolute inline-flex h-full w-full rounded-full bg-current opacity-75"}),e.jsx("span",{className:"relative inline-flex rounded-full h-2 w-2 bg-current"})]});case"failed":return e.jsx(a,{size:12});case"started":return e.jsx(r,{size:12});default:return null}}function f({status:t,children:n,className:s=""}){return e.jsxs("span",{className:["inline-flex items-center gap-1 rounded-btn px-2.5 py-0.5 text-xs font-semibold",i[t],s].filter(Boolean).join(" "),children:[e.jsx(o,{status:t}),n]})}export{f as B};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as p,j as t}from"./index-
|
|
1
|
+
import{r as p,j as t}from"./index-D8t7KnQf.js";const m={primary:"bg-[#553DE9] text-white hover:bg-[#4432c4] shadow-button rounded-btn",secondary:"border border-[#553DE9] text-[#553DE9] hover:bg-[#E8E4FD] bg-transparent rounded-btn",ghost:"text-[#36342E] hover:bg-[#F8F4F0] rounded-btn",danger:"bg-[#C45B5B]/10 text-[#C45B5B] border border-[#C45B5B]/20 hover:bg-[#C45B5B]/20 rounded-btn"},b={sm:"px-3 py-1.5 text-xs",md:"px-4 py-2 text-sm",lg:"px-6 py-3 text-base"},u={sm:14,md:16,lg:18};function h({size:e}){return t.jsxs("svg",{className:"animate-spin",width:e,height:e,viewBox:"0 0 24 24",fill:"none",children:[t.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),t.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"})]})}const B=p.forwardRef(({variant:e="primary",size:o="md",icon:n,iconRight:i,loading:r=!1,disabled:a,className:c="",children:l,...x},d)=>{const s=u[o];return t.jsxs("button",{ref:d,disabled:a||r,className:["inline-flex items-center justify-center gap-2 font-medium transition-colors",m[e],b[o],(a||r)&&"opacity-60 cursor-not-allowed",c].filter(Boolean).join(" "),...x,children:[r?t.jsx(h,{size:s}):n?t.jsx(n,{size:s}):null,l,i&&!r&&t.jsx(i,{size:s})]})});B.displayName="Button";export{B};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as $,r as l,j as e,G as we,Q as Se,F as Ce,h as Re,T as U,Z as Ee,a as S,V as Me,S as $e,Y as Te,X as Pe,P as ie,i as Ae,g as ze,C as Ie,l as Le,N as I,J as _e,K as De,O as Fe,u as Oe}from"./index-
|
|
1
|
+
import{c as $,r as l,j as e,G as we,Q as Se,F as Ce,h as Re,T as U,Z as Ee,a as S,V as Me,S as $e,Y as Te,X as Pe,P as ie,i as Ae,g as ze,C as Ie,l as Le,N as I,J as _e,K as De,O as Fe,u as Oe}from"./index-D8t7KnQf.js";import{P as K,S as He,E as Be}from"./ErrorBoundary-BuE0kH3W.js";import{C as qe,F as Ge,M as Ue,H as Ke,a as We,G as Ve,T as Qe}from"./thumbs-up-CTIxLzPD.js";import{C as oe}from"./check-CnBdKk3L.js";import{C as Ye}from"./clock-DFBZPSsH.js";import{D as Xe}from"./dollar-sign-D0wBa4Cv.js";import{U as Ze}from"./upload-HxR34-A8.js";import{L as V}from"./loader-circle-2-nMGmcb.js";/**
|
|
2
2
|
* @license lucide-react v0.577.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as o,j as e}from"./index-Dvny-1SU.js";function f({value:r,prefix:t="",suffix:s=""}){const[u,n]=o.useState(0),c=o.useRef(0);return o.useEffect(()=>{const a=c.current,h=r-a;if(h===0)return;const i=600,d=performance.now();function l(b){const v=b-d,m=Math.min(v/i,1),N=1-Math.pow(1-m,3),p=Math.round(a+h*N);n(p),c.current=p,m<1&&requestAnimationFrame(l)}requestAnimationFrame(l)},[r]),e.jsxs("span",{className:"tabular-nums",children:[t,u.toLocaleString(),s]})}function g(){const[r,t]=o.useState(5),[s,u]=o.useState(30),[n,c]=o.useState(75),a=.4,i=Math.round(r*s*a*4.33),d=Math.round(i*n);return e.jsxs("div",{className:"bg-white border border-[#ECEAE3] rounded-xl p-6 shadow-sm",children:[e.jsx("h3",{className:"text-lg font-bold text-[#36342E] mb-1",children:"ROI Calculator"}),e.jsx("p",{className:"text-sm text-[#6B6960] mb-6",children:"Estimate how much your team could save with autonomous development."}),e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between mb-2",children:[e.jsx("label",{className:"text-sm font-medium text-[#36342E]",children:"Team size"}),e.jsxs("span",{className:"text-sm font-semibold text-[#553DE9] tabular-nums",children:[r," developers"]})]}),e.jsx("input",{type:"range",min:1,max:50,value:r,onChange:l=>t(Number(l.target.value)),className:"w-full h-2 bg-[#ECEAE3] rounded-full appearance-none cursor-pointer accent-[#553DE9]"}),e.jsxs("div",{className:"flex justify-between text-xs text-[#939084] mt-1",children:[e.jsx("span",{children:"1"}),e.jsx("span",{children:"50"})]})]}),e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between mb-2",children:[e.jsx("label",{className:"text-sm font-medium text-[#36342E]",children:"Hours coding per week"}),e.jsxs("span",{className:"text-sm font-semibold text-[#553DE9] tabular-nums",children:[s,"h"]})]}),e.jsx("input",{type:"range",min:5,max:60,value:s,onChange:l=>u(Number(l.target.value)),className:"w-full h-2 bg-[#ECEAE3] rounded-full appearance-none cursor-pointer accent-[#553DE9]"}),e.jsxs("div",{className:"flex justify-between text-xs text-[#939084] mt-1",children:[e.jsx("span",{children:"5h"}),e.jsx("span",{children:"60h"})]})]}),e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between mb-2",children:[e.jsx("label",{className:"text-sm font-medium text-[#36342E]",children:"Average hourly rate"}),e.jsxs("span",{className:"text-sm font-semibold text-[#553DE9] tabular-nums",children:["$",n,"/hr"]})]}),e.jsx("input",{type:"range",min:25,max:250,step:5,value:n,onChange:l=>c(Number(l.target.value)),className:"w-full h-2 bg-[#ECEAE3] rounded-full appearance-none cursor-pointer accent-[#553DE9]"}),e.jsxs("div",{className:"flex justify-between text-xs text-[#939084] mt-1",children:[e.jsx("span",{children:"$25"}),e.jsx("span",{children:"$250"})]})]})]}),e.jsxs("div",{className:"mt-6 pt-6 border-t border-[#ECEAE3]",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-4 text-center",children:[e.jsxs("div",{children:[e.jsx("div",{className:"text-2xl font-bold text-[#553DE9]",children:e.jsx(f,{value:i,suffix:"h"})}),e.jsx("div",{className:"text-xs text-[#6B6960] mt-1",children:"Est. hours saved / month"})]}),e.jsxs("div",{children:[e.jsx("div",{className:"text-2xl font-bold text-[#1FC5A8]",children:e.jsx(f,{value:d,prefix:"$"})}),e.jsx("div",{className:"text-xs text-[#6B6960] mt-1",children:"Est. cost saved / month"})]})]}),e.jsxs("div",{className:"mt-4 p-3 rounded-lg bg-[#553DE9]/5 border border-[#553DE9]/10",children:[e.jsxs("p",{className:"text-sm text-[#36342E] text-center",children:["If Loki Mode saved ",Math.round(a*100),"% of coding time, your team could save"," ",e.jsxs("span",{className:"font-bold text-[#553DE9]",children:[i.toLocaleString()," hours"]})," ","and"," ",e.jsxs("span",{className:"font-bold text-[#1FC5A8]",children:["$",d.toLocaleString()]})," ","per month."]}),e.jsxs("p",{className:"mt-2 text-xs text-[#6B6960] text-center",children:["The ",Math.round(a*100),"% is an assumption, not a measurement. Model and API spend is not included."]})]})]})]})}const j=[{feature:"Claude (Opus/Sonnet/Haiku)",category:"AI Models",lokiMode:!0,boltNew:!1,replit:!1,lovable:!0},{feature:"GPT-4o / Codex",category:"AI Models",lokiMode:!0,boltNew:!0,replit:!0,lovable:!1},{feature:"5-provider auto-failover",category:"AI Models",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"8 automated quality gates",category:"Quality",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"Blind 3-reviewer code review",category:"Quality",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"Anti-sycophancy checks",category:"Quality",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"Automated test generation",category:"Quality",lokiMode:!0,boltNew:!1,replit:"Partial",lovable:!1},{feature:"Docker container generation",category:"Deployment",lokiMode:!0,boltNew:!1,replit:!0,lovable:!1},{feature:"CI/CD pipeline generation",category:"Deployment",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"One-click cloud deploy",category:"Deployment",lokiMode:"Planned",boltNew:!0,replit:!0,lovable:!0},{feature:"Self-hosted (your keys)",category:"Cost",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"Free tier available",category:"Cost",lokiMode:"Source-available (BUSL-1.1)",boltNew:!0,replit:!0,lovable:!0},{feature:"No vendor lock-in",category:"Cost",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"SSO / OIDC authentication",category:"Enterprise",lokiMode:!0,boltNew:!1,replit:!0,lovable:!1},{feature:"RBAC access control",category:"Enterprise",lokiMode:!0,boltNew:!1,replit:!0,lovable:!1},{feature:"Audit logging",category:"Enterprise",lokiMode:!0,boltNew:!1,replit:!0,lovable:!1},{feature:"Air-gapped deployment",category:"Enterprise",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1}];function y(){return e.jsx("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",className:"text-[#1FC5A8]",children:e.jsx("path",{d:"M20 6L9 17l-5-5",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round"})})}function w(){return e.jsx("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",className:"text-[#C45B5B]/40",children:e.jsx("path",{d:"M18 6L6 18M6 6l12 12",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})})}function x({value:r}){return typeof r=="string"?e.jsx("span",{className:"text-xs text-[#6B6960] font-medium",children:r}):r?e.jsx(y,{}):e.jsx(w,{})}const k=[{title:"Full Autonomy",description:"Describe what you want. Loki Mode handles planning, coding, testing, and verification automatically.",icon:e.jsx("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",className:"text-[#553DE9]",children:e.jsx("path",{d:"M13 2L3 14h9l-1 8 10-12h-9l1-8z",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})})},{title:"Production Quality",description:"8 quality gates, blind code review, and anti-sycophancy checks ensure code that actually works.",icon:e.jsx("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",className:"text-[#1FC5A8]",children:e.jsx("path",{d:"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})})},{title:"Your Infrastructure",description:"Self-hosted, source-available (BUSL-1.1), no data leaves your network. Use your own API keys across 5 providers.",icon:e.jsxs("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",className:"text-[#2F71E3]",children:[e.jsx("rect",{x:"2",y:"2",width:"20",height:"8",rx:"2",stroke:"currentColor",strokeWidth:"2"}),e.jsx("rect",{x:"2",y:"14",width:"20",height:"8",rx:"2",stroke:"currentColor",strokeWidth:"2"}),e.jsx("circle",{cx:"6",cy:"6",r:"1",fill:"currentColor"}),e.jsx("circle",{cx:"6",cy:"18",r:"1",fill:"currentColor"})]})},{title:"Built for Teams",description:"Enterprise auth, RBAC, audit logs, and parallel workflows for teams of any size.",icon:e.jsxs("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",className:"text-[#E93D82]",children:[e.jsx("path",{d:"M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"}),e.jsx("circle",{cx:"9",cy:"7",r:"4",stroke:"currentColor",strokeWidth:"2"}),e.jsx("path",{d:"M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})]})}];function C(){const r=[...new Set(j.map(t=>t.category))];return e.jsx("div",{className:"min-h-screen bg-[#FAF9F6]",children:e.jsxs("div",{className:"max-w-6xl mx-auto px-6 py-8",children:[e.jsxs("div",{className:"mb-8",children:[e.jsx("h1",{className:"text-2xl font-bold text-[#36342E]",children:"Compare"}),e.jsx("p",{className:"text-[#6B6960] mt-1",children:"See how Loki Mode stacks up against other AI development platforms."})]}),e.jsx("div",{className:"bg-white border border-[#ECEAE3] rounded-xl overflow-hidden shadow-sm",children:e.jsx("div",{className:"overflow-x-auto",children:e.jsxs("table",{className:"w-full text-sm",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"border-b border-[#ECEAE3]",children:[e.jsx("th",{className:"text-left px-4 py-3 font-medium text-[#6B6960] w-[240px]",children:"Feature"}),e.jsx("th",{className:"text-center px-4 py-3 font-bold text-[#553DE9] bg-[#553DE9]/5 w-[130px]",children:"Loki Mode"}),e.jsx("th",{className:"text-center px-4 py-3 font-medium text-[#6B6960] w-[130px]",children:"bolt.new"}),e.jsx("th",{className:"text-center px-4 py-3 font-medium text-[#6B6960] w-[130px]",children:"Replit"}),e.jsx("th",{className:"text-center px-4 py-3 font-medium text-[#6B6960] w-[130px]",children:"Lovable"})]})}),e.jsx("tbody",{children:r.map(t=>e.jsxs(e.Fragment,{children:[e.jsx("tr",{className:"bg-[#FAF9F6]",children:e.jsx("td",{colSpan:5,className:"px-4 py-2 text-xs font-bold text-[#939084] uppercase tracking-wider",children:t})},`cat-${t}`),j.filter(s=>s.category===t).map(s=>e.jsxs("tr",{className:"border-b border-[#ECEAE3] last:border-b-0",children:[e.jsx("td",{className:"px-4 py-2.5 text-[#36342E]",children:s.feature}),e.jsx("td",{className:"px-4 py-2.5 text-center bg-[#553DE9]/5",children:e.jsx("div",{className:"flex justify-center",children:e.jsx(x,{value:s.lokiMode})})}),e.jsx("td",{className:"px-4 py-2.5 text-center",children:e.jsx("div",{className:"flex justify-center",children:e.jsx(x,{value:s.boltNew})})}),e.jsx("td",{className:"px-4 py-2.5 text-center",children:e.jsx("div",{className:"flex justify-center",children:e.jsx(x,{value:s.replit})})}),e.jsx("td",{className:"px-4 py-2.5 text-center",children:e.jsx("div",{className:"flex justify-center",children:e.jsx(x,{value:s.lovable})})})]},s.feature))]}))})]})})}),e.jsxs("div",{className:"mt-12",children:[e.jsx("h2",{className:"text-xl font-bold text-[#36342E] mb-6",children:"Why developers choose Loki Mode"}),e.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-5",children:k.map(t=>e.jsxs("div",{className:"bg-white border border-[#ECEAE3] rounded-xl p-5 shadow-sm hover:shadow-md transition-shadow",children:[e.jsx("div",{className:"mb-3",children:t.icon}),e.jsx("h3",{className:"text-base font-bold text-[#36342E] mb-1",children:t.title}),e.jsx("p",{className:"text-sm text-[#6B6960]",children:t.description})]},t.title))})]}),e.jsx("div",{className:"mt-12 max-w-xl mx-auto",children:e.jsx(g,{})})]})})}export{C as default};
|
|
1
|
+
import{r as o,j as e}from"./index-D8t7KnQf.js";function f({value:r,prefix:t="",suffix:s=""}){const[u,n]=o.useState(0),c=o.useRef(0);return o.useEffect(()=>{const a=c.current,h=r-a;if(h===0)return;const i=600,d=performance.now();function l(b){const v=b-d,m=Math.min(v/i,1),N=1-Math.pow(1-m,3),p=Math.round(a+h*N);n(p),c.current=p,m<1&&requestAnimationFrame(l)}requestAnimationFrame(l)},[r]),e.jsxs("span",{className:"tabular-nums",children:[t,u.toLocaleString(),s]})}function g(){const[r,t]=o.useState(5),[s,u]=o.useState(30),[n,c]=o.useState(75),a=.4,i=Math.round(r*s*a*4.33),d=Math.round(i*n);return e.jsxs("div",{className:"bg-white border border-[#ECEAE3] rounded-xl p-6 shadow-sm",children:[e.jsx("h3",{className:"text-lg font-bold text-[#36342E] mb-1",children:"ROI Calculator"}),e.jsx("p",{className:"text-sm text-[#6B6960] mb-6",children:"Estimate how much your team could save with autonomous development."}),e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between mb-2",children:[e.jsx("label",{className:"text-sm font-medium text-[#36342E]",children:"Team size"}),e.jsxs("span",{className:"text-sm font-semibold text-[#553DE9] tabular-nums",children:[r," developers"]})]}),e.jsx("input",{type:"range",min:1,max:50,value:r,onChange:l=>t(Number(l.target.value)),className:"w-full h-2 bg-[#ECEAE3] rounded-full appearance-none cursor-pointer accent-[#553DE9]"}),e.jsxs("div",{className:"flex justify-between text-xs text-[#939084] mt-1",children:[e.jsx("span",{children:"1"}),e.jsx("span",{children:"50"})]})]}),e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between mb-2",children:[e.jsx("label",{className:"text-sm font-medium text-[#36342E]",children:"Hours coding per week"}),e.jsxs("span",{className:"text-sm font-semibold text-[#553DE9] tabular-nums",children:[s,"h"]})]}),e.jsx("input",{type:"range",min:5,max:60,value:s,onChange:l=>u(Number(l.target.value)),className:"w-full h-2 bg-[#ECEAE3] rounded-full appearance-none cursor-pointer accent-[#553DE9]"}),e.jsxs("div",{className:"flex justify-between text-xs text-[#939084] mt-1",children:[e.jsx("span",{children:"5h"}),e.jsx("span",{children:"60h"})]})]}),e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between mb-2",children:[e.jsx("label",{className:"text-sm font-medium text-[#36342E]",children:"Average hourly rate"}),e.jsxs("span",{className:"text-sm font-semibold text-[#553DE9] tabular-nums",children:["$",n,"/hr"]})]}),e.jsx("input",{type:"range",min:25,max:250,step:5,value:n,onChange:l=>c(Number(l.target.value)),className:"w-full h-2 bg-[#ECEAE3] rounded-full appearance-none cursor-pointer accent-[#553DE9]"}),e.jsxs("div",{className:"flex justify-between text-xs text-[#939084] mt-1",children:[e.jsx("span",{children:"$25"}),e.jsx("span",{children:"$250"})]})]})]}),e.jsxs("div",{className:"mt-6 pt-6 border-t border-[#ECEAE3]",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-4 text-center",children:[e.jsxs("div",{children:[e.jsx("div",{className:"text-2xl font-bold text-[#553DE9]",children:e.jsx(f,{value:i,suffix:"h"})}),e.jsx("div",{className:"text-xs text-[#6B6960] mt-1",children:"Est. hours saved / month"})]}),e.jsxs("div",{children:[e.jsx("div",{className:"text-2xl font-bold text-[#1FC5A8]",children:e.jsx(f,{value:d,prefix:"$"})}),e.jsx("div",{className:"text-xs text-[#6B6960] mt-1",children:"Est. cost saved / month"})]})]}),e.jsxs("div",{className:"mt-4 p-3 rounded-lg bg-[#553DE9]/5 border border-[#553DE9]/10",children:[e.jsxs("p",{className:"text-sm text-[#36342E] text-center",children:["If Loki Mode saved ",Math.round(a*100),"% of coding time, your team could save"," ",e.jsxs("span",{className:"font-bold text-[#553DE9]",children:[i.toLocaleString()," hours"]})," ","and"," ",e.jsxs("span",{className:"font-bold text-[#1FC5A8]",children:["$",d.toLocaleString()]})," ","per month."]}),e.jsxs("p",{className:"mt-2 text-xs text-[#6B6960] text-center",children:["The ",Math.round(a*100),"% is an assumption, not a measurement. Model and API spend is not included."]})]})]})]})}const j=[{feature:"Claude (Opus/Sonnet/Haiku)",category:"AI Models",lokiMode:!0,boltNew:!1,replit:!1,lovable:!0},{feature:"GPT-4o / Codex",category:"AI Models",lokiMode:!0,boltNew:!0,replit:!0,lovable:!1},{feature:"5-provider auto-failover",category:"AI Models",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"8 automated quality gates",category:"Quality",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"Blind 3-reviewer code review",category:"Quality",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"Anti-sycophancy checks",category:"Quality",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"Automated test generation",category:"Quality",lokiMode:!0,boltNew:!1,replit:"Partial",lovable:!1},{feature:"Docker container generation",category:"Deployment",lokiMode:!0,boltNew:!1,replit:!0,lovable:!1},{feature:"CI/CD pipeline generation",category:"Deployment",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"One-click cloud deploy",category:"Deployment",lokiMode:"Planned",boltNew:!0,replit:!0,lovable:!0},{feature:"Self-hosted (your keys)",category:"Cost",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"Free tier available",category:"Cost",lokiMode:"Source-available (BUSL-1.1)",boltNew:!0,replit:!0,lovable:!0},{feature:"No vendor lock-in",category:"Cost",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1},{feature:"SSO / OIDC authentication",category:"Enterprise",lokiMode:!0,boltNew:!1,replit:!0,lovable:!1},{feature:"RBAC access control",category:"Enterprise",lokiMode:!0,boltNew:!1,replit:!0,lovable:!1},{feature:"Audit logging",category:"Enterprise",lokiMode:!0,boltNew:!1,replit:!0,lovable:!1},{feature:"Air-gapped deployment",category:"Enterprise",lokiMode:!0,boltNew:!1,replit:!1,lovable:!1}];function y(){return e.jsx("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",className:"text-[#1FC5A8]",children:e.jsx("path",{d:"M20 6L9 17l-5-5",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round"})})}function w(){return e.jsx("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",className:"text-[#C45B5B]/40",children:e.jsx("path",{d:"M18 6L6 18M6 6l12 12",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})})}function x({value:r}){return typeof r=="string"?e.jsx("span",{className:"text-xs text-[#6B6960] font-medium",children:r}):r?e.jsx(y,{}):e.jsx(w,{})}const k=[{title:"Full Autonomy",description:"Describe what you want. Loki Mode handles planning, coding, testing, and verification automatically.",icon:e.jsx("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",className:"text-[#553DE9]",children:e.jsx("path",{d:"M13 2L3 14h9l-1 8 10-12h-9l1-8z",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})})},{title:"Production Quality",description:"8 quality gates, blind code review, and anti-sycophancy checks ensure code that actually works.",icon:e.jsx("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",className:"text-[#1FC5A8]",children:e.jsx("path",{d:"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})})},{title:"Your Infrastructure",description:"Self-hosted, source-available (BUSL-1.1), no data leaves your network. Use your own API keys across 5 providers.",icon:e.jsxs("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",className:"text-[#2F71E3]",children:[e.jsx("rect",{x:"2",y:"2",width:"20",height:"8",rx:"2",stroke:"currentColor",strokeWidth:"2"}),e.jsx("rect",{x:"2",y:"14",width:"20",height:"8",rx:"2",stroke:"currentColor",strokeWidth:"2"}),e.jsx("circle",{cx:"6",cy:"6",r:"1",fill:"currentColor"}),e.jsx("circle",{cx:"6",cy:"18",r:"1",fill:"currentColor"})]})},{title:"Built for Teams",description:"Enterprise auth, RBAC, audit logs, and parallel workflows for teams of any size.",icon:e.jsxs("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",className:"text-[#E93D82]",children:[e.jsx("path",{d:"M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"}),e.jsx("circle",{cx:"9",cy:"7",r:"4",stroke:"currentColor",strokeWidth:"2"}),e.jsx("path",{d:"M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})]})}];function C(){const r=[...new Set(j.map(t=>t.category))];return e.jsx("div",{className:"min-h-screen bg-[#FAF9F6]",children:e.jsxs("div",{className:"max-w-6xl mx-auto px-6 py-8",children:[e.jsxs("div",{className:"mb-8",children:[e.jsx("h1",{className:"text-2xl font-bold text-[#36342E]",children:"Compare"}),e.jsx("p",{className:"text-[#6B6960] mt-1",children:"See how Loki Mode stacks up against other AI development platforms."})]}),e.jsx("div",{className:"bg-white border border-[#ECEAE3] rounded-xl overflow-hidden shadow-sm",children:e.jsx("div",{className:"overflow-x-auto",children:e.jsxs("table",{className:"w-full text-sm",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"border-b border-[#ECEAE3]",children:[e.jsx("th",{className:"text-left px-4 py-3 font-medium text-[#6B6960] w-[240px]",children:"Feature"}),e.jsx("th",{className:"text-center px-4 py-3 font-bold text-[#553DE9] bg-[#553DE9]/5 w-[130px]",children:"Loki Mode"}),e.jsx("th",{className:"text-center px-4 py-3 font-medium text-[#6B6960] w-[130px]",children:"bolt.new"}),e.jsx("th",{className:"text-center px-4 py-3 font-medium text-[#6B6960] w-[130px]",children:"Replit"}),e.jsx("th",{className:"text-center px-4 py-3 font-medium text-[#6B6960] w-[130px]",children:"Lovable"})]})}),e.jsx("tbody",{children:r.map(t=>e.jsxs(e.Fragment,{children:[e.jsx("tr",{className:"bg-[#FAF9F6]",children:e.jsx("td",{colSpan:5,className:"px-4 py-2 text-xs font-bold text-[#939084] uppercase tracking-wider",children:t})},`cat-${t}`),j.filter(s=>s.category===t).map(s=>e.jsxs("tr",{className:"border-b border-[#ECEAE3] last:border-b-0",children:[e.jsx("td",{className:"px-4 py-2.5 text-[#36342E]",children:s.feature}),e.jsx("td",{className:"px-4 py-2.5 text-center bg-[#553DE9]/5",children:e.jsx("div",{className:"flex justify-center",children:e.jsx(x,{value:s.lokiMode})})}),e.jsx("td",{className:"px-4 py-2.5 text-center",children:e.jsx("div",{className:"flex justify-center",children:e.jsx(x,{value:s.boltNew})})}),e.jsx("td",{className:"px-4 py-2.5 text-center",children:e.jsx("div",{className:"flex justify-center",children:e.jsx(x,{value:s.replit})})}),e.jsx("td",{className:"px-4 py-2.5 text-center",children:e.jsx("div",{className:"flex justify-center",children:e.jsx(x,{value:s.lovable})})})]},s.feature))]}))})]})})}),e.jsxs("div",{className:"mt-12",children:[e.jsx("h2",{className:"text-xl font-bold text-[#36342E] mb-6",children:"Why developers choose Loki Mode"}),e.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-5",children:k.map(t=>e.jsxs("div",{className:"bg-white border border-[#ECEAE3] rounded-xl p-5 shadow-sm hover:shadow-md transition-shadow",children:[e.jsx("div",{className:"mb-3",children:t.icon}),e.jsx("h3",{className:"text-base font-bold text-[#36342E] mb-1",children:t.title}),e.jsx("p",{className:"text-sm text-[#6B6960]",children:t.description})]},t.title))})]}),e.jsx("div",{className:"mt-12 max-w-xl mx-auto",children:e.jsx(g,{})})]})})}export{C as default};
|