aiden-runtime 4.8.0 → 4.9.0

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.
Files changed (99) hide show
  1. package/README.md +88 -1
  2. package/dist/cli/v4/aidenCLI.js +35 -4
  3. package/dist/cli/v4/chatSession.js +43 -16
  4. package/dist/cli/v4/commands/daemon.js +47 -2
  5. package/dist/cli/v4/commands/daemonDoctor.js +212 -0
  6. package/dist/cli/v4/commands/daemonStatus.js +1 -1
  7. package/dist/cli/v4/commands/help.js +2 -0
  8. package/dist/cli/v4/commands/hooks.js +428 -0
  9. package/dist/cli/v4/commands/index.js +5 -1
  10. package/dist/cli/v4/commands/mcp.js +89 -1
  11. package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
  12. package/dist/cli/v4/commands/memory.js +702 -0
  13. package/dist/cli/v4/commands/recovery.js +1 -1
  14. package/dist/cli/v4/commands/skin.js +7 -0
  15. package/dist/cli/v4/commands/theme.js +217 -0
  16. package/dist/cli/v4/commands/trigger.js +1 -1
  17. package/dist/cli/v4/commands/update.js +14 -2
  18. package/dist/cli/v4/design/tokens.js +52 -4
  19. package/dist/cli/v4/display.js +102 -46
  20. package/dist/cli/v4/pasteIntercept.js +214 -70
  21. package/dist/cli/v4/replyRenderer.js +145 -5
  22. package/dist/cli/v4/skinEngine.js +67 -0
  23. package/dist/core/v4/aidenAgent.js +45 -2
  24. package/dist/core/v4/daemon/api/runs.js +131 -0
  25. package/dist/core/v4/daemon/bootstrap.js +368 -13
  26. package/dist/core/v4/daemon/db/migrations.js +169 -0
  27. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
  28. package/dist/core/v4/daemon/incarnationStore.js +47 -0
  29. package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
  30. package/dist/core/v4/daemon/runs/reclaim.js +88 -0
  31. package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
  32. package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
  33. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
  34. package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
  35. package/dist/core/v4/daemon/spans/spanStore.js +113 -0
  36. package/dist/core/v4/daemon/triggerBus.js +50 -19
  37. package/dist/core/v4/hooks/auditQuery.js +67 -0
  38. package/dist/core/v4/hooks/dispatcher.js +286 -0
  39. package/dist/core/v4/hooks/index.js +46 -0
  40. package/dist/core/v4/hooks/lifecycle.js +27 -0
  41. package/dist/core/v4/hooks/manifest.js +142 -0
  42. package/dist/core/v4/hooks/registry.js +149 -0
  43. package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
  44. package/dist/core/v4/hooks/toolHookGate.js +76 -0
  45. package/dist/core/v4/hooks/trust.js +14 -0
  46. package/dist/core/v4/identity/contextManager.js +83 -0
  47. package/dist/core/v4/identity/daemonId.js +85 -0
  48. package/dist/core/v4/identity/enforcement.js +103 -0
  49. package/dist/core/v4/identity/executionContext.js +153 -0
  50. package/dist/core/v4/identity/hookExecution.js +62 -0
  51. package/dist/core/v4/identity/httpContext.js +68 -0
  52. package/dist/core/v4/identity/ids.js +185 -0
  53. package/dist/core/v4/identity/index.js +60 -0
  54. package/dist/core/v4/identity/subprocessContext.js +98 -0
  55. package/dist/core/v4/identity/traceparent.js +114 -0
  56. package/dist/core/v4/logger/index.js +3 -1
  57. package/dist/core/v4/logger/logger.js +28 -1
  58. package/dist/core/v4/logger/redact.js +149 -0
  59. package/dist/core/v4/logger/sinks/fileSink.js +13 -0
  60. package/dist/core/v4/logger/sinks/stdSink.js +19 -1
  61. package/dist/core/v4/mcp/install/backup.js +78 -0
  62. package/dist/core/v4/mcp/install/clientPaths.js +90 -0
  63. package/dist/core/v4/mcp/install/clients.js +203 -0
  64. package/dist/core/v4/mcp/install/healthCheck.js +83 -0
  65. package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
  66. package/dist/core/v4/mcp/install/profiles.js +109 -0
  67. package/dist/core/v4/mcp/install/wslDetect.js +62 -0
  68. package/dist/core/v4/memory/namespaceRegistry.js +117 -0
  69. package/dist/core/v4/memory/projectRoot.js +76 -0
  70. package/dist/core/v4/memory/reviewer/index.js +162 -0
  71. package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
  72. package/dist/core/v4/memory/reviewer/prompt.js +105 -0
  73. package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
  74. package/dist/core/v4/memoryManager.js +57 -10
  75. package/dist/core/v4/paths.js +2 -0
  76. package/dist/core/v4/promptBuilder.js +6 -0
  77. package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
  78. package/dist/core/v4/theme/bundledThemes.js +106 -0
  79. package/dist/core/v4/theme/themeLoader.js +160 -0
  80. package/dist/core/v4/theme/themeRegistry.js +97 -0
  81. package/dist/core/v4/theme/themeWatcher.js +95 -0
  82. package/dist/core/v4/toolRegistry.js +71 -8
  83. package/dist/core/v4/update/executeInstall.js +10 -6
  84. package/dist/core/v4/update/installMethodDetect.js +7 -0
  85. package/dist/core/version.js +67 -2
  86. package/dist/moat/approvalEngine.js +4 -0
  87. package/dist/moat/memoryGuard.js +8 -1
  88. package/dist/providers/v4/anthropicAdapter.js +10 -4
  89. package/dist/tools/v4/backends/local.js +19 -2
  90. package/dist/tools/v4/sessions/recallSession.js +6 -1
  91. package/package.json +3 -3
  92. package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
  93. package/themes/default.yaml +52 -0
  94. package/themes/dracula.yaml +32 -0
  95. package/themes/light.yaml +32 -0
  96. package/themes/monochrome.yaml +31 -0
  97. package/themes/tokyo-night.yaml +32 -0
  98. package/dist/core/pluginSystem.js +0 -121
  99. package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
