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,203 @@
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/clients.ts — v4.9.0 Slice 2a.
10
+ *
11
+ * Single per-client adapter exposing read / write / install /
12
+ * uninstall helpers above the path + JSONC primitives. Claude Desktop
13
+ * and Cursor share most of the install flow; only the on-disk format
14
+ * (`json` vs `jsonc`) and the per-OS path differ — both already
15
+ * captured in `clientPaths.ts`.
16
+ */
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.readClient = readClient;
22
+ exports.planInstall = planInstall;
23
+ exports.installClient = installClient;
24
+ exports.uninstallClient = uninstallClient;
25
+ exports.planUninstall = planUninstall;
26
+ const node_fs_1 = require("node:fs");
27
+ const node_path_1 = __importDefault(require("node:path"));
28
+ const clientPaths_1 = require("./clientPaths");
29
+ const jsoncMerge_1 = require("./jsoncMerge");
30
+ const backup_1 = require("./backup");
31
+ /**
32
+ * Read the current Aiden entry from a client's config. Returns null
33
+ * when the file doesn't exist OR the entry is absent.
34
+ */
35
+ function readClient(clientId, override) {
36
+ const resolution = override ?? (0, clientPaths_1.resolveClientPath)(clientId);
37
+ if (!(0, node_fs_1.existsSync)(resolution.configPath)) {
38
+ return { resolution, entry: null, exists: false, text: null };
39
+ }
40
+ const text = (0, node_fs_1.readFileSync)(resolution.configPath, 'utf8');
41
+ return { resolution, entry: (0, jsoncMerge_1.readAidenEntry)(text, resolution.schema), exists: true, text };
42
+ }
43
+ /**
44
+ * Compute the new file content without writing. Used by `--dry-run`.
45
+ * Returns null when the parent directory doesn't exist (client not
46
+ * installed); caller surfaces this as a user-facing error.
47
+ */
48
+ function planInstall(clientId, opts) {
49
+ const resolution = opts.pathOverride ?? (0, clientPaths_1.resolveClientPath)(clientId);
50
+ if (!(0, node_fs_1.existsSync)(resolution.parentDir)) {
51
+ return { resolution, newText: '', parentMissing: true };
52
+ }
53
+ const existingText = (0, node_fs_1.existsSync)(resolution.configPath)
54
+ ? (0, node_fs_1.readFileSync)(resolution.configPath, 'utf8')
55
+ : (0, jsoncMerge_1.emptyConfig)(resolution.format, resolution.schema);
56
+ const entry = (0, jsoncMerge_1.buildAidenEntryObject)({
57
+ command: opts.command,
58
+ args: opts.args,
59
+ envKeys: opts.envKeys,
60
+ profile: opts.profile,
61
+ schema: resolution.schema,
62
+ });
63
+ const newText = (0, jsoncMerge_1.mergeAidenEntry)(existingText, entry, resolution.format, resolution.schema);
64
+ return { resolution, newText, parentMissing: false };
65
+ }
66
+ /**
67
+ * Atomic write: tmp file → rename. Caller has already backed up the
68
+ * original via `backupConfig`.
69
+ */
70
+ function atomicWrite(filepath, content) {
71
+ const dir = node_path_1.default.dirname(filepath);
72
+ if (!(0, node_fs_1.existsSync)(dir))
73
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
74
+ const tmp = `${filepath}.tmp-${process.pid}-${Date.now()}`;
75
+ (0, node_fs_1.writeFileSync)(tmp, content, 'utf8');
76
+ (0, node_fs_1.renameSync)(tmp, filepath);
77
+ }
78
+ /**
79
+ * Full install flow: backup → merge → atomic write. Skips the write
80
+ * (and skips creating a backup) when the existing entry already
81
+ * matches the canonical form.
82
+ */
83
+ function installClient(clientId, opts) {
84
+ const planned = planInstall(clientId, opts);
85
+ if (!planned) {
86
+ return {
87
+ outcome: 'error',
88
+ configPath: '',
89
+ backupPath: null,
90
+ error: `Could not resolve config path for ${clientId}`,
91
+ };
92
+ }
93
+ if (planned.parentMissing) {
94
+ return {
95
+ outcome: 'error',
96
+ configPath: planned.resolution.configPath,
97
+ backupPath: null,
98
+ error: `${planned.resolution.displayName} not installed (parent dir ${planned.resolution.parentDir} missing).`,
99
+ };
100
+ }
101
+ // Idempotency: if the file already exists and its content is byte-
102
+ // identical to what we'd write, skip with no backup churn.
103
+ if ((0, node_fs_1.existsSync)(planned.resolution.configPath)) {
104
+ const currentText = (0, node_fs_1.readFileSync)(planned.resolution.configPath, 'utf8');
105
+ if (currentText === planned.newText) {
106
+ return {
107
+ outcome: 'noop',
108
+ configPath: planned.resolution.configPath,
109
+ backupPath: null,
110
+ };
111
+ }
112
+ }
113
+ let backupPath = null;
114
+ try {
115
+ backupPath = (0, backup_1.backupConfig)(planned.resolution.configPath);
116
+ atomicWrite(planned.resolution.configPath, planned.newText);
117
+ return {
118
+ outcome: 'written',
119
+ configPath: planned.resolution.configPath,
120
+ backupPath,
121
+ };
122
+ }
123
+ catch (err) {
124
+ return {
125
+ outcome: 'error',
126
+ configPath: planned.resolution.configPath,
127
+ backupPath,
128
+ error: err.message,
129
+ };
130
+ }
131
+ }
132
+ /**
133
+ * v4.9.0 Slice 2b — surgical removal of `<topKey>.aiden` from a
134
+ * client's config. Preserves every other server entry + every other
135
+ * top-level key. Creates a timestamped backup before writing.
136
+ *
137
+ * Returns `outcome: 'noop'` when the config file is missing or the
138
+ * entry isn't present — uninstall is idempotent.
139
+ */
140
+ function uninstallClient(clientId, override) {
141
+ const resolution = override ?? (0, clientPaths_1.resolveClientPath)(clientId);
142
+ if (!(0, node_fs_1.existsSync)(resolution.configPath)) {
143
+ return {
144
+ outcome: 'noop',
145
+ configPath: resolution.configPath,
146
+ backupPath: null,
147
+ wasManaged: false,
148
+ };
149
+ }
150
+ const existingText = (0, node_fs_1.readFileSync)(resolution.configPath, 'utf8');
151
+ const existingEntry = (0, jsoncMerge_1.readAidenEntry)(existingText, resolution.schema);
152
+ if (!existingEntry) {
153
+ return {
154
+ outcome: 'noop',
155
+ configPath: resolution.configPath,
156
+ backupPath: null,
157
+ wasManaged: false,
158
+ };
159
+ }
160
+ const { text: newText, removed } = (0, jsoncMerge_1.removeAidenEntry)(existingText, resolution.format, resolution.schema);
161
+ if (!removed) {
162
+ return {
163
+ outcome: 'noop',
164
+ configPath: resolution.configPath,
165
+ backupPath: null,
166
+ wasManaged: existingEntry._aiden?.managed === true,
167
+ };
168
+ }
169
+ let backupPath = null;
170
+ try {
171
+ backupPath = (0, backup_1.backupConfig)(resolution.configPath);
172
+ atomicWrite(resolution.configPath, newText);
173
+ return {
174
+ outcome: 'removed',
175
+ configPath: resolution.configPath,
176
+ backupPath,
177
+ wasManaged: existingEntry._aiden?.managed === true,
178
+ };
179
+ }
180
+ catch (err) {
181
+ return {
182
+ outcome: 'error',
183
+ configPath: resolution.configPath,
184
+ backupPath,
185
+ wasManaged: existingEntry._aiden?.managed === true,
186
+ error: err.message,
187
+ };
188
+ }
189
+ }
190
+ /** Preview uninstall without writing. */
191
+ function planUninstall(clientId, override) {
192
+ const resolution = override ?? (0, clientPaths_1.resolveClientPath)(clientId);
193
+ if (!(0, node_fs_1.existsSync)(resolution.configPath)) {
194
+ return { resolution, entry: null, newText: '', willRemove: false };
195
+ }
196
+ const existingText = (0, node_fs_1.readFileSync)(resolution.configPath, 'utf8');
197
+ const entry = (0, jsoncMerge_1.readAidenEntry)(existingText, resolution.schema);
198
+ if (!entry) {
199
+ return { resolution, entry: null, newText: existingText, willRemove: false };
200
+ }
201
+ const { text, removed } = (0, jsoncMerge_1.removeAidenEntry)(existingText, resolution.format, resolution.schema);
202
+ return { resolution, entry, newText: text, willRemove: removed };
203
+ }
@@ -0,0 +1,83 @@
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/healthCheck.ts — v4.9.0 Slice 2a.
10
+ *
11
+ * Spawn `aiden mcp serve --health-check` (or whatever command the
12
+ * caller writes into the third-party config) as a subprocess and
13
+ * parse the JSON status line from stdout. Used by `init` after a
14
+ * successful write to surface "tools exposed: N, version: X" as
15
+ * immediate confirmation that the wired entry actually launches.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.runHealthCheck = runHealthCheck;
19
+ const node_child_process_1 = require("node:child_process");
20
+ const DEFAULT_TIMEOUT_MS = 5000;
21
+ function runHealthCheck(opts) {
22
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
23
+ const spawnFn = opts.spawnImpl ?? node_child_process_1.spawn;
24
+ return new Promise((resolve) => {
25
+ let child;
26
+ try {
27
+ // Append --health-check so the spawned `aiden mcp serve`
28
+ // emits a single JSON line + exits cleanly instead of
29
+ // running the stdio MCP loop.
30
+ child = spawnFn(opts.command, [...opts.args, '--health-check'], {
31
+ stdio: ['ignore', 'pipe', 'pipe'],
32
+ });
33
+ }
34
+ catch (err) {
35
+ resolve({ ok: false, error: `Could not spawn ${opts.command}: ${err.message}` });
36
+ return;
37
+ }
38
+ let stdoutBuf = '';
39
+ let stderrBuf = '';
40
+ child.stdout?.on('data', (chunk) => {
41
+ stdoutBuf += chunk.toString();
42
+ });
43
+ child.stderr?.on('data', (chunk) => {
44
+ stderrBuf += chunk.toString();
45
+ });
46
+ let timedOut = false;
47
+ const timer = setTimeout(() => {
48
+ timedOut = true;
49
+ try {
50
+ child.kill('SIGTERM');
51
+ }
52
+ catch { /* noop */ }
53
+ }, timeoutMs);
54
+ child.on('error', (err) => {
55
+ clearTimeout(timer);
56
+ resolve({ ok: false, error: `Spawn failed: ${err.message}` });
57
+ });
58
+ child.on('close', () => {
59
+ clearTimeout(timer);
60
+ if (timedOut) {
61
+ resolve({ ok: false, error: `Health check timed out after ${timeoutMs}ms` });
62
+ return;
63
+ }
64
+ try {
65
+ const trimmed = stdoutBuf.trim();
66
+ const line = trimmed.split('\n').find((l) => l.startsWith('{')) ?? trimmed;
67
+ const parsed = JSON.parse(line);
68
+ if (parsed.status === 'ok') {
69
+ resolve({ ok: true, status: 'ok', tools: parsed.tools, version: parsed.version });
70
+ }
71
+ else {
72
+ resolve({ ok: false, error: `status=${parsed.status ?? '<missing>'}` });
73
+ }
74
+ }
75
+ catch (err) {
76
+ resolve({
77
+ ok: false,
78
+ error: `Could not parse health-check output: ${err.message}. stderr: ${stderrBuf.trim().slice(0, 200)}`,
79
+ });
80
+ }
81
+ });
82
+ });
83
+ }
@@ -0,0 +1,174 @@
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/jsoncMerge.ts — v4.9.0 Slice 2a.
10
+ *
11
+ * Merge Aiden's `mcpServers.aiden` entry into a third-party client
12
+ * config file. Two formats:
13
+ *
14
+ * format: 'json' — Claude Desktop's claude_desktop_config.json,
15
+ * plain JSON. Round-trip via JSON.parse +
16
+ * JSON.stringify with 2-space indent.
17
+ *
18
+ * format: 'jsonc' — Cursor's mcp.json, JSON-with-comments. Round-
19
+ * tripping through JSON.parse destroys user
20
+ * comments + custom formatting. We use
21
+ * `jsonc-parser`'s `modify()` + `applyEdits()`
22
+ * to make a surgical edit that preserves the
23
+ * rest of the file verbatim.
24
+ *
25
+ * Either path is atomic from the caller's POV: this module returns
26
+ * the NEW file content as a string; the caller is responsible for
27
+ * tmp-file-then-rename.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.buildAidenEntryObject = buildAidenEntryObject;
31
+ exports.emptyConfig = emptyConfig;
32
+ exports.mergeAidenEntry = mergeAidenEntry;
33
+ exports.removeAidenEntry = removeAidenEntry;
34
+ exports.readAidenEntry = readAidenEntry;
35
+ const jsonc_parser_1 = require("jsonc-parser");
36
+ const DEFAULT_SCHEMA = { topKey: 'mcpServers' };
37
+ /** Produce the canonical Aiden entry. */
38
+ function buildAidenEntryObject(opts) {
39
+ const entry = {
40
+ command: opts.command,
41
+ args: opts.args,
42
+ _aiden: {
43
+ managed: true,
44
+ version: 1,
45
+ ...(opts.profile ? { profile: opts.profile } : {}),
46
+ },
47
+ };
48
+ if (opts.schema?.requiresType) {
49
+ entry.type = 'stdio';
50
+ }
51
+ if (opts.envKeys && opts.envKeys.length > 0) {
52
+ entry.env = {};
53
+ for (const k of opts.envKeys)
54
+ entry.env[k] = `\${${k}}`;
55
+ }
56
+ return entry;
57
+ }
58
+ /**
59
+ * Build the empty starter content for a brand-new config file. Used
60
+ * when a client's parent dir exists but the config file doesn't.
61
+ * The starter mirrors the client's top-level schema so the next
62
+ * `modify()` call has a stable shape to edit into.
63
+ */
64
+ function emptyConfig(format, schema = DEFAULT_SCHEMA) {
65
+ void format;
66
+ return `{\n "${schema.topKey}": {}\n}\n`;
67
+ }
68
+ /**
69
+ * Merge `entry` into the existing JSON / JSONC text under
70
+ * `<topKey>.aiden` (where `topKey` is `'mcpServers'` for Claude/Cursor
71
+ * or `'servers'` for VS Code). Returns the new text. Existing
72
+ * siblings under `<topKey>.*` are preserved untouched; other top-
73
+ * level keys (Claude Desktop has many) are preserved untouched.
74
+ *
75
+ * v4.9.0 Slice 2b — `schema` parameter added (default keeps Slice 2a
76
+ * mcpServers behaviour). VS Code passes `{ topKey: 'servers' }`.
77
+ */
78
+ function mergeAidenEntry(existingText, entry, format, schema = DEFAULT_SCHEMA) {
79
+ const topKey = schema.topKey;
80
+ if (format === 'json') {
81
+ let doc;
82
+ try {
83
+ doc = JSON.parse(existingText);
84
+ }
85
+ catch {
86
+ doc = {};
87
+ }
88
+ if (typeof doc !== 'object' || doc === null)
89
+ doc = {};
90
+ const servers = doc[topKey] ?? {};
91
+ servers.aiden = entry;
92
+ doc[topKey] = servers;
93
+ return JSON.stringify(doc, null, 2) + '\n';
94
+ }
95
+ // JSONC path: use modify() to produce a minimal edit that preserves
96
+ // comments + formatting outside the touched key path.
97
+ const formatOpts = { tabSize: 2, insertSpaces: true };
98
+ const path = [topKey, 'aiden'];
99
+ const tree = (0, jsonc_parser_1.parseTree)(existingText);
100
+ const root = tree;
101
+ let text = existingText;
102
+ if (!root || root.type !== 'object') {
103
+ text = emptyConfig('jsonc', schema);
104
+ }
105
+ else {
106
+ const mcpNode = (0, jsonc_parser_1.findNodeAtLocation)(root, [topKey]);
107
+ if (!mcpNode || mcpNode.type !== 'object') {
108
+ const edits = (0, jsonc_parser_1.modify)(text, [topKey], {}, { formattingOptions: formatOpts });
109
+ text = (0, jsonc_parser_1.applyEdits)(text, edits);
110
+ }
111
+ }
112
+ const edits = (0, jsonc_parser_1.modify)(text, path, entry, {
113
+ formattingOptions: formatOpts,
114
+ });
115
+ return (0, jsonc_parser_1.applyEdits)(text, edits);
116
+ }
117
+ /**
118
+ * Remove the Aiden entry from the existing text. Returns the new
119
+ * text + a boolean reporting whether anything was actually removed.
120
+ * Preserves all other `<topKey>.*` siblings + other top-level keys.
121
+ */
122
+ function removeAidenEntry(existingText, format, schema = DEFAULT_SCHEMA) {
123
+ const topKey = schema.topKey;
124
+ if (format === 'json') {
125
+ let doc;
126
+ try {
127
+ doc = JSON.parse(existingText);
128
+ }
129
+ catch {
130
+ return { text: existingText, removed: false };
131
+ }
132
+ if (typeof doc !== 'object' || doc === null) {
133
+ return { text: existingText, removed: false };
134
+ }
135
+ const servers = doc[topKey];
136
+ if (!servers || typeof servers !== 'object' || !('aiden' in servers)) {
137
+ return { text: existingText, removed: false };
138
+ }
139
+ delete servers.aiden;
140
+ doc[topKey] = servers;
141
+ return { text: JSON.stringify(doc, null, 2) + '\n', removed: true };
142
+ }
143
+ const tree = (0, jsonc_parser_1.parseTree)(existingText);
144
+ if (!tree)
145
+ return { text: existingText, removed: false };
146
+ const aidenNode = (0, jsonc_parser_1.findNodeAtLocation)(tree, [topKey, 'aiden']);
147
+ if (!aidenNode)
148
+ return { text: existingText, removed: false };
149
+ const edits = (0, jsonc_parser_1.modify)(existingText, [topKey, 'aiden'], undefined, {
150
+ formattingOptions: { tabSize: 2, insertSpaces: true },
151
+ });
152
+ return { text: (0, jsonc_parser_1.applyEdits)(existingText, edits), removed: true };
153
+ }
154
+ /**
155
+ * Read the current Aiden entry (or null when absent) from text.
156
+ * Tolerates both formats; jsonc-parser handles plain JSON too.
157
+ * Pass `schema` for clients whose topKey differs from the default
158
+ * `mcpServers` (e.g. VS Code's `servers`).
159
+ */
160
+ function readAidenEntry(existingText, schema = DEFAULT_SCHEMA) {
161
+ const tree = (0, jsonc_parser_1.parseTree)(existingText);
162
+ if (!tree)
163
+ return null;
164
+ const node = (0, jsonc_parser_1.findNodeAtLocation)(tree, [schema.topKey, 'aiden']);
165
+ if (!node)
166
+ return null;
167
+ try {
168
+ const segment = existingText.slice(node.offset, node.offset + node.length);
169
+ return JSON.parse(segment);
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
@@ -0,0 +1,109 @@
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/profiles.ts — v4.9.0 Slice 2b.
10
+ *
11
+ * Five user-facing profile names → three distinct tool allowlists.
12
+ * `browser` aliases to `general`; `research` aliases to `readonly`.
13
+ * Aliases reserve the namespace for future divergence without forcing
14
+ * users to relearn the command surface today.
15
+ *
16
+ * Profiles bridge to the existing toolBridge filter: at serve start,
17
+ * the CLI resolves the profile, then sets `AIDEN_MCP_ALLOW_DESTRUCTIVE`
18
+ * + `AIDEN_MCP_TOOL_ALLOWLIST` in the runtime env BEFORE
19
+ * `readToolBridgeEnv()` runs. `--profile <name>` always wins over
20
+ * inherited env values so the client-config-pinned profile is
21
+ * authoritative.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.PROFILE_NAMES = exports.CLIENT_DEFAULT_PROFILE = exports.PROFILES = void 0;
25
+ exports.resolveProfile = resolveProfile;
26
+ exports.applyProfileToEnv = applyProfileToEnv;
27
+ const GENERAL = {
28
+ name: 'general',
29
+ description: 'All non-destructive tools (default for chat clients like Claude Desktop).',
30
+ tools: 'all-default',
31
+ allowDestructive: false,
32
+ };
33
+ const DEV = {
34
+ name: 'dev',
35
+ description: 'Developer tools: file ops, shell, code execution, browser, web.',
36
+ tools: [
37
+ 'file_read', 'file_list', 'file_write', 'file_patch',
38
+ 'shell_exec', 'execute_code', 'system_info',
39
+ 'browser_navigate', 'browser_extract', 'browser_screenshot',
40
+ 'web_search', 'fetch_url', 'fetch_page',
41
+ 'skills_list', 'skill_view', 'spawn_sub_agent',
42
+ 'session_search', 'recall_session',
43
+ ],
44
+ allowDestructive: true,
45
+ };
46
+ const READONLY = {
47
+ name: 'readonly',
48
+ description: 'Read-only: web/research/skills/memory inspection, no mutations.',
49
+ tools: [
50
+ 'web_search', 'fetch_url', 'fetch_page', 'deep_research', 'youtube_search',
51
+ 'file_read', 'file_list',
52
+ 'skills_list', 'skill_view',
53
+ 'session_search', 'recall_session',
54
+ 'system_info',
55
+ ],
56
+ allowDestructive: false,
57
+ };
58
+ /** All 5 user-facing names. `browser` / `research` alias for now. */
59
+ exports.PROFILES = {
60
+ general: GENERAL,
61
+ dev: DEV,
62
+ readonly: READONLY,
63
+ // Aliases — same object reference today; namespace reserved for
64
+ // future divergence (e.g. browser could drop file_read in v5).
65
+ browser: GENERAL,
66
+ research: READONLY,
67
+ };
68
+ /** Client → default profile when --profile is omitted. */
69
+ exports.CLIENT_DEFAULT_PROFILE = {
70
+ claude: 'general', // chat client
71
+ cursor: 'dev', // code editor
72
+ vscode: 'dev', // code editor with workspace context
73
+ };
74
+ exports.PROFILE_NAMES = Object.keys(exports.PROFILES);
75
+ /**
76
+ * Resolve a profile by explicit name or by client default. Throws
77
+ * on an unknown name with a clear "Available: ..." hint.
78
+ */
79
+ function resolveProfile(name, clientId) {
80
+ if (name) {
81
+ const p = exports.PROFILES[name];
82
+ if (!p) {
83
+ throw new Error(`Unknown profile '${name}'. Available: ${exports.PROFILE_NAMES.join(', ')}`);
84
+ }
85
+ return p;
86
+ }
87
+ const fallback = exports.CLIENT_DEFAULT_PROFILE[clientId] ?? 'general';
88
+ return exports.PROFILES[fallback];
89
+ }
90
+ /**
91
+ * Apply a resolved profile to the env so `readToolBridgeEnv()` picks
92
+ * it up. Used by `aiden mcp serve --profile <name>` at startup.
93
+ *
94
+ * Pure mutation on the passed-in env object (defaults to
95
+ * `process.env`). Returns the previous values so callers can restore
96
+ * for tests; production code never restores.
97
+ */
98
+ function applyProfileToEnv(profile, env = process.env) {
99
+ const allowlistBefore = env.AIDEN_MCP_TOOL_ALLOWLIST;
100
+ const destructiveBefore = env.AIDEN_MCP_ALLOW_DESTRUCTIVE;
101
+ env.AIDEN_MCP_ALLOW_DESTRUCTIVE = profile.allowDestructive ? '1' : '0';
102
+ if (profile.tools === 'all-default') {
103
+ delete env.AIDEN_MCP_TOOL_ALLOWLIST;
104
+ }
105
+ else {
106
+ env.AIDEN_MCP_TOOL_ALLOWLIST = profile.tools.join(',');
107
+ }
108
+ return { allowlistBefore, destructiveBefore };
109
+ }
@@ -0,0 +1,62 @@
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/wslDetect.ts — v4.9.0 Slice 2a.
10
+ *
11
+ * Detect whether Aiden is running inside WSL. WSL users targeting a
12
+ * Windows-host MCP client (Claude Desktop / Cursor for Windows) need
13
+ * a `wsl.exe -d <distro> -- aiden mcp serve` command in the JSON
14
+ * entry, NOT a Linux path.
15
+ *
16
+ * Detection signals (any match → WSL):
17
+ * 1. `process.env.WSL_DISTRO_NAME` is set
18
+ * 2. `process.env.WSL_INTEROP` is set
19
+ * 3. `/proc/version` content contains "microsoft" or "WSL"
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.detectWsl = detectWsl;
23
+ exports.buildAidenEntry = buildAidenEntry;
24
+ const node_fs_1 = require("node:fs");
25
+ function detectWsl(opts = {}) {
26
+ const env = opts.env ?? process.env;
27
+ const readFile = opts.readFile ?? ((p) => (0, node_fs_1.readFileSync)(p, 'utf8'));
28
+ if (typeof env.WSL_DISTRO_NAME === 'string' && env.WSL_DISTRO_NAME.length > 0) {
29
+ return { inWsl: true, distro: env.WSL_DISTRO_NAME };
30
+ }
31
+ if (typeof env.WSL_INTEROP === 'string' && env.WSL_INTEROP.length > 0) {
32
+ return { inWsl: true, distro: env.WSL_DISTRO_NAME ?? null };
33
+ }
34
+ try {
35
+ const content = readFile('/proc/version').toLowerCase();
36
+ if (content.includes('microsoft') || content.includes('wsl')) {
37
+ return { inWsl: true, distro: env.WSL_DISTRO_NAME ?? null };
38
+ }
39
+ }
40
+ catch {
41
+ /* /proc/version unreadable — not on Linux, definitely not WSL */
42
+ }
43
+ return { inWsl: false, distro: null };
44
+ }
45
+ /**
46
+ * Build the `command` + `args` shape for the Aiden entry given the
47
+ * detected environment + target. When targeting a Windows host from
48
+ * within WSL, wrap the call in `wsl.exe -d <distro> -- aiden mcp serve`
49
+ * so the host-side Claude Desktop launches `aiden` via the WSL
50
+ * interop layer.
51
+ */
52
+ function buildAidenEntry(opts = {}) {
53
+ const wsl = opts.wsl ?? detectWsl();
54
+ if (wsl.inWsl && opts.target === 'host') {
55
+ const distro = wsl.distro ?? 'default';
56
+ return {
57
+ command: 'wsl.exe',
58
+ args: ['-d', distro, '--', 'aiden', 'mcp', 'serve'],
59
+ };
60
+ }
61
+ return { command: 'aiden', args: ['mcp', 'serve'] };
62
+ }