aiden-runtime 4.8.1 → 4.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 +88 -1
- package/dist/cli/v4/aidenCLI.js +37 -6
- package/dist/cli/v4/chatSession.js +53 -13
- package/dist/cli/v4/commands/daemon.js +53 -3
- package/dist/cli/v4/commands/daemonDoctor.js +212 -0
- package/dist/cli/v4/commands/daemonStatus.js +45 -26
- package/dist/cli/v4/commands/help.js +5 -0
- package/dist/cli/v4/commands/hooks.js +466 -0
- package/dist/cli/v4/commands/hooksSlash.js +33 -0
- package/dist/cli/v4/commands/index.js +13 -1
- package/dist/cli/v4/commands/mcp.js +89 -1
- package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
- package/dist/cli/v4/commands/memory.js +707 -0
- package/dist/cli/v4/commands/memorySlash.js +38 -0
- package/dist/cli/v4/commands/recovery.js +1 -1
- package/dist/cli/v4/commands/skin.js +7 -0
- package/dist/cli/v4/commands/theme.js +217 -0
- package/dist/cli/v4/commands/trigger.js +1 -1
- package/dist/cli/v4/design/tokens.js +52 -4
- package/dist/cli/v4/display.js +39 -26
- package/dist/cli/v4/replyRenderer.js +6 -5
- package/dist/cli/v4/skinEngine.js +67 -0
- package/dist/cli/v4/ui/progressBar.js +179 -0
- package/dist/cli/v4/util/closestAction.js +48 -0
- package/dist/core/v4/aidenAgent.js +45 -2
- package/dist/core/v4/daemon/api/runs.js +131 -0
- package/dist/core/v4/daemon/bootstrap.js +368 -13
- package/dist/core/v4/daemon/db/migrations.js +169 -0
- package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
- package/dist/core/v4/daemon/incarnationStore.js +47 -0
- package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
- package/dist/core/v4/daemon/runs/reclaim.js +88 -0
- package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
- package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
- package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
- package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
- package/dist/core/v4/daemon/spans/spanStore.js +113 -0
- package/dist/core/v4/daemon/triggerBus.js +50 -19
- package/dist/core/v4/hooks/auditQuery.js +67 -0
- package/dist/core/v4/hooks/dispatcher.js +286 -0
- package/dist/core/v4/hooks/index.js +46 -0
- package/dist/core/v4/hooks/lifecycle.js +27 -0
- package/dist/core/v4/hooks/manifest.js +142 -0
- package/dist/core/v4/hooks/registry.js +149 -0
- package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
- package/dist/core/v4/hooks/toolHookGate.js +76 -0
- package/dist/core/v4/hooks/trust.js +14 -0
- package/dist/core/v4/identity/contextManager.js +83 -0
- package/dist/core/v4/identity/daemonId.js +85 -0
- package/dist/core/v4/identity/enforcement.js +103 -0
- package/dist/core/v4/identity/executionContext.js +153 -0
- package/dist/core/v4/identity/hookExecution.js +62 -0
- package/dist/core/v4/identity/httpContext.js +68 -0
- package/dist/core/v4/identity/ids.js +185 -0
- package/dist/core/v4/identity/index.js +60 -0
- package/dist/core/v4/identity/subprocessContext.js +98 -0
- package/dist/core/v4/identity/traceparent.js +114 -0
- package/dist/core/v4/logger/index.js +3 -1
- package/dist/core/v4/logger/logger.js +28 -1
- package/dist/core/v4/logger/redact.js +149 -0
- package/dist/core/v4/logger/sinks/fileSink.js +13 -0
- package/dist/core/v4/logger/sinks/stdSink.js +19 -1
- package/dist/core/v4/mcp/install/backup.js +78 -0
- package/dist/core/v4/mcp/install/clientPaths.js +90 -0
- package/dist/core/v4/mcp/install/clients.js +203 -0
- package/dist/core/v4/mcp/install/healthCheck.js +83 -0
- package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
- package/dist/core/v4/mcp/install/profiles.js +109 -0
- package/dist/core/v4/mcp/install/wslDetect.js +62 -0
- package/dist/core/v4/memory/namespaceRegistry.js +117 -0
- package/dist/core/v4/memory/projectRoot.js +76 -0
- package/dist/core/v4/memory/reviewer/index.js +162 -0
- package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
- package/dist/core/v4/memory/reviewer/prompt.js +105 -0
- package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
- package/dist/core/v4/memoryManager.js +57 -10
- package/dist/core/v4/paths.js +2 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
- package/dist/core/v4/theme/bundledThemes.js +106 -0
- package/dist/core/v4/theme/themeLoader.js +160 -0
- package/dist/core/v4/theme/themeRegistry.js +97 -0
- package/dist/core/v4/theme/themeWatcher.js +95 -0
- package/dist/core/v4/toolRegistry.js +71 -8
- package/dist/core/v4/update/depWarningFilter.js +76 -0
- package/dist/core/v4/update/executeInstall.js +41 -35
- package/dist/core/v4/update/platformInstructions.js +128 -0
- package/dist/moat/approvalEngine.js +4 -0
- package/dist/moat/memoryGuard.js +8 -1
- package/dist/providers/v4/anthropicAdapter.js +10 -4
- package/dist/tools/v4/backends/local.js +19 -2
- package/dist/tools/v4/sessions/recallSession.js +6 -1
- package/package.json +3 -1
- package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
- package/themes/default.yaml +52 -0
- package/themes/dracula.yaml +32 -0
- package/themes/light.yaml +32 -0
- package/themes/monochrome.yaml +31 -0
- package/themes/tokyo-night.yaml +32 -0
- package/dist/core/pluginSystem.js +0 -121
- package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/memory/reviewer/skipRules.ts — v4.9.0 Slice 10.
|
|
10
|
+
*
|
|
11
|
+
* Hard skip rules applied to every reviewer-proposed candidate AFTER
|
|
12
|
+
* the LLM returns. Belt-and-braces — the reviewer prompt also tells
|
|
13
|
+
* the LLM not to propose these, but the parser drops violators
|
|
14
|
+
* defensively because LLMs occasionally disregard prompt constraints.
|
|
15
|
+
*
|
|
16
|
+
* Rules (each has a `class` label that appears in the drop log):
|
|
17
|
+
* - sensitive_class — PII / medical / political / religious / financial inference
|
|
18
|
+
* - negation — "not X", "doesn't Y", "no longer Z"
|
|
19
|
+
* - transient — "this session", "today", "just now", "recently"
|
|
20
|
+
* - duplicate — substring overlap with an existing live entry
|
|
21
|
+
* - char_cap — > 200 chars
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.MAX_CANDIDATE_CHARS = void 0;
|
|
25
|
+
exports.evaluateCandidate = evaluateCandidate;
|
|
26
|
+
const SENSITIVE_PATTERNS = [
|
|
27
|
+
// Medical / health (broad — includes common conditions, treatments, symptoms)
|
|
28
|
+
/\b(diagnos|prescrib|medication|illness|disorder|disease|therap|antidepress|mental[- ]health|disabilit|cancer|tumou?r|anxiety|depress|HIV|AIDS\b|chronic|symptom)/i,
|
|
29
|
+
// Political / religious
|
|
30
|
+
/\b(votes? for|political affiliat|religious|conservative|liberal|democrat|republican|catholic|muslim|jewish|hindu|atheist)/i,
|
|
31
|
+
// Financial inference
|
|
32
|
+
/\b(income|salary|net worth|debt|bankrupt|tax bracket|credit score)/i,
|
|
33
|
+
// Sexual orientation / family planning
|
|
34
|
+
/\b(sexual orientat|\bgay\b|lesbian|straight\b|transgender|pregnan|fertility|miscarriage)/i,
|
|
35
|
+
];
|
|
36
|
+
const NEGATION_PROBES = [
|
|
37
|
+
// Explicit "X does/did/is not Y" — match the negative verb forms,
|
|
38
|
+
// not the prefix. The reviewer often phrases candidates as
|
|
39
|
+
// "User does not …" so we match `does not`, `doesn't`, etc.
|
|
40
|
+
/\b(?:does\s*not|did\s*not|do\s*not|is\s*not|are\s*not|was\s*not|were\s*not|will\s*not|cannot|can\s*not|could\s*not|should\s*not|would\s*not)\b/i,
|
|
41
|
+
// Contracted forms — match across straight quote, smart quote, or
|
|
42
|
+
// apostrophe-less typo ("dont").
|
|
43
|
+
/\b(?:don[''’]?t|doesn[''’]?t|didn[''’]?t|isn[''’]?t|aren[''’]?t|wasn[''’]?t|weren[''’]?t|won[''’]?t|can[''’]?t|couldn[''’]?t|shouldn[''’]?t|wouldn[''’]?t)\b/i,
|
|
44
|
+
// "no longer" / "never" / leading "not"
|
|
45
|
+
/\b(?:no longer|never)\b/i,
|
|
46
|
+
/^\s*not\s+\w/i,
|
|
47
|
+
];
|
|
48
|
+
const TRANSIENT_MARKERS = [
|
|
49
|
+
/\b(this session|this conversation|just now|right now|currently|today|tomorrow|yesterday|recently|earlier|a moment ago|this turn|last turn)\b/i,
|
|
50
|
+
];
|
|
51
|
+
exports.MAX_CANDIDATE_CHARS = 200;
|
|
52
|
+
/**
|
|
53
|
+
* Fuzzy duplicate detection: candidate is dropped if it's a substring
|
|
54
|
+
* of any live entry (or vice-versa), case-insensitively. Cheap heuristic
|
|
55
|
+
* — matches the Slice 9 `MemoryManager.add()` substring-dedup pattern.
|
|
56
|
+
*/
|
|
57
|
+
function isDuplicate(candidate, liveEntries) {
|
|
58
|
+
const c = candidate.toLowerCase().trim();
|
|
59
|
+
if (c.length < 8)
|
|
60
|
+
return false; // too short to dedup against
|
|
61
|
+
for (const live of liveEntries) {
|
|
62
|
+
const l = live.toLowerCase().trim();
|
|
63
|
+
if (l.length === 0)
|
|
64
|
+
continue;
|
|
65
|
+
if (l.includes(c) || c.includes(l))
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
/** Evaluate a candidate against every rule. Returns the first hit. */
|
|
71
|
+
function evaluateCandidate(candidate, liveEntries) {
|
|
72
|
+
const trimmed = candidate.trim();
|
|
73
|
+
if (trimmed.length === 0)
|
|
74
|
+
return { drop: true, klass: 'char_cap' };
|
|
75
|
+
if (trimmed.length > exports.MAX_CANDIDATE_CHARS)
|
|
76
|
+
return { drop: true, klass: 'char_cap' };
|
|
77
|
+
for (const p of SENSITIVE_PATTERNS) {
|
|
78
|
+
if (p.test(trimmed))
|
|
79
|
+
return { drop: true, klass: 'sensitive_class' };
|
|
80
|
+
}
|
|
81
|
+
for (const p of NEGATION_PROBES) {
|
|
82
|
+
if (p.test(trimmed))
|
|
83
|
+
return { drop: true, klass: 'negation' };
|
|
84
|
+
}
|
|
85
|
+
for (const p of TRANSIENT_MARKERS) {
|
|
86
|
+
if (p.test(trimmed))
|
|
87
|
+
return { drop: true, klass: 'transient' };
|
|
88
|
+
}
|
|
89
|
+
if (isDuplicate(trimmed, liveEntries))
|
|
90
|
+
return { drop: true, klass: 'duplicate' };
|
|
91
|
+
return { drop: false };
|
|
92
|
+
}
|
|
@@ -50,6 +50,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
50
50
|
exports.MemoryManager = exports.ENTRY_SEPARATOR = exports.USER_CHAR_LIMIT = exports.MEMORY_CHAR_LIMIT = void 0;
|
|
51
51
|
const node_fs_1 = require("node:fs");
|
|
52
52
|
const node_path_1 = __importDefault(require("node:path"));
|
|
53
|
+
// v4.9.0 Slice 11 — namespace registry generalizes 'memory'|'user' to N.
|
|
54
|
+
const namespaceRegistry_1 = require("./memory/namespaceRegistry");
|
|
53
55
|
/**
|
|
54
56
|
* Char budgets per arch doc Memory section. NOT tokens — char counts are
|
|
55
57
|
* model-independent and easy to enforce client-side.
|
|
@@ -59,8 +61,11 @@ exports.USER_CHAR_LIMIT = 1375;
|
|
|
59
61
|
/** Entry separator: `\n§\n`. Distinct enough to never collide with content. */
|
|
60
62
|
exports.ENTRY_SEPARATOR = '\n§\n';
|
|
61
63
|
class MemoryManager {
|
|
62
|
-
constructor
|
|
63
|
-
|
|
64
|
+
// v4.9.0 Slice 11 — constructor now accepts either the legacy
|
|
65
|
+
// `AidenPaths` (back-compat) OR an options object with optional
|
|
66
|
+
// `projectRoot`. Both shapes preserve `this.paths` for the rest of
|
|
67
|
+
// the class.
|
|
68
|
+
constructor(opts) {
|
|
64
69
|
this.name = 'builtin';
|
|
65
70
|
this.writeQueue = Promise.resolve();
|
|
66
71
|
/**
|
|
@@ -70,6 +75,15 @@ class MemoryManager {
|
|
|
70
75
|
* fire listeners — preserves the "stale snapshot stays clean" invariant.
|
|
71
76
|
*/
|
|
72
77
|
this.mutationListeners = new Set();
|
|
78
|
+
if (opts.paths !== undefined) {
|
|
79
|
+
const o = opts;
|
|
80
|
+
this.paths = o.paths;
|
|
81
|
+
this.projectRoot = o.projectRoot ?? null;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.paths = opts;
|
|
85
|
+
this.projectRoot = null;
|
|
86
|
+
}
|
|
73
87
|
}
|
|
74
88
|
/**
|
|
75
89
|
* Phase 16d: subscribe to successful memory mutations. Returns an
|
|
@@ -102,12 +116,28 @@ class MemoryManager {
|
|
|
102
116
|
async loadSnapshot() {
|
|
103
117
|
const memoryMd = await readFileOrEmpty(this.paths.memoryMd);
|
|
104
118
|
const userMd = await readFileOrEmpty(this.paths.userMd);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
// v4.9.0 Slice 11 — collect every registered namespace's content
|
|
120
|
+
// into the generalized `files` map. Legacy `memoryMd` / `userMd`
|
|
121
|
+
// fields stay populated for back-compat with the 9 v4 consumers
|
|
122
|
+
// that read them directly (memoryGuard, aidenAgent listeners, ...).
|
|
123
|
+
const files = {};
|
|
124
|
+
let anyContent = memoryMd.trim().length > 0 || userMd.trim().length > 0;
|
|
125
|
+
for (const ns of (0, namespaceRegistry_1.listNamespaces)()) {
|
|
126
|
+
let p;
|
|
127
|
+
try {
|
|
128
|
+
p = ns.resolve(this.paths, this.projectRoot);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
continue; /* requiresProject + no root — silently skip */
|
|
132
|
+
}
|
|
133
|
+
const content = await readFileOrEmpty(p);
|
|
134
|
+
files[ns.name] = {
|
|
135
|
+
content, charCount: content.length, charLimit: ns.charLimit, path: p,
|
|
136
|
+
};
|
|
137
|
+
if (content.trim().length > 0)
|
|
138
|
+
anyContent = true;
|
|
139
|
+
}
|
|
140
|
+
return { memoryMd, userMd, loadedAt: Date.now(), isEmpty: !anyContent, files };
|
|
111
141
|
}
|
|
112
142
|
add(file, content) {
|
|
113
143
|
return this.serialised(async () => {
|
|
@@ -224,8 +254,21 @@ class MemoryManager {
|
|
|
224
254
|
});
|
|
225
255
|
}
|
|
226
256
|
// ── Internals ────────────────────────────────────────────────────────
|
|
257
|
+
// v4.9.0 Slice 11 — resolve via namespace registry. Legacy callers
|
|
258
|
+
// passing 'memory' / 'user' hit the built-in resolvers; new namespaces
|
|
259
|
+
// (project, future plugin namespaces) flow through the same path.
|
|
227
260
|
pathFor(file) {
|
|
228
|
-
|
|
261
|
+
if (!(0, namespaceRegistry_1.hasNamespace)(file)) {
|
|
262
|
+
throw new Error(`unknown memory namespace '${file}'`);
|
|
263
|
+
}
|
|
264
|
+
return (0, namespaceRegistry_1.getNamespace)(file).resolve(this.paths, this.projectRoot);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* v4.9.0 Slice 11 — public char-limit lookup for any namespace.
|
|
268
|
+
* Replaces the legacy file-local `limitFor` switch with a registry hit.
|
|
269
|
+
*/
|
|
270
|
+
charLimitFor(namespace) {
|
|
271
|
+
return (0, namespaceRegistry_1.getNamespace)(namespace).charLimit;
|
|
229
272
|
}
|
|
230
273
|
/**
|
|
231
274
|
* Serialise mutations so two concurrent add()s can't collapse into a
|
|
@@ -238,8 +281,12 @@ class MemoryManager {
|
|
|
238
281
|
}
|
|
239
282
|
}
|
|
240
283
|
exports.MemoryManager = MemoryManager;
|
|
284
|
+
// v4.9.0 Slice 11 — registry-aware. Legacy two-namespace callers
|
|
285
|
+
// (memoryGuard, tools) hit this via the existing add/replace/remove
|
|
286
|
+
// paths; the constants above stay exported for any consumer still
|
|
287
|
+
// importing them directly.
|
|
241
288
|
function limitFor(file) {
|
|
242
|
-
return
|
|
289
|
+
return (0, namespaceRegistry_1.getNamespace)(file).charLimit;
|
|
243
290
|
}
|
|
244
291
|
async function readFileOrEmpty(p) {
|
|
245
292
|
try {
|
package/dist/core/v4/paths.js
CHANGED
|
@@ -102,6 +102,8 @@ function resolveAidenPaths(opts = {}) {
|
|
|
102
102
|
skinsDir: node_path_1.default.join(root, 'skins'),
|
|
103
103
|
recentCommandsFile: node_path_1.default.join(root, '.recent-commands.json'),
|
|
104
104
|
sessionsDir: node_path_1.default.join(root, 'sessions'),
|
|
105
|
+
distillationsDir: node_path_1.default.join(root, 'distillations'),
|
|
106
|
+
memoryBackupsDir: node_path_1.default.join(root, 'memory-backups'),
|
|
105
107
|
pluginsDir: node_path_1.default.join(root, 'plugins'),
|
|
106
108
|
logsDir: node_path_1.default.join(root, 'logs'),
|
|
107
109
|
bundledManifest: node_path_1.default.join(root, '.bundled_manifest'),
|
|
@@ -29,6 +29,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
exports.spawnSubAgent = spawnSubAgent;
|
|
30
30
|
const node_crypto_1 = require("node:crypto");
|
|
31
31
|
const childBuilder_1 = require("./childBuilder");
|
|
32
|
+
// v4.9.0 Slice 7 — fork ExecutionContext into the child agent.
|
|
33
|
+
const identity_1 = require("../identity");
|
|
32
34
|
const factory_1 = require("../logger/factory");
|
|
33
35
|
// ── Constants ─────────────────────────────────────────────────────────────
|
|
34
36
|
const DEFAULT_TIMEOUT_MS = 600000;
|
|
@@ -189,14 +191,25 @@ async function spawnSubAgent(spec, deps, ctx) {
|
|
|
189
191
|
let apiCalls = 0;
|
|
190
192
|
let tokensIn = 0;
|
|
191
193
|
let tokensOut = 0;
|
|
194
|
+
// v4.9.0 Slice 7 — fork an ExecutionContext for the child so its
|
|
195
|
+
// tool/LLM spans chain off the parent's spanId. AsyncLocalStorage
|
|
196
|
+
// is in-process; the child runs inside `runWithContext(childCtx, ...)`
|
|
197
|
+
// so any `currentContext()` reads during the child's runConversation
|
|
198
|
+
// see the child's chain (same traceId, fresh spanId, parentSpanId =
|
|
199
|
+
// parent's spanId). Out-of-context callers (legacy paths) leave
|
|
200
|
+
// the call exactly as it was pre-Slice-7.
|
|
201
|
+
const parentCtx = (0, identity_1.currentContext)();
|
|
202
|
+
const runChild = async () => agentBundle.agent.runConversation(agentBundle.history, {
|
|
203
|
+
signal: childCtrl.signal,
|
|
204
|
+
// v4.8.0 Phase 2.2 — uiOnly events from a subagent are
|
|
205
|
+
// dropped. Subagents have no chat surface; the parent
|
|
206
|
+
// assembles their summary. Stub stays a no-op forever.
|
|
207
|
+
onUiEvent: () => { },
|
|
208
|
+
});
|
|
192
209
|
try {
|
|
193
|
-
const result = await
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// dropped. Subagents have no chat surface; the parent
|
|
197
|
-
// assembles their summary. Stub stays a no-op forever.
|
|
198
|
-
onUiEvent: () => { },
|
|
199
|
-
});
|
|
210
|
+
const result = await (parentCtx
|
|
211
|
+
? (0, identity_1.runWithContext)((0, identity_1.childSpan)({ ...parentCtx, source: 'subagent' }), runChild)
|
|
212
|
+
: runChild());
|
|
200
213
|
apiCalls = result.turnCount; // one provider call per turn
|
|
201
214
|
tokensIn = result.totalUsage.inputTokens;
|
|
202
215
|
tokensOut = result.totalUsage.outputTokens;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/theme/bundledThemes.ts — v4.9.0 Slice 1b.
|
|
10
|
+
*
|
|
11
|
+
* Locates the bundled theme YAML files. Themes live at `themes/*.yaml`
|
|
12
|
+
* at the repo root and are included in the published npm artifact via
|
|
13
|
+
* `package.json#files`. This module resolves the on-disk path for
|
|
14
|
+
* each theme name and exposes a tiny API that the `/theme list` and
|
|
15
|
+
* `/theme set` slash commands consume without needing to know the
|
|
16
|
+
* fs layout.
|
|
17
|
+
*
|
|
18
|
+
* Defense-in-depth: if a corrupted install is missing the `themes/`
|
|
19
|
+
* directory entirely, callers fall through gracefully — `getYaml()`
|
|
20
|
+
* returns null, `/theme list` shows zero bundled themes (but still
|
|
21
|
+
* lists any user themes from `~/.aiden/themes/`).
|
|
22
|
+
*/
|
|
23
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.BUNDLED_NAMES = void 0;
|
|
28
|
+
exports.bundledYamlPath = bundledYamlPath;
|
|
29
|
+
exports.getYaml = getYaml;
|
|
30
|
+
exports.listBundled = listBundled;
|
|
31
|
+
exports.isBundled = isBundled;
|
|
32
|
+
exports._resetForTests = _resetForTests;
|
|
33
|
+
const node_fs_1 = require("node:fs");
|
|
34
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
35
|
+
/** Bundled theme names, sorted to give `/theme list` a stable order. */
|
|
36
|
+
exports.BUNDLED_NAMES = ['default', 'monochrome', 'light', 'tokyo-night', 'dracula'];
|
|
37
|
+
/**
|
|
38
|
+
* Walk up from `__dirname` looking for the repo root that holds the
|
|
39
|
+
* `themes/` directory. Works for:
|
|
40
|
+
* - tsc tree (dist/core/v4/theme/bundledThemes.js → walk 3 up)
|
|
41
|
+
* - esbuild (dist-bundle/cli.js → walk 1 up)
|
|
42
|
+
* - source (core/v4/theme/bundledThemes.ts → walk 3 up via tsx)
|
|
43
|
+
* - npm install (node_modules/aiden-runtime/dist/... → walk to the
|
|
44
|
+
* aiden-runtime root)
|
|
45
|
+
*
|
|
46
|
+
* Returns null when no `themes/` directory is found within 6 ancestors
|
|
47
|
+
* (corrupted install). Callers treat null as "no bundled themes
|
|
48
|
+
* available" and degrade gracefully.
|
|
49
|
+
*/
|
|
50
|
+
function findThemesDir() {
|
|
51
|
+
let dir = __dirname;
|
|
52
|
+
for (let i = 0; i < 6; i += 1) {
|
|
53
|
+
const candidate = node_path_1.default.join(dir, 'themes');
|
|
54
|
+
if ((0, node_fs_1.existsSync)(candidate) && (0, node_fs_1.existsSync)(node_path_1.default.join(candidate, 'default.yaml'))) {
|
|
55
|
+
return candidate;
|
|
56
|
+
}
|
|
57
|
+
const parent = node_path_1.default.dirname(dir);
|
|
58
|
+
if (parent === dir)
|
|
59
|
+
break;
|
|
60
|
+
dir = parent;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
let cachedThemesDir;
|
|
65
|
+
function themesDir() {
|
|
66
|
+
if (cachedThemesDir === undefined)
|
|
67
|
+
cachedThemesDir = findThemesDir();
|
|
68
|
+
return cachedThemesDir;
|
|
69
|
+
}
|
|
70
|
+
/** Absolute path to a bundled YAML, or null when the themes/ dir is missing. */
|
|
71
|
+
function bundledYamlPath(name) {
|
|
72
|
+
const dir = themesDir();
|
|
73
|
+
if (!dir)
|
|
74
|
+
return null;
|
|
75
|
+
return node_path_1.default.join(dir, `${name}.yaml`);
|
|
76
|
+
}
|
|
77
|
+
/** Read the YAML for a bundled theme. Returns null on missing-file. */
|
|
78
|
+
function getYaml(name) {
|
|
79
|
+
const file = bundledYamlPath(name);
|
|
80
|
+
if (!file || !(0, node_fs_1.existsSync)(file))
|
|
81
|
+
return null;
|
|
82
|
+
try {
|
|
83
|
+
return (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const DESCRIPTIONS = {
|
|
90
|
+
default: "Aiden's signature brand-orange theme on a dark terminal.",
|
|
91
|
+
monochrome: 'Pure greyscale. Semantic accents retained for error/success readability.',
|
|
92
|
+
light: 'Light terminal. Dark text on light background. Brand orange accent.',
|
|
93
|
+
'tokyo-night': 'Inspired by Tokyo Night. Cool nocturnal palette.',
|
|
94
|
+
dracula: 'Inspired by Dracula. High-contrast dark with vivid accents.',
|
|
95
|
+
};
|
|
96
|
+
/** Enumerate bundled themes. Filters out any that fail to resolve on disk. */
|
|
97
|
+
function listBundled() {
|
|
98
|
+
return exports.BUNDLED_NAMES
|
|
99
|
+
.filter((n) => bundledYamlPath(n) !== null)
|
|
100
|
+
.map((n) => ({ name: n, description: DESCRIPTIONS[n] }));
|
|
101
|
+
}
|
|
102
|
+
function isBundled(name) {
|
|
103
|
+
return exports.BUNDLED_NAMES.includes(name);
|
|
104
|
+
}
|
|
105
|
+
/** Test-only reset of the themes-dir cache. */
|
|
106
|
+
function _resetForTests() { cachedThemesDir = undefined; }
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/theme/themeLoader.ts — v4.9.0 Slice 1a.
|
|
10
|
+
*
|
|
11
|
+
* YAML parser + validator for user theme files. Permissive by design:
|
|
12
|
+
* a malformed file or invalid field warns and falls back per-field
|
|
13
|
+
* rather than rejecting the entire theme. Aiden must keep running
|
|
14
|
+
* with a sensible visual even if the user's theme.yaml has typos.
|
|
15
|
+
*
|
|
16
|
+
* Wire format (hex strings throughout — see also the legacy
|
|
17
|
+
* `skins/*.yaml` format which uses RGB tuples; the two systems
|
|
18
|
+
* deliberately use distinct formats so users can tell which file
|
|
19
|
+
* goes with which slash command):
|
|
20
|
+
*
|
|
21
|
+
* name: "my-theme"
|
|
22
|
+
* description: "..."
|
|
23
|
+
* inherits: null # or a bundled theme name
|
|
24
|
+
* colors:
|
|
25
|
+
* brand: { primary: "#FF6B35", muted: "#7a3119" }
|
|
26
|
+
* # ... etc
|
|
27
|
+
* glyphs:
|
|
28
|
+
* panel: { bar: "│" }
|
|
29
|
+
* trail: { gutter: "┊" }
|
|
30
|
+
* # ... etc
|
|
31
|
+
*/
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseThemeYaml = parseThemeYaml;
|
|
37
|
+
exports.loadThemeFile = loadThemeFile;
|
|
38
|
+
const node_fs_1 = require("node:fs");
|
|
39
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
40
|
+
const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
41
|
+
/**
|
|
42
|
+
* Parse a YAML string into a ParsedTheme. Returns `parsed: null`
|
|
43
|
+
* only on top-level YAML parse failure (in which case the caller
|
|
44
|
+
* should keep the current theme). Otherwise returns the best-effort
|
|
45
|
+
* parse with per-field warnings; bad fields are silently dropped
|
|
46
|
+
* from the override maps so the baseline shows through.
|
|
47
|
+
*/
|
|
48
|
+
function parseThemeYaml(text) {
|
|
49
|
+
const warnings = [];
|
|
50
|
+
let doc;
|
|
51
|
+
try {
|
|
52
|
+
doc = js_yaml_1.default.load(text);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
warnings.push(`YAML parse error: ${err.message}`);
|
|
56
|
+
return { parsed: null, warnings };
|
|
57
|
+
}
|
|
58
|
+
// v4.9.0 Slice 1a hotfix — `yaml.load('')` returns `undefined` for
|
|
59
|
+
// an empty file. That's a benign transient state — the user just
|
|
60
|
+
// created theme.yaml via /theme edit and hasn't typed content yet,
|
|
61
|
+
// OR they cleared the file before retyping. Don't surface a warning
|
|
62
|
+
// for that case; just signal "no theme parsed" and let the caller
|
|
63
|
+
// keep the current theme active.
|
|
64
|
+
if (doc === undefined || doc === null) {
|
|
65
|
+
return { parsed: null, warnings };
|
|
66
|
+
}
|
|
67
|
+
if (typeof doc !== 'object') {
|
|
68
|
+
warnings.push('Theme root must be a mapping; got ' + typeof doc);
|
|
69
|
+
return { parsed: null, warnings };
|
|
70
|
+
}
|
|
71
|
+
const root = doc;
|
|
72
|
+
const name = typeof root.name === 'string' && root.name.trim().length > 0
|
|
73
|
+
? root.name.trim()
|
|
74
|
+
: 'custom';
|
|
75
|
+
const description = typeof root.description === 'string'
|
|
76
|
+
? root.description
|
|
77
|
+
: undefined;
|
|
78
|
+
const colorOverrides = {};
|
|
79
|
+
collectStringPaths(root.colors, 'colors', colorOverrides, warnings, (value, path) => {
|
|
80
|
+
if (!HEX_RE.test(value)) {
|
|
81
|
+
warnings.push(`Invalid hex at ${path}: "${value}" (expected #RGB or #RRGGBB); falling back to default.`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
});
|
|
86
|
+
const glyphOverrides = {};
|
|
87
|
+
collectStringPaths(root.glyphs, 'glyphs', glyphOverrides, warnings, (value /* , path */) => {
|
|
88
|
+
// Glyphs are free-form strings — no validation beyond "is a string".
|
|
89
|
+
return value;
|
|
90
|
+
});
|
|
91
|
+
// The dotted paths returned by collectStringPaths include the
|
|
92
|
+
// `colors.` / `glyphs.` prefix. Strip those so the registry can
|
|
93
|
+
// write directly into the `colors` / `glyphs` singletons.
|
|
94
|
+
const colorOverridesStripped = {};
|
|
95
|
+
for (const [k, v] of Object.entries(colorOverrides)) {
|
|
96
|
+
colorOverridesStripped[k.replace(/^colors\./, '')] = v;
|
|
97
|
+
}
|
|
98
|
+
const glyphOverridesStripped = {};
|
|
99
|
+
for (const [k, v] of Object.entries(glyphOverrides)) {
|
|
100
|
+
glyphOverridesStripped[k.replace(/^glyphs\./, '')] = v;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
parsed: {
|
|
104
|
+
name,
|
|
105
|
+
description,
|
|
106
|
+
colorOverrides: colorOverridesStripped,
|
|
107
|
+
glyphOverrides: glyphOverridesStripped,
|
|
108
|
+
},
|
|
109
|
+
warnings,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Load + parse a theme file from disk. Returns `{ parsed: null }`
|
|
114
|
+
* on missing-file or read-error so the caller can degrade gracefully.
|
|
115
|
+
*/
|
|
116
|
+
function loadThemeFile(filepath) {
|
|
117
|
+
let text;
|
|
118
|
+
try {
|
|
119
|
+
text = (0, node_fs_1.readFileSync)(filepath, 'utf8');
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return {
|
|
123
|
+
parsed: null,
|
|
124
|
+
warnings: [`Could not read theme file ${filepath}: ${err.message}`],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return parseThemeYaml(text);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Walk `node` recursively. For every string leaf, call `transform`
|
|
131
|
+
* to optionally validate / normalise it. If `transform` returns null
|
|
132
|
+
* the leaf is dropped (validation failure). Non-string non-object
|
|
133
|
+
* leaves trigger a warning and are dropped.
|
|
134
|
+
*/
|
|
135
|
+
function collectStringPaths(node, pathSoFar, out, warnings, transform) {
|
|
136
|
+
if (node === undefined || node === null)
|
|
137
|
+
return;
|
|
138
|
+
if (typeof node !== 'object') {
|
|
139
|
+
warnings.push(`Expected object at ${pathSoFar}; got ${typeof node}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(node)) {
|
|
143
|
+
warnings.push(`Array not supported at ${pathSoFar}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
for (const [k, v] of Object.entries(node)) {
|
|
147
|
+
const childPath = `${pathSoFar}.${k}`;
|
|
148
|
+
if (typeof v === 'string') {
|
|
149
|
+
const value = transform(v, childPath);
|
|
150
|
+
if (value !== null)
|
|
151
|
+
out[childPath] = value;
|
|
152
|
+
}
|
|
153
|
+
else if (typeof v === 'object' && v !== null) {
|
|
154
|
+
collectStringPaths(v, childPath, out, warnings, transform);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
warnings.push(`Unsupported leaf type at ${childPath}: ${typeof v}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/theme/themeRegistry.ts — v4.9.0 Slice 1a.
|
|
10
|
+
*
|
|
11
|
+
* Singleton holding the active theme's name + path. The actual token
|
|
12
|
+
* values live on the mutable `colors` / `glyphs` exports of
|
|
13
|
+
* `cli/v4/design/tokens.ts`; this registry orchestrates applying a
|
|
14
|
+
* parsed theme onto those singletons and notifying subscribers when
|
|
15
|
+
* a hot-reload happens.
|
|
16
|
+
*
|
|
17
|
+
* Subscribers are anything that wants to be notified when the theme
|
|
18
|
+
* changes (e.g. the SkinEngine, which caches resolved RGB tuples).
|
|
19
|
+
*
|
|
20
|
+
* Why a separate module: keeps the theme-loading state machine out of
|
|
21
|
+
* `tokens.ts` (which is pure data) and out of `cli/v4/` (so core code
|
|
22
|
+
* with no UI dependency can still consult the registry without a
|
|
23
|
+
* core → cli import).
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.BASELINE_GLYPHS = exports.BASELINE_COLORS = void 0;
|
|
27
|
+
exports.applyTheme = applyTheme;
|
|
28
|
+
exports.resetToDefault = resetToDefault;
|
|
29
|
+
exports.getCurrentName = getCurrentName;
|
|
30
|
+
exports.getActivePath = getActivePath;
|
|
31
|
+
exports.subscribe = subscribe;
|
|
32
|
+
const tokens_1 = require("../../../cli/v4/design/tokens");
|
|
33
|
+
Object.defineProperty(exports, "BASELINE_COLORS", { enumerable: true, get: function () { return tokens_1.BASELINE_COLORS; } });
|
|
34
|
+
Object.defineProperty(exports, "BASELINE_GLYPHS", { enumerable: true, get: function () { return tokens_1.BASELINE_GLYPHS; } });
|
|
35
|
+
let currentName = 'default';
|
|
36
|
+
let activePath = null;
|
|
37
|
+
const listeners = new Set();
|
|
38
|
+
/**
|
|
39
|
+
* Apply a parsed theme on top of the baseline. Restores baseline
|
|
40
|
+
* first (so successive `applyTheme` calls don't accumulate stale
|
|
41
|
+
* overrides), then walks each override path and writes the value
|
|
42
|
+
* into the live `colors` / `glyphs` singletons.
|
|
43
|
+
*/
|
|
44
|
+
function applyTheme(parsed, path = null) {
|
|
45
|
+
// Reset to baseline first — guarantees idempotency.
|
|
46
|
+
(0, tokens_1._restoreBaselineForTokens)();
|
|
47
|
+
for (const [dotted, value] of Object.entries(parsed.colorOverrides)) {
|
|
48
|
+
writePath(tokens_1.colors, dotted, value);
|
|
49
|
+
}
|
|
50
|
+
for (const [dotted, value] of Object.entries(parsed.glyphOverrides)) {
|
|
51
|
+
writePath(tokens_1.glyphs, dotted, value);
|
|
52
|
+
}
|
|
53
|
+
currentName = parsed.name || 'custom';
|
|
54
|
+
activePath = path;
|
|
55
|
+
notify();
|
|
56
|
+
}
|
|
57
|
+
/** Restore baseline token values and clear active theme metadata. */
|
|
58
|
+
function resetToDefault() {
|
|
59
|
+
(0, tokens_1._restoreBaselineForTokens)();
|
|
60
|
+
currentName = 'default';
|
|
61
|
+
activePath = null;
|
|
62
|
+
notify();
|
|
63
|
+
}
|
|
64
|
+
function getCurrentName() { return currentName; }
|
|
65
|
+
function getActivePath() { return activePath; }
|
|
66
|
+
/** Subscribe; returns an unsubscribe fn. */
|
|
67
|
+
function subscribe(fn) {
|
|
68
|
+
listeners.add(fn);
|
|
69
|
+
return () => { listeners.delete(fn); };
|
|
70
|
+
}
|
|
71
|
+
function notify() {
|
|
72
|
+
const snap = { name: currentName, activePath };
|
|
73
|
+
for (const fn of listeners) {
|
|
74
|
+
try {
|
|
75
|
+
fn(snap);
|
|
76
|
+
}
|
|
77
|
+
catch { /* listener crash must not break registry */ }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Write a value into a nested object via a dotted path. Creates
|
|
82
|
+
* intermediate keys as needed (won't happen with validated themes
|
|
83
|
+
* but defensive against schema drift). Last segment is the leaf
|
|
84
|
+
* write. Pure mutation on the input root.
|
|
85
|
+
*/
|
|
86
|
+
function writePath(root, dotted, value) {
|
|
87
|
+
const segments = dotted.split('.');
|
|
88
|
+
let node = root;
|
|
89
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
90
|
+
const seg = segments[i];
|
|
91
|
+
if (typeof node[seg] !== 'object' || node[seg] === null) {
|
|
92
|
+
node[seg] = {};
|
|
93
|
+
}
|
|
94
|
+
node = node[seg];
|
|
95
|
+
}
|
|
96
|
+
node[segments[segments.length - 1]] = value;
|
|
97
|
+
}
|