@@ -0,0 +1,114 @@
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/identity/traceparent.ts — v4.9.0 Slice 7.
10
+ *
11
+ * W3C Trace Context (https://www.w3.org/TR/trace-context/) parse +
12
+ * emit for the `traceparent` HTTP header. We DO NOT swap Aiden's
13
+ * typed-prefix `trc_<uuidv7>` ID; instead, an inbound trace's 32-hex
14
+ * traceId is stored alongside in `spans.external_trace_id` /
15
+ * `runs.external_trace_id` (schema v10). That gives us debuggable
16
+ * typed IDs internally while still letting an external caller
17
+ * correlate via W3C-standard headers.
18
+ *
19
+ * traceparent: 00-<32hex traceId>-<16hex spanId>-<2hex flags>
20
+ *
21
+ * Total length: 4 + 32 + 16 + 2 + 3 dashes = 55 chars exactly.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.parseTraceparent = parseTraceparent;
25
+ exports.emitTraceparent = emitTraceparent;
26
+ exports.stripPrefix = stripPrefix;
27
+ exports.validateExternalRequestId = validateExternalRequestId;
28
+ const RE_HEX_32 = /^[0-9a-f]{32}$/;
29
+ const RE_HEX_16 = /^[0-9a-f]{16}$/;
30
+ const RE_HEX_2 = /^[0-9a-f]{2}$/;
31
+ const ZERO_TRACE = '00000000000000000000000000000000';
32
+ const ZERO_SPAN = '0000000000000000';
33
+ /**
34
+ * Parse a `traceparent` header. Returns `null` on ANY validation
35
+ * failure — caller decides whether to log + generate fresh or fail.
36
+ *
37
+ * Per spec: a vendor MUST NOT propagate an invalid header. We follow
38
+ * the "ignore + start fresh" recommendation so a malformed upstream
39
+ * doesn't poison Aiden's trace.
40
+ */
41
+ function parseTraceparent(header) {
42
+ if (!header || typeof header !== 'string')
43
+ return null;
44
+ if (header.length !== 55)
45
+ return null;
46
+ const parts = header.split('-');
47
+ if (parts.length !== 4)
48
+ return null;
49
+ const [version, traceId, parentSpanId, flagsHex] = parts;
50
+ if (version !== '00')
51
+ return null;
52
+ if (!RE_HEX_32.test(traceId))
53
+ return null;
54
+ if (!RE_HEX_16.test(parentSpanId))
55
+ return null;
56
+ if (!RE_HEX_2.test(flagsHex))
57
+ return null;
58
+ if (traceId === ZERO_TRACE)
59
+ return null;
60
+ if (parentSpanId === ZERO_SPAN)
61
+ return null;
62
+ const flags = parseInt(flagsHex, 16);
63
+ if (!Number.isFinite(flags))
64
+ return null;
65
+ return { traceId, parentSpanId, flags };
66
+ }
67
+ /**
68
+ * Emit a `traceparent` header from Aiden ID components. The caller
69
+ * strips the typed prefix (`trc_` / `spn_`) and converts the dashless
70
+ * 32-char compact form to the W3C shape. (Aiden's compact UUIDv7 IS
71
+ * 32 lowercase hex chars, so it's directly W3C-compatible after the
72
+ * prefix is removed.)
73
+ *
74
+ * `sampled` defaults to `true` — we want downstream services to record
75
+ * the trace by default. Set to false for low-priority work.
76
+ */
77
+ function emitTraceparent(traceIdHex, spanIdHex, sampled = true) {
78
+ const t = stripPrefix(traceIdHex, 'trc_');
79
+ const s = stripPrefix(spanIdHex, 'spn_');
80
+ if (!RE_HEX_32.test(t)) {
81
+ throw new Error(`emitTraceparent: traceId must be 32 hex chars (got ${t.length})`);
82
+ }
83
+ // W3C spans are 16 hex; Aiden's span_id compact form is 32. Take
84
+ // the first 16 — UUIDv7's first 16 chars carry the ms timestamp +
85
+ // version + half of randomness, so collisions across one trace are
86
+ // astronomical.
87
+ const s16 = s.length === 16 ? s : s.slice(0, 16);
88
+ if (!RE_HEX_16.test(s16)) {
89
+ throw new Error(`emitTraceparent: spanId must be 16 hex chars (got ${s16.length})`);
90
+ }
91
+ const flags = sampled ? '01' : '00';
92
+ return `00-${t}-${s16}-${flags}`;
93
+ }
94
+ /** Strip the Aiden typed prefix (`trc_` / `spn_`) if present. */
95
+ function stripPrefix(id, prefix) {
96
+ return id.startsWith(prefix) ? id.slice(prefix.length) : id;
97
+ }
98
+ /**
99
+ * Validate an external `X-Request-Id` style header. Per the project
100
+ * rule "don't poison indexes": max 128 chars, ASCII printable only.
101
+ * Returns the value if safe, `null` if it should be dropped + a
102
+ * fresh id generated.
103
+ */
104
+ function validateExternalRequestId(raw) {
105
+ if (raw === undefined || raw === null)
106
+ return null;
107
+ if (typeof raw !== 'string')
108
+ return null;
109
+ if (raw.length === 0 || raw.length > 128)
110
+ return null;
111
+ if (!/^[\x20-\x7E]+$/.test(raw))
112
+ return null;
113
+ return raw;
114
+ }
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // core/v4/logger/index.ts — Phase v4.1-1.3a barrel export.
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.MultiSink = exports.MemorySink = exports.NullSink = exports.StdoutJsonSink = exports.StderrSink = exports.FileSink = exports.isReplActive = exports.markReplInactive = exports.markReplActive = exports.noopLogger = exports.createBootLogger = exports.CoreLogger = void 0;
4
+ exports.RedactingSink = exports.MultiSink = exports.MemorySink = exports.NullSink = exports.StdoutJsonSink = exports.StderrSink = exports.FileSink = exports.isReplActive = exports.markReplInactive = exports.markReplActive = exports.noopLogger = exports.createBootLogger = exports.CoreLogger = void 0;
5
5
  var logger_1 = require("./logger");
6
6
  Object.defineProperty(exports, "CoreLogger", { enumerable: true, get: function () { return logger_1.CoreLogger; } });
7
7
  var factory_1 = require("./factory");
@@ -20,3 +20,5 @@ Object.defineProperty(exports, "NullSink", { enumerable: true, get: function ()
20
20
  Object.defineProperty(exports, "MemorySink", { enumerable: true, get: function () { return nullSink_1.MemorySink; } });
21
21
  var multiSink_1 = require("./sinks/multiSink");
22
22
  Object.defineProperty(exports, "MultiSink", { enumerable: true, get: function () { return multiSink_1.MultiSink; } });
23
+ var redact_1 = require("./redact");
24
+ Object.defineProperty(exports, "RedactingSink", { enumerable: true, get: function () { return redact_1.RedactingSink; } });
@@ -37,11 +37,18 @@ class CoreLogger {
37
37
  /**
38
38
  * Construct a root logger. Use `child(segment)` for sub-loggers.
39
39
  * `sinks` may be empty — useful for tests; writes silently drop.
40
+ *
41
+ * v4.9.0 Slice 4 — `getContext` (optional) returns ambient fields
42
+ * (e.g. ExecutionContext + identity holders) merged into every
43
+ * record's `ctx`. Defaults to no-op. The Logger NEVER throws when
44
+ * the provider returns undefined or throws — that would defeat the
45
+ * point of an always-on diagnostic channel.
40
46
  */
41
47
  constructor(opts) {
42
48
  this.scope = opts.scope ?? '';
43
49
  this.sinks = opts.sinks;
44
50
  this.level = opts.level ?? 'debug';
51
+ this.getContext = opts.getContext;
45
52
  this.sinksOwner = {
46
53
  sinks: this.sinks,
47
54
  level: this.level,
@@ -57,6 +64,11 @@ class CoreLogger {
57
64
  sinks: parent.sinksOwner.sinks,
58
65
  level: parent.sinksOwner.level,
59
66
  sinksOwner: parent.sinksOwner,
67
+ // v4.9.0 Slice 4 — children inherit the parent's context provider
68
+ // so child-loggers stamped via `parent.child('foo')` carry the
69
+ // same identity fields without each sub-logger needing its own
70
+ // wiring.
71
+ getContext: parent.getContext,
60
72
  });
61
73
  return c;
62
74
  }
@@ -95,12 +107,27 @@ class CoreLogger {
95
107
  write(level, msg, ctx) {
96
108
  if (exports.LOG_LEVEL_ORDER[level] < exports.LOG_LEVEL_ORDER[this.sinksOwner.level])
97
109
  return;
110
+ // v4.9.0 Slice 4 — merge ambient context fields. Caller-supplied
111
+ // `ctx` wins on key collision (callers stamp run-specific data with
112
+ // intent; identity fields are the default backdrop). The provider
113
+ // is wrapped in try/catch — project rule "no log formatter throws
114
+ // because context is missing".
115
+ let merged = ctx;
116
+ if (this.getContext) {
117
+ try {
118
+ const ambient = this.getContext();
119
+ if (ambient && Object.keys(ambient).length > 0) {
120
+ merged = { ...ambient, ...(ctx ?? {}) };
121
+ }
122
+ }
123
+ catch { /* never let a context provider break logging */ }
124
+ }
98
125
  const record = {
99
126
  ts: new Date(),
100
127
  level,
101
128
  scope: this.scope,
102
129
  msg,
103
- ctx,
130
+ ctx: merged,
104
131
  };
105
132
  // Sinks must not throw — the helpers in ./sinks/* all wrap their
106
133
  // I/O in try/catch. Be defensive anyway. Phase v4.1.2-slice3:
@@ -0,0 +1,149 @@
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/logger/redact.ts — v4.9.0 Slice 3.
10
+ *
11
+ * `RedactingSink` wraps any inner `LoggerSink` and scrubs known secret
12
+ * shapes out of `msg` + `ctx` before the inner sink sees the record.
13
+ * It does NOT touch `ts`, `level`, or `scope` — those are operational
14
+ * metadata, never user-supplied.
15
+ *
16
+ * The patterns are intentionally conservative: false positives (a stray
17
+ * `[REDACTED]` in a debug line) are cheap, false negatives (a real key
18
+ * leaking to disk) are not. When in doubt, add a pattern; when a
19
+ * pattern overlaps with normal log content, narrow it.
20
+ *
21
+ * The decorator pattern composes naturally with the existing sinks
22
+ * (FileSink, StderrSink, StdoutJsonSink, MultiSink): wrap whatever the
23
+ * inner sink is, hand the wrapper to `new CoreLogger({ sinks: [...] })`.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports._SECRET_PATTERNS_FOR_TESTS = exports.RedactingSink = void 0;
27
+ /**
28
+ * Patterns are conservative: each requires either a recognisable
29
+ * prefix (sk-, AKIA, Bearer …) or a labelled context (api_key="…"),
30
+ * so plain user text like a 32-char hash slug doesn't accidentally
31
+ * trip them.
32
+ */
33
+ const SECRET_PATTERNS = [
34
+ { name: 'bearer',
35
+ regex: /Bearer\s+[A-Za-z0-9+/=._-]{20,}/g,
36
+ replacement: 'Bearer [REDACTED]' },
37
+ { name: 'sk-key',
38
+ regex: /sk-[a-zA-Z0-9_-]{20,}/g,
39
+ replacement: 'sk-[REDACTED]' },
40
+ { name: 'xoxp-key',
41
+ regex: /xox[pbar]-[a-zA-Z0-9-]{10,}/g,
42
+ replacement: 'xox*-[REDACTED]' },
43
+ { name: 'aws-akia',
44
+ regex: /AKIA[0-9A-Z]{16}/g,
45
+ replacement: 'AKIA[REDACTED]' },
46
+ { name: 'aws-asia',
47
+ regex: /ASIA[0-9A-Z]{16}/g,
48
+ replacement: 'ASIA[REDACTED]' },
49
+ { name: 'gcp-key',
50
+ regex: /AIza[0-9A-Za-z_-]{35}/g,
51
+ replacement: 'AIza[REDACTED]' },
52
+ { name: 'github',
53
+ regex: /gh[ps]_[A-Za-z0-9]{36,}/g,
54
+ replacement: 'gh*_[REDACTED]' },
55
+ // Generic "api_key = secret" / "token: secret" / "password=secret".
56
+ // The `\b` ensures we don't chew up part of a longer identifier; the
57
+ // 20+ char tail keeps short values (e.g. `password=admin`) out.
58
+ { name: 'generic-labelled',
59
+ regex: /\b(api[_-]?key|token|password|secret)(["':\s=]+)([A-Za-z0-9_-]{20,})/gi,
60
+ replacement: '$1$2[REDACTED]' },
61
+ ];
62
+ /** Apply every pattern to a single string. Cheap; called per record. */
63
+ function scrubString(s) {
64
+ let out = s;
65
+ for (const p of SECRET_PATTERNS) {
66
+ out = out.replace(p.regex, p.replacement);
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * Recursively scrub strings inside a structured payload. Numbers,
72
+ * booleans, null, undefined pass through. Cycles collapse to
73
+ * `[circular]` so a misbehaving caller can't OOM the sink.
74
+ *
75
+ * Depth is capped at 8 — deep enough for normal `ctx` shapes (error
76
+ * + stack + nested cause), shallow enough that pathological inputs
77
+ * don't burn CPU.
78
+ */
79
+ function scrubValue(v, seen, depth) {
80
+ if (depth > 8)
81
+ return '[depth-capped]';
82
+ if (v === null || v === undefined)
83
+ return v;
84
+ if (typeof v === 'string')
85
+ return scrubString(v);
86
+ if (typeof v === 'number' || typeof v === 'boolean' || typeof v === 'bigint')
87
+ return v;
88
+ if (typeof v === 'function' || typeof v === 'symbol')
89
+ return String(v);
90
+ if (v instanceof Error) {
91
+ return {
92
+ type: v.name,
93
+ message: scrubString(v.message),
94
+ stack: v.stack ? scrubString(v.stack) : undefined,
95
+ };
96
+ }
97
+ if (typeof v === 'object') {
98
+ if (seen.has(v))
99
+ return '[circular]';
100
+ seen.add(v);
101
+ if (Array.isArray(v)) {
102
+ return v.map((item) => scrubValue(item, seen, depth + 1));
103
+ }
104
+ const out = {};
105
+ for (const [k, val] of Object.entries(v)) {
106
+ out[k] = scrubValue(val, seen, depth + 1);
107
+ }
108
+ return out;
109
+ }
110
+ return v;
111
+ }
112
+ /**
113
+ * `LoggerSink` decorator. Construct with the inner sink you want to
114
+ * protect; `write()` produces a redacted copy of the record before
115
+ * delegating. Never mutates the input — the original `record.msg` /
116
+ * `record.ctx` are untouched (other sinks attached to the same
117
+ * `CoreLogger` see their own unredacted copies if they're not
118
+ * themselves wrapped).
119
+ *
120
+ * Operational metadata (`ts`, `level`, `scope`) bypasses redaction:
121
+ * those fields can't carry user input, and aggregators key on them.
122
+ */
123
+ class RedactingSink {
124
+ constructor(inner) {
125
+ this.inner = inner;
126
+ this.name = inner.name ? `redact:${inner.name}` : 'redact:anon';
127
+ }
128
+ write(record) {
129
+ const seen = new WeakSet();
130
+ const scrubbed = {
131
+ ts: record.ts,
132
+ level: record.level,
133
+ scope: record.scope,
134
+ msg: scrubString(record.msg),
135
+ ctx: record.ctx
136
+ ? scrubValue(record.ctx, seen, 0)
137
+ : undefined,
138
+ };
139
+ this.inner.write(scrubbed);
140
+ }
141
+ async close() {
142
+ if (typeof this.inner.close === 'function') {
143
+ await this.inner.close();
144
+ }
145
+ }
146
+ }
147
+ exports.RedactingSink = RedactingSink;
148
+ /** Test seam — pattern list shape is exposed for assertions. */
149
+ exports._SECRET_PATTERNS_FOR_TESTS = SECRET_PATTERNS.map((p) => ({ name: p.name }));
@@ -34,6 +34,8 @@ class FileSink {
34
34
  this.dirReady = false;
35
35
  this.dir = opts.dir;
36
36
  this.filePath = node_path_1.default.join(opts.dir, `${opts.name}.log`);
37
+ this.fmt = opts.format ?? 'human';
38
+ this.name = `file:${this.filePath}`;
37
39
  }
38
40
  write(record) {
39
41
  if (!this.ensureDir())
@@ -88,6 +90,17 @@ class FileSink {
88
90
  * 2026-05-08T01:32:50.001Z [warn] [channels.telegram] Polling 409 {"streak":1}
89
91
  */
90
92
  format(r) {
93
+ if (this.fmt === 'ndjson') {
94
+ const payload = {
95
+ ts: r.ts.toISOString(),
96
+ level: r.level,
97
+ scope: r.scope || undefined,
98
+ msg: r.msg,
99
+ };
100
+ if (r.ctx)
101
+ Object.assign(payload, r.ctx);
102
+ return safeJson(payload) + '\n';
103
+ }
91
104
  const scope = r.scope ? ` [${r.scope}]` : '';
92
105
  const ctx = r.ctx && Object.keys(r.ctx).length > 0
93
106
  ? ' ' + safeJson(r.ctx)
@@ -30,19 +30,37 @@ const LEVEL_THRESHOLD = {
30
30
  };
31
31
  class StderrSink {
32
32
  constructor(opts = {}) {
33
+ this.name = 'stderr';
33
34
  this.minLevel = LEVEL_THRESHOLD[opts.minLevel ?? 'warn'];
35
+ this.pretty = opts.pretty ?? false;
34
36
  }
35
37
  write(r) {
36
38
  if (LEVEL_THRESHOLD[r.level] < this.minLevel)
37
39
  return;
38
40
  const scope = r.scope ? ` [${r.scope}]` : '';
39
41
  try {
40
- process.stderr.write(`${r.ts.toISOString()} [${r.level}]${scope} ${r.msg}\n`);
42
+ if (this.pretty) {
43
+ const ts = formatShortTime(r.ts);
44
+ const runId = typeof r.ctx?.runId === 'string' ? r.ctx.runId : '';
45
+ // Dim ANSI 2 = faint; 22 = reset intensity. Suffix renders as
46
+ // ` (run_…last8)` only when an ambient runId is present.
47
+ const suffix = runId.length >= 8
48
+ ? ` \x1b[2m(${runId.slice(-8)})\x1b[22m`
49
+ : '';
50
+ process.stderr.write(`${ts} [${r.level}]${scope} ${r.msg}${suffix}\n`);
51
+ }
52
+ else {
53
+ process.stderr.write(`${r.ts.toISOString()} [${r.level}]${scope} ${r.msg}\n`);
54
+ }
41
55
  }
42
56
  catch { /* dropped */ }
43
57
  }
44
58
  }
45
59
  exports.StderrSink = StderrSink;
60
+ function formatShortTime(d) {
61
+ const pad = (n) => (n < 10 ? `0${n}` : String(n));
62
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
63
+ }
46
64
  /**
47
65
  * NDJSON stdout for `serve` mode. One record per line, structured
48
66
  * fields preserved. Aggregators love this; humans don't. Headless
@@ -0,0 +1,78 @@
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/mcp/install/backup.ts — v4.9.0 Slice 2a.
10
+ *
11
+ * Timestamped backup of a third-party client config before Aiden's
12
+ * `init` / `repair` writes to it. Backups are kept indefinitely so
13
+ * a user can recover if Aiden's entry breaks something downstream.
14
+ * Naming: `<configPath>.aiden-backup-YYYYMMDD-HHMMSS`.
15
+ */
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.backupConfig = backupConfig;
21
+ exports.countBackups = countBackups;
22
+ exports.findLatestBackup = findLatestBackup;
23
+ const node_fs_1 = require("node:fs");
24
+ const node_path_1 = __importDefault(require("node:path"));
25
+ /** Generate a timestamp suffix matching the YYYYMMDD-HHMMSS pattern. */
26
+ function nowStamp(date = new Date()) {
27
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
28
+ return (`${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}` +
29
+ `-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`);
30
+ }
31
+ /**
32
+ * Create a backup of `configPath`. Returns the backup path on success,
33
+ * or null if the source doesn't exist (first-time init — nothing to
34
+ * back up). Re-throws fs errors so the caller can abort cleanly.
35
+ */
36
+ function backupConfig(configPath, now = new Date()) {
37
+ if (!(0, node_fs_1.existsSync)(configPath))
38
+ return null;
39
+ const backupPath = `${configPath}.aiden-backup-${nowStamp(now)}`;
40
+ (0, node_fs_1.copyFileSync)(configPath, backupPath);
41
+ return backupPath;
42
+ }
43
+ /**
44
+ * Count existing backups for a given config path. Used by `doctor`
45
+ * to report backup count + by tests.
46
+ */
47
+ function countBackups(configPath) {
48
+ const dir = node_path_1.default.dirname(configPath);
49
+ const base = node_path_1.default.basename(configPath);
50
+ if (!(0, node_fs_1.existsSync)(dir))
51
+ return 0;
52
+ try {
53
+ return (0, node_fs_1.readdirSync)(dir).filter((f) => f.startsWith(`${base}.aiden-backup-`)).length;
54
+ }
55
+ catch {
56
+ return 0;
57
+ }
58
+ }
59
+ /**
60
+ * Find the newest backup matching `<configPath>.aiden-backup-*`.
61
+ * Used by `repair` when restoring from a corrupted edit.
62
+ */
63
+ function findLatestBackup(configPath) {
64
+ const dir = node_path_1.default.dirname(configPath);
65
+ const base = node_path_1.default.basename(configPath);
66
+ if (!(0, node_fs_1.existsSync)(dir))
67
+ return null;
68
+ try {
69
+ const files = (0, node_fs_1.readdirSync)(dir).filter((f) => f.startsWith(`${base}.aiden-backup-`));
70
+ if (files.length === 0)
71
+ return null;
72
+ files.sort(); // timestamp suffix sorts lexicographically = newest last
73
+ return node_path_1.default.join(dir, files[files.length - 1]);
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
@@ -0,0 +1,90 @@
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/mcp/install/clientPaths.ts — v4.9.0 Slice 2a.
10
+ *
11
+ * Per-client per-OS config-file path discovery. No hardcoded user
12
+ * paths — every path is built from `os.homedir()` / `process.env`
13
+ * at call time so tests can override via the env.
14
+ */
15
+ var __importDefault = (this && this.__importDefault) || function (mod) {
16
+ return (mod && mod.__esModule) ? mod : { "default": mod };
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.resolveClientPath = resolveClientPath;
20
+ const node_os_1 = __importDefault(require("node:os"));
21
+ const node_path_1 = __importDefault(require("node:path"));
22
+ /**
23
+ * Resolve the config path for `clientId` on the host platform. Pass a
24
+ * custom env / platform / homedir override for tests.
25
+ */
26
+ const MCP_SERVERS_SCHEMA = { topKey: 'mcpServers' };
27
+ const VSCODE_SCHEMA = { topKey: 'servers', requiresType: true };
28
+ function resolveClientPath(clientId, opts = {}) {
29
+ const platform = opts.platform ?? process.platform;
30
+ const homedir = opts.homedir ?? node_os_1.default.homedir();
31
+ const env = opts.env ?? process.env;
32
+ const cwd = opts.cwd ?? process.cwd();
33
+ if (clientId === 'vscode') {
34
+ // v4.9.0 Slice 2b — workspace-only config. The user must run
35
+ // `aiden mcp init vscode` from inside a VS Code project (where
36
+ // `.vscode/` is meaningful). We don't fabricate a global path;
37
+ // VS Code doesn't ship a stable global MCP config location.
38
+ const wsDir = node_path_1.default.join(cwd, '.vscode');
39
+ const wsFile = node_path_1.default.join(wsDir, 'mcp.json');
40
+ return {
41
+ configPath: wsFile,
42
+ parentDir: wsDir,
43
+ displayName: 'VS Code (workspace)',
44
+ format: 'jsonc',
45
+ schema: VSCODE_SCHEMA,
46
+ };
47
+ }
48
+ if (clientId === 'claude') {
49
+ if (platform === 'darwin') {
50
+ const dir = node_path_1.default.join(homedir, 'Library', 'Application Support', 'Claude');
51
+ return {
52
+ configPath: node_path_1.default.join(dir, 'claude_desktop_config.json'),
53
+ parentDir: dir,
54
+ displayName: 'Claude Desktop',
55
+ format: 'json',
56
+ schema: MCP_SERVERS_SCHEMA,
57
+ };
58
+ }
59
+ if (platform === 'win32') {
60
+ const appData = env.APPDATA ?? node_path_1.default.join(homedir, 'AppData', 'Roaming');
61
+ const dir = node_path_1.default.join(appData, 'Claude');
62
+ return {
63
+ configPath: node_path_1.default.join(dir, 'claude_desktop_config.json'),
64
+ parentDir: dir,
65
+ displayName: 'Claude Desktop',
66
+ format: 'json',
67
+ schema: MCP_SERVERS_SCHEMA,
68
+ };
69
+ }
70
+ // Linux / others — Claude Desktop has no official Linux build.
71
+ return {
72
+ configPath: node_path_1.default.join(homedir, '.config', 'Claude', 'claude_desktop_config.json'),
73
+ parentDir: node_path_1.default.join(homedir, '.config', 'Claude'),
74
+ displayName: 'Claude Desktop',
75
+ format: 'json',
76
+ schema: MCP_SERVERS_SCHEMA,
77
+ unsupportedOs: true,
78
+ };
79
+ }
80
+ // Cursor — same `~/.cursor/mcp.json` layout across all three OSes.
81
+ // Windows uses USERPROFILE if HOME isn't set; we already resolved
82
+ // via homedir which handles both.
83
+ return {
84
+ configPath: node_path_1.default.join(homedir, '.cursor', 'mcp.json'),
85
+ parentDir: node_path_1.default.join(homedir, '.cursor'),
86
+ displayName: 'Cursor',
87
+ format: 'jsonc',
88
+ schema: MCP_SERVERS_SCHEMA,
89
+ };
90
+ }