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,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
|
+
}
|