aiden-runtime 4.1.1 → 4.1.3
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 +78 -26
- package/dist/cli/v4/aidenCLI.js +169 -9
- package/dist/cli/v4/callbacks.js +20 -2
- package/dist/cli/v4/chatSession.js +644 -16
- package/dist/cli/v4/commands/auth.js +6 -3
- package/dist/cli/v4/commands/doctor.js +23 -27
- package/dist/cli/v4/commands/help.js +4 -0
- package/dist/cli/v4/commands/index.js +10 -1
- package/dist/cli/v4/commands/model.js +30 -1
- package/dist/cli/v4/commands/reloadSoul.js +37 -0
- package/dist/cli/v4/commands/update.js +102 -0
- package/dist/cli/v4/defaultSoul.js +68 -2
- package/dist/cli/v4/display/capabilityCard.js +135 -0
- package/dist/cli/v4/display/sessionEndCard.js +127 -0
- package/dist/cli/v4/display/toolTrail.js +172 -0
- package/dist/cli/v4/display.js +492 -142
- package/dist/cli/v4/doctor.js +472 -58
- package/dist/cli/v4/doctorLiveness.js +65 -10
- package/dist/cli/v4/promotionPrompt.js +332 -0
- package/dist/cli/v4/providerBootSelector.js +144 -0
- package/dist/cli/v4/replyRenderer.js +311 -20
- package/dist/cli/v4/sessionSummaryGate.js +66 -0
- package/dist/cli/v4/skinEngine.js +14 -3
- package/dist/cli/v4/toolPreview.js +153 -0
- package/dist/core/tools/nowPlaying.js +7 -15
- package/dist/core/v4/aidenAgent.js +91 -29
- package/dist/core/v4/capabilities.js +89 -0
- package/dist/core/v4/contextCompressor.js +25 -8
- package/dist/core/v4/distillationIndex.js +167 -0
- package/dist/core/v4/distillationStore.js +98 -0
- package/dist/core/v4/logger/logger.js +40 -9
- package/dist/core/v4/promotionCandidates.js +234 -0
- package/dist/core/v4/promptBuilder.js +145 -1
- package/dist/core/v4/sessionDistiller.js +452 -0
- package/dist/core/v4/skillMining/skillMiner.js +43 -6
- package/dist/core/v4/skillOutcomeTracker.js +323 -0
- package/dist/core/v4/subsystemHealth.js +143 -0
- package/dist/core/v4/toolRegistry.js +16 -1
- package/dist/core/v4/update/executeInstall.js +233 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/memoryGuard.js +111 -0
- package/dist/moat/plannerGuard.js +19 -0
- package/dist/moat/skillTeacher.js +14 -5
- package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
- package/dist/providers/v4/errors.js +112 -4
- package/dist/providers/v4/modelDefaults.js +65 -0
- package/dist/providers/v4/registry.js +9 -2
- package/dist/providers/v4/runtimeResolver.js +6 -0
- package/dist/tools/v4/index.js +80 -1
- package/dist/tools/v4/memory/memoryRemove.js +57 -2
- package/dist/tools/v4/memory/sessionSummary.js +151 -0
- package/dist/tools/v4/sessions/recallSession.js +177 -0
- package/dist/tools/v4/sessions/sessionSearch.js +5 -1
- package/dist/tools/v4/system/_psHelpers.js +123 -0
- package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
- package/dist/tools/v4/system/appClose.js +79 -0
- package/dist/tools/v4/system/appInput.js +154 -0
- package/dist/tools/v4/system/appLaunch.js +218 -0
- package/dist/tools/v4/system/clipboardRead.js +54 -0
- package/dist/tools/v4/system/clipboardWrite.js +84 -0
- package/dist/tools/v4/system/mediaKey.js +109 -0
- package/dist/tools/v4/system/mediaSessions.js +163 -0
- package/dist/tools/v4/system/mediaTransport.js +211 -0
- package/dist/tools/v4/system/osProcessList.js +99 -0
- package/dist/tools/v4/system/screenshot.js +106 -0
- package/dist/tools/v4/system/volumeSet.js +157 -0
- package/package.json +4 -1
- package/skills/system_control.md +185 -69
|
@@ -0,0 +1,233 @@
|
|
|
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/update/executeInstall.ts — Phase v4.1.2-update.
|
|
10
|
+
*
|
|
11
|
+
* Shared in-process installer for `npm install -g aiden-runtime@latest`.
|
|
12
|
+
* Used by two surfaces:
|
|
13
|
+
* - `/update install` slash command (cli/v4/commands/update.ts)
|
|
14
|
+
* - `aiden_self_update` tool (tools/v4/system/aidenSelfUpdate.ts)
|
|
15
|
+
*
|
|
16
|
+
* Both call this single executor so install behavior — timeout,
|
|
17
|
+
* permission-denied fallback, version detection — has ONE source of
|
|
18
|
+
* truth. Future v4.1.3+ rollback / package-manager-swap work edits
|
|
19
|
+
* one file.
|
|
20
|
+
*
|
|
21
|
+
* Behavior:
|
|
22
|
+
* - Spawns `npm install -g aiden-runtime@latest` with INSTALL_TIMEOUT_MS
|
|
23
|
+
* wall-clock cap.
|
|
24
|
+
* - Captures stdout/stderr; returns both for diagnostics regardless
|
|
25
|
+
* of outcome.
|
|
26
|
+
* - Detects the installed version from npm's `+ aiden-runtime@x.y.z`
|
|
27
|
+
* output line; null when not parseable.
|
|
28
|
+
* - On permission-denied (EACCES / "EACCES" / Windows ENOPRIV /
|
|
29
|
+
* "operation not permitted"): returns structured failure with
|
|
30
|
+
* platform-specific copy-paste commands so the user can run the
|
|
31
|
+
* install manually with proper privileges.
|
|
32
|
+
*
|
|
33
|
+
* Honest about what it doesn't do:
|
|
34
|
+
* - No auto-restart of the running REPL. The currently-running
|
|
35
|
+
* process keeps running the OLD version regardless of what npm
|
|
36
|
+
* just installed globally — claiming otherwise would lie to the
|
|
37
|
+
* user. Caller prints the "type /quit and rerun aiden" hint
|
|
38
|
+
* instead so the user knows exactly when the new version takes
|
|
39
|
+
* effect.
|
|
40
|
+
* - No self-escalation to UAC/sudo. We try once; on permission
|
|
41
|
+
* failure we surface the right copy-paste, not silent escalation.
|
|
42
|
+
* - No registry probe — call `checkForUpdate` first if you need to
|
|
43
|
+
* know whether an install is warranted.
|
|
44
|
+
*/
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.INSTALL_TIMEOUT_MS = void 0;
|
|
47
|
+
exports.executeInstall = executeInstall;
|
|
48
|
+
exports.parseInstalledVersion = parseInstalledVersion;
|
|
49
|
+
const node_child_process_1 = require("node:child_process");
|
|
50
|
+
/** 90 s wall-clock cap. Generous on cold caches / slow networks. */
|
|
51
|
+
exports.INSTALL_TIMEOUT_MS = 90000;
|
|
52
|
+
const DEFAULT_PACKAGE_SPEC = 'aiden-runtime@latest';
|
|
53
|
+
/**
|
|
54
|
+
* Run the install. Returns a structured result; NEVER throws — the
|
|
55
|
+
* outer surface (slash command / tool) renders the result to the user.
|
|
56
|
+
*
|
|
57
|
+
* Error path is intentionally string-typed (single user-visible
|
|
58
|
+
* paragraph). The structured fields (stdout/stderr/exitCode) are for
|
|
59
|
+
* diagnostics; callers that want to surface them to the user can
|
|
60
|
+
* compose their own message from those.
|
|
61
|
+
*/
|
|
62
|
+
async function executeInstall(opts = {}) {
|
|
63
|
+
const spawn = opts.spawnImpl ?? node_child_process_1.spawn;
|
|
64
|
+
const timeoutMs = opts.timeoutMs ?? exports.INSTALL_TIMEOUT_MS;
|
|
65
|
+
const packageSpec = opts.packageSpec ?? DEFAULT_PACKAGE_SPEC;
|
|
66
|
+
const platform = opts.platform ?? process.platform;
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
const args = ['install', '-g', packageSpec];
|
|
69
|
+
// shell: true on Windows so npm.cmd is found via PATHEXT; on
|
|
70
|
+
// POSIX we spawn npm directly. Either way the args are validated
|
|
71
|
+
// (only npm + install + a hardcoded spec by default) — no user
|
|
72
|
+
// input flows into argv.
|
|
73
|
+
const spawnOpts = {
|
|
74
|
+
shell: platform === 'win32',
|
|
75
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
76
|
+
};
|
|
77
|
+
let child;
|
|
78
|
+
try {
|
|
79
|
+
child = spawn('npm', args, spawnOpts);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
resolve({
|
|
83
|
+
success: false,
|
|
84
|
+
error: `Could not launch npm: ${err.message}. ` +
|
|
85
|
+
`Run \`npm install -g aiden-runtime@latest\` manually.`,
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let stdoutBuf = '';
|
|
90
|
+
let stderrBuf = '';
|
|
91
|
+
child.stdout?.on('data', (chunk) => {
|
|
92
|
+
stdoutBuf += chunk.toString();
|
|
93
|
+
});
|
|
94
|
+
child.stderr?.on('data', (chunk) => {
|
|
95
|
+
stderrBuf += chunk.toString();
|
|
96
|
+
});
|
|
97
|
+
// Timeout — kill the child + resolve as a failure with the captured
|
|
98
|
+
// output so the user sees what npm was doing.
|
|
99
|
+
let timedOut = false;
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
timedOut = true;
|
|
102
|
+
try {
|
|
103
|
+
child.kill('SIGTERM');
|
|
104
|
+
}
|
|
105
|
+
catch { /* ignore */ }
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
child.on('error', (err) => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
// Spawn-level error (ENOENT — npm not on PATH).
|
|
110
|
+
resolve({
|
|
111
|
+
success: false,
|
|
112
|
+
error: `npm spawn failed: ${err.message}. Is npm installed and on PATH?`,
|
|
113
|
+
stderr: stderrBuf,
|
|
114
|
+
stdout: stdoutBuf,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
child.on('close', (code) => {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
const stdout = stdoutBuf;
|
|
120
|
+
const stderr = stderrBuf;
|
|
121
|
+
const exitCode = code ?? -1;
|
|
122
|
+
if (timedOut) {
|
|
123
|
+
resolve({
|
|
124
|
+
success: false,
|
|
125
|
+
error: `Install timed out after ${timeoutMs}ms. ` +
|
|
126
|
+
`Try \`npm install -g aiden-runtime@latest\` manually.`,
|
|
127
|
+
stdout, stderr, exitCode: -1,
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Permission-denied: surface platform-specific remediations.
|
|
132
|
+
if (isPermissionDenied(stdout, stderr, exitCode)) {
|
|
133
|
+
resolve({
|
|
134
|
+
success: false,
|
|
135
|
+
error: permissionDeniedMessage(platform),
|
|
136
|
+
stdout, stderr, exitCode,
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (exitCode !== 0) {
|
|
141
|
+
resolve({
|
|
142
|
+
success: false,
|
|
143
|
+
error: `Install failed (npm exit ${exitCode}). ` +
|
|
144
|
+
(stderr.trim().slice(0, 200) ||
|
|
145
|
+
'See stderr/stdout for details. Try `npm install -g aiden-runtime@latest` manually.'),
|
|
146
|
+
stdout, stderr, exitCode,
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Success — parse installed version from npm output. Pattern:
|
|
151
|
+
// "+ aiden-runtime@4.1.3" or "added 1 package ... aiden-runtime@4.1.3"
|
|
152
|
+
const installedVersion = parseInstalledVersion(stdout) ?? parseInstalledVersion(stderr) ?? undefined;
|
|
153
|
+
resolve({
|
|
154
|
+
success: true,
|
|
155
|
+
installedVersion,
|
|
156
|
+
stdout, stderr, exitCode,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
162
|
+
/**
|
|
163
|
+
* Did npm fail because of a permission error? Heuristics across the
|
|
164
|
+
* three platforms — npm doesn't return a single canonical exit code
|
|
165
|
+
* for this, so we sniff the captured streams.
|
|
166
|
+
*/
|
|
167
|
+
function isPermissionDenied(stdout, stderr, exitCode) {
|
|
168
|
+
const haystack = `${stderr}\n${stdout}`.toLowerCase();
|
|
169
|
+
// POSIX: "EACCES", "permission denied", "operation not permitted"
|
|
170
|
+
if (haystack.includes('eacces'))
|
|
171
|
+
return true;
|
|
172
|
+
if (haystack.includes('permission denied'))
|
|
173
|
+
return true;
|
|
174
|
+
if (haystack.includes('operation not permitted'))
|
|
175
|
+
return true;
|
|
176
|
+
// Windows: usually exit 1 with stderr containing "EPERM" or
|
|
177
|
+
// "operation not permitted" or "access is denied"
|
|
178
|
+
if (haystack.includes('eperm'))
|
|
179
|
+
return true;
|
|
180
|
+
if (haystack.includes('access is denied'))
|
|
181
|
+
return true;
|
|
182
|
+
// exit 243 is the npm conventional "permission" code on some setups;
|
|
183
|
+
// we don't gate on exit code alone (too noisy) but combined with
|
|
184
|
+
// any of the above strings it's a clear signal.
|
|
185
|
+
void exitCode;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Build the platform-specific copy-paste remediation. Provides three
|
|
190
|
+
* distinct paths — system-wide-with-elevation (Windows admin),
|
|
191
|
+
* sudo (macOS/Linux), or user-local-prefix (cross-platform) — so the
|
|
192
|
+
* user has options without us trying to self-escalate to UAC/sudo
|
|
193
|
+
* from inside the running REPL.
|
|
194
|
+
*/
|
|
195
|
+
function permissionDeniedMessage(platform) {
|
|
196
|
+
const userLocal = 'Or use a user-local npm prefix to avoid privileges entirely:\n' +
|
|
197
|
+
' npm config set prefix ~/.npm-global\n' +
|
|
198
|
+
' export PATH=~/.npm-global/bin:$PATH # add to your shell profile\n' +
|
|
199
|
+
' npm install -g aiden-runtime@latest';
|
|
200
|
+
if (platform === 'win32') {
|
|
201
|
+
return [
|
|
202
|
+
'Install failed: permission denied (npm needs Administrator for global install).',
|
|
203
|
+
'',
|
|
204
|
+
'To update manually:',
|
|
205
|
+
' Windows: Open PowerShell as Administrator, then:',
|
|
206
|
+
' npm install -g aiden-runtime@latest',
|
|
207
|
+
'',
|
|
208
|
+
userLocal,
|
|
209
|
+
].join('\n');
|
|
210
|
+
}
|
|
211
|
+
// darwin / linux / others — sudo path.
|
|
212
|
+
return [
|
|
213
|
+
'Install failed: permission denied (npm needs sudo for global install).',
|
|
214
|
+
'',
|
|
215
|
+
'To update manually:',
|
|
216
|
+
' macOS / Linux:',
|
|
217
|
+
' sudo npm install -g aiden-runtime@latest',
|
|
218
|
+
'',
|
|
219
|
+
userLocal,
|
|
220
|
+
].join('\n');
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Find the installed version in npm output. Two common patterns:
|
|
224
|
+
* "+ aiden-runtime@4.1.3"
|
|
225
|
+
* "added 1 package in 12s ... aiden-runtime@4.1.3"
|
|
226
|
+
* Returns the bare version string (no `v` prefix) or null.
|
|
227
|
+
*/
|
|
228
|
+
function parseInstalledVersion(out) {
|
|
229
|
+
if (!out)
|
|
230
|
+
return null;
|
|
231
|
+
const m = out.match(/aiden-runtime@(\d+\.\d+\.\d+(?:-[a-z0-9.]+)?)/i);
|
|
232
|
+
return m ? m[1] : null;
|
|
233
|
+
}
|
package/dist/core/version.js
CHANGED
package/dist/moat/memoryGuard.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
33
|
exports.MemoryGuard = void 0;
|
|
34
|
+
exports.containsInSection = containsInSection;
|
|
34
35
|
class MemoryGuard {
|
|
35
36
|
constructor(memory) {
|
|
36
37
|
this.memory = memory;
|
|
@@ -113,6 +114,75 @@ class MemoryGuard {
|
|
|
113
114
|
}
|
|
114
115
|
return { ok: true, verified: true, fileLength: text.length };
|
|
115
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Phase v4.1.2 alive-core: section-aware write. Replaces the body of
|
|
119
|
+
* a markdown `## <header>` section, creating the section at file end
|
|
120
|
+
* if it doesn't yet exist. Body lines below the header up to the
|
|
121
|
+
* next `## ` (or EOF) are replaced wholesale.
|
|
122
|
+
*
|
|
123
|
+
* Preserves the standard verify-on-disk contract: the post-write read
|
|
124
|
+
* confirms `newBody` is present and (when applicable) the previous
|
|
125
|
+
* section body is gone before returning `verified: true`.
|
|
126
|
+
*
|
|
127
|
+
* Additive — does not change `guardedAdd` / `guardedReplace` /
|
|
128
|
+
* `guardedRemove` semantics.
|
|
129
|
+
*/
|
|
130
|
+
async replaceSection(file, header, newBody) {
|
|
131
|
+
const headerTrim = header.trim();
|
|
132
|
+
if (!headerTrim.startsWith('## ')) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
verified: false,
|
|
136
|
+
reason: 'header must start with "## " (markdown h2)',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const bodyTrim = newBody.trim();
|
|
140
|
+
if (!bodyTrim) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
verified: false,
|
|
144
|
+
reason: 'newBody cannot be empty. Use guardedRemove() to drop a section.',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Read current file state so we can compute the precise old block.
|
|
148
|
+
const snapBefore = await this.memory.loadSnapshot();
|
|
149
|
+
const before = pickFile(snapBefore, file);
|
|
150
|
+
// Match `<header>` and everything below it up to the next `## `
|
|
151
|
+
// line or EOF. No `m` flag — we want `$` to mean end-of-string;
|
|
152
|
+
// with `m`, the trailing-`$` lookahead would match at every line
|
|
153
|
+
// ending and chop off all but the first body line.
|
|
154
|
+
const escapedHeader = headerTrim.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
155
|
+
const sectionRe = new RegExp(`${escapedHeader}[^\\n]*(?:\\r?\\n[\\s\\S]*?)?(?=\\n## |$)`);
|
|
156
|
+
const match = before.match(sectionRe);
|
|
157
|
+
const newSection = `${headerTrim}\n${bodyTrim}`;
|
|
158
|
+
let mutation;
|
|
159
|
+
if (match && match[0]) {
|
|
160
|
+
mutation = await this.memory.replace(file, match[0], newSection);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Section doesn't exist — append at end with a blank-line gap.
|
|
164
|
+
const sep = before.length > 0 && !before.endsWith('\n') ? '\n\n' : '\n';
|
|
165
|
+
mutation = await this.memory.add(file, `${sep}${newSection}`);
|
|
166
|
+
}
|
|
167
|
+
if (!mutation.ok) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
verified: false,
|
|
171
|
+
reason: mutation.reason ?? 'section write failed',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const snapAfter = await this.memory.loadSnapshot();
|
|
175
|
+
const after = pickFile(snapAfter, file);
|
|
176
|
+
if (!after.includes(headerTrim) || !after.includes(bodyTrim)) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
verified: false,
|
|
180
|
+
reason: 'Section write claimed but header/body not found post-write',
|
|
181
|
+
fileLength: after.length,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, verified: true, fileLength: after.length };
|
|
185
|
+
}
|
|
116
186
|
async guardedRemove(file, text) {
|
|
117
187
|
const trimmed = text.trim();
|
|
118
188
|
if (!trimmed) {
|
|
@@ -144,3 +214,44 @@ exports.MemoryGuard = MemoryGuard;
|
|
|
144
214
|
function pickFile(snap, file) {
|
|
145
215
|
return file === 'user' ? snap.userMd : snap.memoryMd;
|
|
146
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Phase v4.1.2-bug-X: section-aware containment check.
|
|
219
|
+
*
|
|
220
|
+
* Returns `true` if `target` appears anywhere within the body of the
|
|
221
|
+
* section identified by `sectionHeader` (e.g. `"## Durable facts"`).
|
|
222
|
+
* The section body runs from the line after the header to the line
|
|
223
|
+
* before the next `## ` header — or end-of-file, whichever comes
|
|
224
|
+
* first. Returns `false` when the section doesn't exist OR when the
|
|
225
|
+
* target sits outside it.
|
|
226
|
+
*
|
|
227
|
+
* Pure: no I/O, deterministic from inputs. Used by `memory_remove`
|
|
228
|
+
* to protect user-approved durable facts from autonomous deletion:
|
|
229
|
+
* the model proposed substring-match against MEMORY.md, but
|
|
230
|
+
* substring removal operates whole-file — partial protection would
|
|
231
|
+
* still nuke the durable copy as side-effect. STRICT containment
|
|
232
|
+
* (rejects if the substring appears ANYWHERE in the section body)
|
|
233
|
+
* is the honest guard.
|
|
234
|
+
*
|
|
235
|
+
* Case-sensitive: matches the existing `guardedRemove` semantics
|
|
236
|
+
* which use `String.prototype.includes` directly on the raw content.
|
|
237
|
+
*
|
|
238
|
+
* @param fileContent Full file content (e.g. MEMORY.md as one string).
|
|
239
|
+
* @param target Substring the caller intends to remove.
|
|
240
|
+
* @param sectionHeader Header line including the `## ` prefix.
|
|
241
|
+
*/
|
|
242
|
+
function containsInSection(fileContent, target, sectionHeader) {
|
|
243
|
+
if (!fileContent || !target || !sectionHeader)
|
|
244
|
+
return false;
|
|
245
|
+
const headerEscaped = sectionHeader.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
246
|
+
// Match the header line, then capture the body until the next `## `
|
|
247
|
+
// (any h2) or end-of-string. No `m` flag — the trailing-`$` would
|
|
248
|
+
// otherwise match every line ending and chop the body at the first
|
|
249
|
+
// newline (the same trap the slice2 sessionSummary regex already
|
|
250
|
+
// documents).
|
|
251
|
+
const sectionRe = new RegExp(`${headerEscaped}[^\\n]*\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
252
|
+
const m = fileContent.match(sectionRe);
|
|
253
|
+
if (!m)
|
|
254
|
+
return false;
|
|
255
|
+
const body = m[1] ?? '';
|
|
256
|
+
return body.includes(target);
|
|
257
|
+
}
|
|
@@ -91,6 +91,25 @@ const RULES = [
|
|
|
91
91
|
keywords: /\b(process|background|long.?running|server|spawn|kill|daemon)\b/i,
|
|
92
92
|
toolsets: ['process'],
|
|
93
93
|
},
|
|
94
|
+
// Media playback control (v4.1.4-media)
|
|
95
|
+
//
|
|
96
|
+
// Without this, intents like "list media sessions" matched the
|
|
97
|
+
// `sessions` rule via the bare word "session" → narrowed surface to
|
|
98
|
+
// toolset `sessions` only → `media_sessions` (toolset `system`) was
|
|
99
|
+
// filtered out and the model honestly reported it as unavailable.
|
|
100
|
+
// UNION semantics mean both rules contribute on phrases that hit
|
|
101
|
+
// both ("media sessions" → sessions + system), giving the model the
|
|
102
|
+
// full picture without needing a dedicated `media` toolset.
|
|
103
|
+
//
|
|
104
|
+
// Toolset is `system` (broad but minimal blast radius): the bundle
|
|
105
|
+
// covers media_sessions, media_transport, media_key, app_input,
|
|
106
|
+
// now_playing, plus the plausibly-relevant app_launch / volume_set /
|
|
107
|
+
// os_process_list. Carving out a dedicated `media` toolset is a
|
|
108
|
+
// separate slice if the surface noise becomes a real problem.
|
|
109
|
+
{
|
|
110
|
+
keywords: /\b(play|pause|skip|spotify|music|song|video|youtube|track|playback|media)\b/i,
|
|
111
|
+
toolsets: ['system'],
|
|
112
|
+
},
|
|
94
113
|
];
|
|
95
114
|
/** Always-on tools regardless of mode. The agent needs schema lookup
|
|
96
115
|
* + skill discovery + session search to be useful even on cold turns. */
|
|
@@ -97,7 +97,7 @@ class SkillTeacher {
|
|
|
97
97
|
/** Optional handler-resolver to look up toolset metadata for trace
|
|
98
98
|
* entries that don't carry their own toolset. Used for the 2-toolset
|
|
99
99
|
* diversity check. */
|
|
100
|
-
resolveHandler) {
|
|
100
|
+
resolveHandler, healthTracker) {
|
|
101
101
|
this.skillLoader = skillLoader;
|
|
102
102
|
this.skillManager = skillManager;
|
|
103
103
|
this.resolveHandler = resolveHandler;
|
|
@@ -107,6 +107,7 @@ class SkillTeacher {
|
|
|
107
107
|
this.qualityPath =
|
|
108
108
|
qualityFilePath ??
|
|
109
109
|
node_path_1.default.join(process.cwd(), '.aiden-skill-quality.json');
|
|
110
|
+
this.healthTracker = healthTracker;
|
|
110
111
|
}
|
|
111
112
|
setTier(tier) {
|
|
112
113
|
this.tier = tier;
|
|
@@ -203,9 +204,11 @@ class SkillTeacher {
|
|
|
203
204
|
name: proposal.proposedName,
|
|
204
205
|
content,
|
|
205
206
|
}, {});
|
|
207
|
+
this.healthTracker?.recordSuccess();
|
|
206
208
|
return { created: true, skillName: proposal.proposedName };
|
|
207
209
|
}
|
|
208
210
|
catch (e) {
|
|
211
|
+
this.healthTracker?.recordFailure(e);
|
|
209
212
|
const msg = e instanceof Error ? e.message : String(e);
|
|
210
213
|
return { created: false, reason: `create_failed: ${msg}` };
|
|
211
214
|
}
|
|
@@ -336,8 +339,11 @@ ${[...new Set(proposal.toolsUsed)].map((t) => `- ${t}`).join('\n')}
|
|
|
336
339
|
return this.qualityCache;
|
|
337
340
|
}
|
|
338
341
|
}
|
|
339
|
-
catch {
|
|
340
|
-
//
|
|
342
|
+
catch (e) {
|
|
343
|
+
// Phase v4.1.2-slice3: record corrupt-file / JSON parse / read
|
|
344
|
+
// failures into the health tracker. We still fall through to an
|
|
345
|
+
// empty cache so behavior is unchanged — telemetry is additive.
|
|
346
|
+
this.healthTracker?.recordFailure(e);
|
|
341
347
|
}
|
|
342
348
|
this.qualityCache = {};
|
|
343
349
|
return this.qualityCache;
|
|
@@ -346,8 +352,11 @@ ${[...new Set(proposal.toolsUsed)].map((t) => `- ${t}`).join('\n')}
|
|
|
346
352
|
try {
|
|
347
353
|
await node_fs_1.promises.writeFile(this.qualityPath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
348
354
|
}
|
|
349
|
-
catch {
|
|
350
|
-
//
|
|
355
|
+
catch (e) {
|
|
356
|
+
// Phase v4.1.2-slice3: surface disk-write failures (EACCES,
|
|
357
|
+
// ENOSPC, EROFS) instead of silently dropping. Best-effort
|
|
358
|
+
// semantic preserved — we still return without throwing.
|
|
359
|
+
this.healthTracker?.recordFailure(e);
|
|
351
360
|
}
|
|
352
361
|
}
|
|
353
362
|
}
|
|
@@ -68,6 +68,7 @@ class ChatCompletionsAdapter {
|
|
|
68
68
|
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
69
69
|
this.maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
70
70
|
this.extraHeaders = opts.extraHeaders ?? {};
|
|
71
|
+
this.defaultExtraBody = opts.defaultExtraBody;
|
|
71
72
|
}
|
|
72
73
|
// ── Non-streaming ────────────────────────────────────────────────────
|
|
73
74
|
async call(input) {
|
|
@@ -127,6 +128,14 @@ class ChatCompletionsAdapter {
|
|
|
127
128
|
// the stream closes promptly and we get accurate token accounting.
|
|
128
129
|
body.stream_options = { include_usage: true };
|
|
129
130
|
}
|
|
131
|
+
// Phase v4.1.2-deepseek: merge order is base body → defaultExtraBody
|
|
132
|
+
// (model-mandated, from resolver lookup in MODEL_DEFAULTS) → per-call
|
|
133
|
+
// input.extraBody (caller). Per-call wins so a single request can
|
|
134
|
+
// override a default (e.g. disabling thinking on a probe). This
|
|
135
|
+
// matters because providers/v4/modelDefaults.ts sets thinking +
|
|
136
|
+
// reasoning_effort for deepseek-v4-pro on EVERY call.
|
|
137
|
+
if (this.defaultExtraBody)
|
|
138
|
+
Object.assign(body, this.defaultExtraBody);
|
|
130
139
|
if (input.extraBody)
|
|
131
140
|
Object.assign(body, input.extraBody);
|
|
132
141
|
return body;
|
|
@@ -17,12 +17,21 @@
|
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.ProviderRateLimitError = exports.ProviderTimeoutError = exports.ProviderError = void 0;
|
|
19
19
|
exports.formatRawForMessage = formatRawForMessage;
|
|
20
|
+
exports.classifyProviderError = classifyProviderError;
|
|
21
|
+
exports.suggestForErrorClass = suggestForErrorClass;
|
|
20
22
|
/**
|
|
21
23
|
* Format a raw response body for inclusion in the user-facing error
|
|
22
|
-
* message. Recognises
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* message. Recognises three JSON envelope shapes and falls back to the
|
|
25
|
+
* raw string for plain-text bodies. Returns null when nothing useful is
|
|
26
|
+
* available so callers can omit the ": <detail>" tail entirely.
|
|
27
|
+
*
|
|
28
|
+
* Recognised envelopes (most-specific first):
|
|
29
|
+
* 1. OpenAI / Anthropic: `{ error: { message: "..." } }`
|
|
30
|
+
* 2. Top-level message: `{ message: "..." }`
|
|
31
|
+
* 3. Codex Responses: `{ detail: "..." }` (Phase v4.1.2-bug3 —
|
|
32
|
+
* surfaced by slice5: the Codex backend at chatgpt.com/backend-api/
|
|
33
|
+
* codex/responses returns 4xx bodies in this shape, e.g.
|
|
34
|
+
* `{"detail": "The 'gpt-5.1-codex-max' model is not supported..."}`)
|
|
26
35
|
*
|
|
27
36
|
* Truncates to 300 chars to keep multi-line responses from blowing
|
|
28
37
|
* up the user's terminal — full body remains on `error.raw` for
|
|
@@ -45,6 +54,15 @@ function formatRawForMessage(raw) {
|
|
|
45
54
|
if (typeof topMsg === 'string' && topMsg.length > 0) {
|
|
46
55
|
return topMsg.length > 300 ? `${topMsg.slice(0, 300)}…` : topMsg;
|
|
47
56
|
}
|
|
57
|
+
// Codex Responses envelope: { detail: "..." }. Distinct from the
|
|
58
|
+
// OpenAI shape — the Codex backend uses FastAPI-style validation
|
|
59
|
+
// errors that surface as `detail` (str) for tier/auth rejections
|
|
60
|
+
// and `detail: [{...}]` for schema errors. Only the string form is
|
|
61
|
+
// useful in the message tail; the array form is left to .raw.
|
|
62
|
+
const detail = raw.detail;
|
|
63
|
+
if (typeof detail === 'string' && detail.length > 0) {
|
|
64
|
+
return detail.length > 300 ? `${detail.slice(0, 300)}…` : detail;
|
|
65
|
+
}
|
|
48
66
|
return null;
|
|
49
67
|
}
|
|
50
68
|
// Plain string body.
|
|
@@ -93,3 +111,93 @@ class ProviderRateLimitError extends ProviderError {
|
|
|
93
111
|
}
|
|
94
112
|
}
|
|
95
113
|
exports.ProviderRateLimitError = ProviderRateLimitError;
|
|
114
|
+
function classifyProviderError(err) {
|
|
115
|
+
if (err == null)
|
|
116
|
+
return 'other';
|
|
117
|
+
// 1. Type-based class detection (fastest, most structured).
|
|
118
|
+
if (err instanceof ProviderRateLimitError)
|
|
119
|
+
return 'rate_limit';
|
|
120
|
+
if (err instanceof ProviderTimeoutError)
|
|
121
|
+
return 'transport';
|
|
122
|
+
if (err instanceof ProviderError) {
|
|
123
|
+
if (err.statusCode === 413)
|
|
124
|
+
return 'context_overflow';
|
|
125
|
+
if (err.statusCode === 429)
|
|
126
|
+
return 'rate_limit';
|
|
127
|
+
if (err.statusCode === 401 || err.statusCode === 403)
|
|
128
|
+
return 'auth';
|
|
129
|
+
}
|
|
130
|
+
// 2. Fall back to message scanning. Adapters that pass through the
|
|
131
|
+
// upstream JSON `error.message` verbatim land here.
|
|
132
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
133
|
+
const lc = msg.toLowerCase();
|
|
134
|
+
// Context overflow / 413 family. Groq's free-tier TPM cap triggers
|
|
135
|
+
// these on the first turn once the prompt + tool schemas inflate.
|
|
136
|
+
if (lc.includes('413') ||
|
|
137
|
+
lc.includes('context_length_exceeded') ||
|
|
138
|
+
lc.includes('context length') ||
|
|
139
|
+
lc.includes('too large') ||
|
|
140
|
+
lc.includes('maximum context length') ||
|
|
141
|
+
lc.includes('payload too large')) {
|
|
142
|
+
return 'context_overflow';
|
|
143
|
+
}
|
|
144
|
+
// Rate-limit family — 429 / TPM / quota / "too many requests".
|
|
145
|
+
if (lc.includes('429') ||
|
|
146
|
+
lc.includes('rate_limit') ||
|
|
147
|
+
lc.includes('rate limit') ||
|
|
148
|
+
lc.includes('too many requests') ||
|
|
149
|
+
lc.includes('quota') ||
|
|
150
|
+
lc.includes('tpm')) {
|
|
151
|
+
return 'rate_limit';
|
|
152
|
+
}
|
|
153
|
+
// Auth family — 401 / 403 / invalid keys / unauthenticated.
|
|
154
|
+
if (lc.includes('401') ||
|
|
155
|
+
lc.includes('403') ||
|
|
156
|
+
lc.includes('invalid_api_key') ||
|
|
157
|
+
lc.includes('invalid api key') ||
|
|
158
|
+
lc.includes('unauthenticated') ||
|
|
159
|
+
lc.includes('unauthorized') ||
|
|
160
|
+
lc.includes('forbidden')) {
|
|
161
|
+
return 'auth';
|
|
162
|
+
}
|
|
163
|
+
// Transport — network, DNS, timeouts that escaped the typed path.
|
|
164
|
+
if (lc.includes('econnrefused') ||
|
|
165
|
+
lc.includes('enotfound') ||
|
|
166
|
+
lc.includes('etimedout') ||
|
|
167
|
+
lc.includes('socket hang up') ||
|
|
168
|
+
lc.includes('network')) {
|
|
169
|
+
return 'transport';
|
|
170
|
+
}
|
|
171
|
+
return 'other';
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* v4.1.3-prebump: produce a single-sentence actionable hint for the
|
|
175
|
+
* given error class. Returns null for `'other'` so the caller can keep
|
|
176
|
+
* its existing default suggestion. Provider name is surfaced where it
|
|
177
|
+
* sharpens the advice ("groq rate-limited" reads clearer than
|
|
178
|
+
* "rate limit").
|
|
179
|
+
*
|
|
180
|
+
* Pure helper. The REPL displays the result; tests assert it. No
|
|
181
|
+
* registry / state access — feed it the class + provider name.
|
|
182
|
+
*/
|
|
183
|
+
function suggestForErrorClass(cls, providerName) {
|
|
184
|
+
const p = providerName ?? 'this provider';
|
|
185
|
+
switch (cls) {
|
|
186
|
+
case 'context_overflow':
|
|
187
|
+
return (`${p} returned 413 (context too large). The combined system prompt ` +
|
|
188
|
+
`+ tool schemas exceed ${p}'s context window. Try \`/model\` to ` +
|
|
189
|
+
`switch to a provider with more headroom (chatgpt-plus, anthropic, ` +
|
|
190
|
+
`deepseek).`);
|
|
191
|
+
case 'rate_limit':
|
|
192
|
+
return (`${p} is rate-limited. Wait a minute, or run \`/model\` to switch ` +
|
|
193
|
+
`to another authed provider while ${p} cools off.`);
|
|
194
|
+
case 'auth':
|
|
195
|
+
return (`${p} rejected the credentials. Run \`/auth status\` (or check the ` +
|
|
196
|
+
`relevant API key env var) and \`/auth login\` if needed.`);
|
|
197
|
+
case 'transport':
|
|
198
|
+
return (`Network or transport error reaching ${p}. Check connectivity, then ` +
|
|
199
|
+
`retry — or \`/model\` to a local provider (ollama) for offline work.`);
|
|
200
|
+
case 'other':
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
* providers/v4/modelDefaults.ts — Phase v4.1.2-deepseek.
|
|
10
|
+
*
|
|
11
|
+
* Per-model default request parameters. Keyed by `${providerId}:${modelId}`
|
|
12
|
+
* so the same model slug appearing under multiple providers doesn't
|
|
13
|
+
* collide (e.g. a future shared-slug case across compat endpoints).
|
|
14
|
+
*
|
|
15
|
+
* Today's only consumer:
|
|
16
|
+
* deepseek:deepseek-v4-pro → always send extra_body.thinking +
|
|
17
|
+
* reasoning_effort, per DeepSeek's V4-Pro API guidance:
|
|
18
|
+
*
|
|
19
|
+
* client.chat.completions.create(
|
|
20
|
+
* model="deepseek-v4-pro",
|
|
21
|
+
* messages=...,
|
|
22
|
+
* reasoning_effort="high",
|
|
23
|
+
* extra_body={"thinking": {"type": "enabled"}},
|
|
24
|
+
* )
|
|
25
|
+
*
|
|
26
|
+
* The defaults are merged into the wire body by ChatCompletionsAdapter:
|
|
27
|
+
* base body → defaultExtraBody (from this map) → per-call extraBody
|
|
28
|
+
* (from the caller's ProviderCallInput)
|
|
29
|
+
*
|
|
30
|
+
* Per-call extraBody wins so a caller can disable thinking on a single
|
|
31
|
+
* request without un-registering the model default.
|
|
32
|
+
*
|
|
33
|
+
* Adding a new entry: keep this file small. Per-model knowledge lives
|
|
34
|
+
* here so resolver / registry / adapter stay pure (credential
|
|
35
|
+
* resolution / provider facts / wire-format mechanics respectively).
|
|
36
|
+
*/
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.MODEL_DEFAULTS = void 0;
|
|
39
|
+
exports.getModelDefaults = getModelDefaults;
|
|
40
|
+
/**
|
|
41
|
+
* Per-`${providerId}:${modelId}` defaults. `undefined` lookup means
|
|
42
|
+
* the model takes no special handling.
|
|
43
|
+
*/
|
|
44
|
+
exports.MODEL_DEFAULTS = Object.freeze({
|
|
45
|
+
// DeepSeek V4 Pro — thinking-mode flagship.
|
|
46
|
+
// Reference: https://api-docs.deepseek.com/ (verified 2026-05).
|
|
47
|
+
// deepseek-v4-flash exists too but is not wired this slice; its
|
|
48
|
+
// legacy aliases (deepseek-chat = v4-flash non-think,
|
|
49
|
+
// deepseek-reasoner = v4-flash think) stay un-defaulted to preserve
|
|
50
|
+
// the existing pass-through behavior for users who explicitly
|
|
51
|
+
// selected them.
|
|
52
|
+
'deepseek:deepseek-v4-pro': {
|
|
53
|
+
extraBody: {
|
|
54
|
+
thinking: { type: 'enabled' },
|
|
55
|
+
reasoning_effort: 'high',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Look up defaults for a (provider, model) pair. Returns `undefined`
|
|
61
|
+
* when no entry exists — caller skips the extraBody merge.
|
|
62
|
+
*/
|
|
63
|
+
function getModelDefaults(providerId, modelId) {
|
|
64
|
+
return exports.MODEL_DEFAULTS[`${providerId}:${modelId}`];
|
|
65
|
+
}
|