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.
Files changed (68) hide show
  1. package/README.md +78 -26
  2. package/dist/cli/v4/aidenCLI.js +169 -9
  3. package/dist/cli/v4/callbacks.js +20 -2
  4. package/dist/cli/v4/chatSession.js +644 -16
  5. package/dist/cli/v4/commands/auth.js +6 -3
  6. package/dist/cli/v4/commands/doctor.js +23 -27
  7. package/dist/cli/v4/commands/help.js +4 -0
  8. package/dist/cli/v4/commands/index.js +10 -1
  9. package/dist/cli/v4/commands/model.js +30 -1
  10. package/dist/cli/v4/commands/reloadSoul.js +37 -0
  11. package/dist/cli/v4/commands/update.js +102 -0
  12. package/dist/cli/v4/defaultSoul.js +68 -2
  13. package/dist/cli/v4/display/capabilityCard.js +135 -0
  14. package/dist/cli/v4/display/sessionEndCard.js +127 -0
  15. package/dist/cli/v4/display/toolTrail.js +172 -0
  16. package/dist/cli/v4/display.js +492 -142
  17. package/dist/cli/v4/doctor.js +472 -58
  18. package/dist/cli/v4/doctorLiveness.js +65 -10
  19. package/dist/cli/v4/promotionPrompt.js +332 -0
  20. package/dist/cli/v4/providerBootSelector.js +144 -0
  21. package/dist/cli/v4/replyRenderer.js +311 -20
  22. package/dist/cli/v4/sessionSummaryGate.js +66 -0
  23. package/dist/cli/v4/skinEngine.js +14 -3
  24. package/dist/cli/v4/toolPreview.js +153 -0
  25. package/dist/core/tools/nowPlaying.js +7 -15
  26. package/dist/core/v4/aidenAgent.js +91 -29
  27. package/dist/core/v4/capabilities.js +89 -0
  28. package/dist/core/v4/contextCompressor.js +25 -8
  29. package/dist/core/v4/distillationIndex.js +167 -0
  30. package/dist/core/v4/distillationStore.js +98 -0
  31. package/dist/core/v4/logger/logger.js +40 -9
  32. package/dist/core/v4/promotionCandidates.js +234 -0
  33. package/dist/core/v4/promptBuilder.js +145 -1
  34. package/dist/core/v4/sessionDistiller.js +452 -0
  35. package/dist/core/v4/skillMining/skillMiner.js +43 -6
  36. package/dist/core/v4/skillOutcomeTracker.js +323 -0
  37. package/dist/core/v4/subsystemHealth.js +143 -0
  38. package/dist/core/v4/toolRegistry.js +16 -1
  39. package/dist/core/v4/update/executeInstall.js +233 -0
  40. package/dist/core/version.js +1 -1
  41. package/dist/moat/memoryGuard.js +111 -0
  42. package/dist/moat/plannerGuard.js +19 -0
  43. package/dist/moat/skillTeacher.js +14 -5
  44. package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
  45. package/dist/providers/v4/errors.js +112 -4
  46. package/dist/providers/v4/modelDefaults.js +65 -0
  47. package/dist/providers/v4/registry.js +9 -2
  48. package/dist/providers/v4/runtimeResolver.js +6 -0
  49. package/dist/tools/v4/index.js +80 -1
  50. package/dist/tools/v4/memory/memoryRemove.js +57 -2
  51. package/dist/tools/v4/memory/sessionSummary.js +151 -0
  52. package/dist/tools/v4/sessions/recallSession.js +177 -0
  53. package/dist/tools/v4/sessions/sessionSearch.js +5 -1
  54. package/dist/tools/v4/system/_psHelpers.js +123 -0
  55. package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
  56. package/dist/tools/v4/system/appClose.js +79 -0
  57. package/dist/tools/v4/system/appInput.js +154 -0
  58. package/dist/tools/v4/system/appLaunch.js +218 -0
  59. package/dist/tools/v4/system/clipboardRead.js +54 -0
  60. package/dist/tools/v4/system/clipboardWrite.js +84 -0
  61. package/dist/tools/v4/system/mediaKey.js +109 -0
  62. package/dist/tools/v4/system/mediaSessions.js +163 -0
  63. package/dist/tools/v4/system/mediaTransport.js +211 -0
  64. package/dist/tools/v4/system/osProcessList.js +99 -0
  65. package/dist/tools/v4/system/screenshot.js +106 -0
  66. package/dist/tools/v4/system/volumeSet.js +157 -0
  67. package/package.json +4 -1
  68. package/skills/system_control.md +185 -69
@@ -0,0 +1,211 @@
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
+ * tools/v4/system/mediaTransport.ts — `media_transport` tool. v4.1.4-media.
10
+ *
11
+ * Verified play/pause/skip against a specific GSMTC session. Replaces
12
+ * the blind-keystroke `media_key` behavior for the common case where
13
+ * the user names an app ("pause Spotify", "resume YouTube"): instead
14
+ * of blasting VK_MEDIA_PLAY_PAUSE at whichever app the OS most
15
+ * recently routed to, we enumerate sessions, match the target by
16
+ * AppUserModelId substring (or fall back to title contains), and call
17
+ * `TryPlayAsync()` / `TryPauseAsync()` / etc. directly on that session.
18
+ *
19
+ * Layer 2 of the three-layer media-control hierarchy v4.1.4 establishes:
20
+ * 1. Semantic API (Spotify Web API when authed) — out of this slice
21
+ * 2. OS media-session API (GSMTC) ← this tool writes
22
+ * 3. Global media keys (mediaKey tool) — blind fallback
23
+ *
24
+ * Honesty story: unlike `media_key`'s blind keystroke + degraded flag,
25
+ * `media_transport` reports `success: true` ONLY when GSMTC returns
26
+ * its `Success` result. Failures (session disappeared mid-call, app
27
+ * doesn't support that action, no matching target) surface as
28
+ * `success: false` with the specific reason. No degraded flag — we
29
+ * either have OS-confirmed action or we have an honest failure.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.mediaTransportTool = void 0;
33
+ const _psHelpers_1 = require("./_psHelpers");
34
+ /** GSMTC API call per action. Keys match the schema enum verbatim. */
35
+ const ACTION_METHOD = {
36
+ play: 'TryPlayAsync',
37
+ pause: 'TryPauseAsync',
38
+ toggle: 'TryTogglePlayPauseAsync',
39
+ next: 'TrySkipNextAsync',
40
+ previous: 'TrySkipPreviousAsync',
41
+ stop: 'TryStopAsync',
42
+ };
43
+ /**
44
+ * Build the PowerShell snippet. `target` is a case-insensitive substring
45
+ * matched against each session's AppUserModelId first, then the track
46
+ * title as a softer fallback. Empty/omitted target selects the current
47
+ * session (matches the legacy `media_key` semantics, no surprise).
48
+ *
49
+ * Output: a single JSON line with `matched` (boolean — did we find a
50
+ * session) and `result` (the GSMTC enum value as a string —
51
+ * `Success` / `Failed` / `UnknownError` etc.).
52
+ */
53
+ function buildPs(action, target) {
54
+ const method = ACTION_METHOD[action];
55
+ // Single-quote-escape target for PS string literal. Lowercase compare
56
+ // happens inside the script so the model can pass "Spotify" or "spotify".
57
+ const safeTarget = target.replace(/'/g, "''");
58
+ return `
59
+ ${(0, _psHelpers_1.winRtAwaitPreamble)()}
60
+ $mgType = [Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control,ContentType=WindowsRuntime]
61
+ $pType = [Windows.Media.Control.GlobalSystemMediaTransportControlsSessionMediaProperties,Windows.Media.Control,ContentType=WindowsRuntime]
62
+ $mgr = Await ($mgType::RequestAsync()) $mgType
63
+ $target = '${safeTarget}'
64
+ $picked = $null
65
+ if ($target.Length -gt 0) {
66
+ $lt = $target.ToLower()
67
+ foreach ($s in $mgr.GetSessions()) {
68
+ if ($s.SourceAppUserModelId -and $s.SourceAppUserModelId.ToLower().Contains($lt)) {
69
+ $picked = $s
70
+ break
71
+ }
72
+ }
73
+ if (-not $picked) {
74
+ # Soft fallback: title contains.
75
+ foreach ($s in $mgr.GetSessions()) {
76
+ $p = $null
77
+ try { $p = Await ($s.TryGetMediaPropertiesAsync()) $pType } catch { $p = $null }
78
+ if ($p -and $p.Title -and $p.Title.ToLower().Contains($lt)) {
79
+ $picked = $s
80
+ break
81
+ }
82
+ }
83
+ }
84
+ } else {
85
+ $picked = $mgr.GetCurrentSession()
86
+ }
87
+ if (-not $picked) {
88
+ @{ matched=$false; result='NoSession'; appUserModelId=$null } | ConvertTo-Json -Compress
89
+ exit 0
90
+ }
91
+ $res = Await ($picked.${method}()) ([bool])
92
+ # v4.1.3-essentials bugfix: PowerShell 5.1 does NOT accept a bare
93
+ # parenthesized \`if\` expression inside a hashtable literal — it
94
+ # parses \`(if ...)\` as a command invocation and fails with
95
+ # "The term 'if' is not recognized as the name of a cmdlet..." (no
96
+ # ternary operator until PS 7+). The \`$(...)\` subexpression
97
+ # operator forces statement-context evaluation in PS 5.1, which is
98
+ # what we need here.
99
+ $status = if ($res) { 'Success' } else { 'Failed' }
100
+ @{ matched=$true; result=$status; appUserModelId=$picked.SourceAppUserModelId } | ConvertTo-Json -Compress
101
+ `.trim();
102
+ }
103
+ exports.mediaTransportTool = {
104
+ schema: {
105
+ name: 'media_transport',
106
+ description: 'PREFERRED for named-app media control. Verified play/pause/skip ' +
107
+ 'against a specific Windows GSMTC media session — returns OS-confirmed ' +
108
+ 'success/failure, NOT a blind keystroke like `media_key`. Use this ' +
109
+ 'whenever the user names an app ("pause Spotify", "resume YouTube"). ' +
110
+ 'Target matches by AppUserModelId substring ("spotify" → Spotify.exe), ' +
111
+ 'then track title as soft fallback. Omit `target` to act on the ' +
112
+ 'current session. Pair with `media_sessions` (read) to enumerate ' +
113
+ 'available apps. Windows-only in v4.1.4.',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ action: {
118
+ type: 'string',
119
+ enum: ['play', 'pause', 'toggle', 'next', 'previous', 'stop'],
120
+ description: "Action to invoke on the matched session. 'toggle' flips " +
121
+ "play/pause. 'play' / 'pause' are explicit. 'next' / 'previous' " +
122
+ "skip tracks. 'stop' halts playback.",
123
+ },
124
+ target: {
125
+ type: 'string',
126
+ description: 'Optional app/track identifier. Case-insensitive substring ' +
127
+ 'match against AppUserModelId first ("spotify" matches ' +
128
+ 'Spotify.exe), then track title. Omit to act on the OS-routed ' +
129
+ 'current session.',
130
+ },
131
+ },
132
+ required: ['action'],
133
+ },
134
+ },
135
+ category: 'execute',
136
+ mutates: true,
137
+ toolset: 'system',
138
+ async execute(args, _ctx) {
139
+ if (!(0, _psHelpers_1.isWindows)()) {
140
+ // v4.1.3-essentials: tailored capability card for non-Windows.
141
+ // Layer-1 (web API) and layer-3b (CDP) alternatives exist on
142
+ // every platform; only layer-2 (GSMTC verified transport) is
143
+ // Windows-bound.
144
+ return (0, _psHelpers_1.windowsOnlyError)('media_transport', {
145
+ canStill: [
146
+ 'Use Spotify Web API via a skill that wraps OAuth + /me/player',
147
+ 'Use Chrome DevTools Protocol (`browser_*` tools) to drive a YouTube tab',
148
+ 'Use `shell_exec` with `playerctl` (Linux) or `osascript` (macOS) for system-wide control',
149
+ ],
150
+ cannotReliably: [
151
+ 'GSMTC-verified play/pause/skip with OS-level success confirmation',
152
+ 'Target a specific app by AppUserModelId without OS media-session APIs',
153
+ ],
154
+ fix: 'Run Aiden on Windows for GSMTC, OR install a Spotify-OAuth skill ' +
155
+ 'for layer-1 control, OR use `shell_exec` with the platform\'s media-key utility.',
156
+ });
157
+ }
158
+ const action = args.action;
159
+ if (!ACTION_METHOD[action]) {
160
+ return {
161
+ success: false,
162
+ error: `Unknown action: ${String(args.action)}. ` +
163
+ `Valid: ${Object.keys(ACTION_METHOD).join(', ')}`,
164
+ };
165
+ }
166
+ const target = typeof args.target === 'string' ? args.target.trim() : '';
167
+ try {
168
+ const { stdout } = await (0, _psHelpers_1.runPowerShell)(buildPs(action, target), {
169
+ timeoutMs: 8000,
170
+ });
171
+ const trimmed = stdout.trim();
172
+ if (trimmed.length === 0) {
173
+ return {
174
+ success: false,
175
+ error: 'media_transport returned empty output from PowerShell',
176
+ };
177
+ }
178
+ const parsed = JSON.parse(trimmed);
179
+ if (!parsed.matched) {
180
+ return {
181
+ success: false,
182
+ error: target
183
+ ? `No media session matched target "${target}". Call media_sessions to see what's available.`
184
+ : 'No active media session. Open a media app first (Spotify, YouTube, etc.).',
185
+ };
186
+ }
187
+ if (parsed.result !== 'Success') {
188
+ return {
189
+ success: false,
190
+ error: `GSMTC ${action} returned ${parsed.result} for ${parsed.appUserModelId}. ` +
191
+ `The app may not support that action in its current state.`,
192
+ appUserModelId: parsed.appUserModelId,
193
+ };
194
+ }
195
+ // OS-confirmed success. No degraded flag — unlike media_key we
196
+ // KNOW the action landed on a specific session and the OS
197
+ // accepted it.
198
+ return {
199
+ success: true,
200
+ action,
201
+ appUserModelId: parsed.appUserModelId,
202
+ };
203
+ }
204
+ catch (e) {
205
+ return {
206
+ success: false,
207
+ error: e instanceof Error ? e.message : String(e),
208
+ };
209
+ }
210
+ },
211
+ };
@@ -0,0 +1,99 @@
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
+ * tools/v4/system/osProcessList.ts — `os_process_list` tool.
10
+ *
11
+ * Lists OS-wide running processes via `Get-Process`. Distinct from the
12
+ * existing `process_list` tool which only enumerates child processes
13
+ * Aiden itself spawned via `process_spawn` — that's the wrong shape
14
+ * for questions like "is claude code running?" (the answer's process
15
+ * was started by the user, not Aiden).
16
+ *
17
+ * Filtering is supported via a substring on the process name. Default
18
+ * (no filter) lists the top-CPU processes capped at 30 so the model
19
+ * doesn't drown in a 200-row dump.
20
+ *
21
+ * Read-only. Cross-platform fallback returns a structured error
22
+ * pointing at the issue tracker.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.osProcessListTool = void 0;
26
+ const _psHelpers_1 = require("./_psHelpers");
27
+ const DEFAULT_LIMIT = 30;
28
+ const MAX_LIMIT = 200;
29
+ function buildPs(nameFilter, limit) {
30
+ // Escape single quotes for the PowerShell -Name argument.
31
+ const filter = (nameFilter ?? '').trim();
32
+ // Get-Process picks `Name` (short), `Id` (pid), `CPU` (seconds), and
33
+ // `WorkingSet` (memory). ConvertTo-Json emits an array we parse.
34
+ const base = filter.length > 0
35
+ ? `Get-Process -Name '*${filter.replace(/'/g, "''")}*' -ErrorAction SilentlyContinue`
36
+ : 'Get-Process';
37
+ return [
38
+ base,
39
+ `| Sort-Object CPU -Descending`,
40
+ `| Select-Object -First ${limit} Name, Id, @{N='CPU';E={[math]::Round($_.CPU,2)}}, @{N='MemoryMB';E={[math]::Round($_.WorkingSet64/1MB,1)}}`,
41
+ `| ConvertTo-Json -Compress -Depth 2`,
42
+ ].join(' ');
43
+ }
44
+ exports.osProcessListTool = {
45
+ schema: {
46
+ name: 'os_process_list',
47
+ description: 'List OS-wide running processes (top by CPU). Use this to answer questions like "is X running?" or "what apps are using CPU?". Supports an optional name substring filter. Distinct from `process_list` which only shows processes Aiden itself spawned. Windows-only in v4.1.2.',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ name: {
52
+ type: 'string',
53
+ description: 'Optional process-name substring filter, e.g. "claude" to find "claude.exe" / "claude_code.exe" / "claude-helper.exe". Omit to list top-CPU processes.',
54
+ },
55
+ limit: {
56
+ type: 'number',
57
+ description: 'Max rows to return (default 30, max 200). Use a higher value when answering "list everything running" style questions.',
58
+ },
59
+ },
60
+ },
61
+ },
62
+ category: 'read',
63
+ mutates: false,
64
+ toolset: 'system',
65
+ async execute(args, _ctx) {
66
+ if (!(0, _psHelpers_1.isWindows)())
67
+ return (0, _psHelpers_1.windowsOnlyError)('os_process_list');
68
+ const nameArg = typeof args.name === 'string' ? args.name : undefined;
69
+ const rawLimit = typeof args.limit === 'number' ? args.limit : DEFAULT_LIMIT;
70
+ const limit = Math.min(Math.max(1, Math.floor(rawLimit)), MAX_LIMIT);
71
+ try {
72
+ const { stdout } = await (0, _psHelpers_1.runPowerShell)(buildPs(nameArg, limit), {
73
+ timeoutMs: 15000,
74
+ });
75
+ const trimmed = stdout.trim();
76
+ // Get-Process returns nothing when the filter matches zero processes;
77
+ // PowerShell pipeline prints empty. Treat as "no matches" success.
78
+ if (trimmed.length === 0) {
79
+ return { success: true, processes: [], count: 0, filter: nameArg };
80
+ }
81
+ // ConvertTo-Json emits an object (single result) or array (multiple).
82
+ // Normalise to array.
83
+ const parsed = JSON.parse(trimmed);
84
+ const processes = Array.isArray(parsed) ? parsed : [parsed];
85
+ return {
86
+ success: true,
87
+ count: processes.length,
88
+ filter: nameArg,
89
+ processes,
90
+ };
91
+ }
92
+ catch (e) {
93
+ return {
94
+ success: false,
95
+ error: e instanceof Error ? e.message : String(e),
96
+ };
97
+ }
98
+ },
99
+ };
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * tools/v4/system/screenshot.ts — `screenshot` tool.
10
+ *
11
+ * Captures the full desktop and writes it as a PNG to
12
+ * `<aidenHome>/screenshots/<timestamp>.png`. Returns the absolute path
13
+ * in `path` so a Telegram / Discord channel adapter can attach the
14
+ * file directly without a separate file_read round-trip.
15
+ *
16
+ * Privacy note (Phase v4.1.2-followup-3): this tool reads what is
17
+ * currently visible on the screen — anything in front of the user.
18
+ * The tool description says so explicitly so users know what they're
19
+ * approving when the model invokes it.
20
+ *
21
+ * Implementation: PowerShell-only, no native dependency. Uses
22
+ * `System.Windows.Forms.Screen` + `System.Drawing.Bitmap` /
23
+ * `Graphics.CopyFromScreen()` — both ship with .NET on every modern
24
+ * Windows install. Cross-platform fallback returns a structured error.
25
+ */
26
+ var __importDefault = (this && this.__importDefault) || function (mod) {
27
+ return (mod && mod.__esModule) ? mod : { "default": mod };
28
+ };
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.screenshotTool = void 0;
31
+ const node_fs_1 = require("node:fs");
32
+ const node_path_1 = __importDefault(require("node:path"));
33
+ const _psHelpers_1 = require("./_psHelpers");
34
+ /**
35
+ * Build the PowerShell capture script. The bitmap dimensions come from
36
+ * `Screen::PrimaryScreen.Bounds` so we get the actual primary-monitor
37
+ * resolution, not a hardcoded value. SaveAs PNG to keep losslessness
38
+ * — file size on a 4K screen is ~4-8 MB which is fine for chat.
39
+ */
40
+ function buildScreenshotPs(outPath) {
41
+ const psQuoted = outPath.replace(/'/g, "''"); // PowerShell single-quote escape
42
+ return [
43
+ 'Add-Type -AssemblyName System.Windows.Forms;',
44
+ 'Add-Type -AssemblyName System.Drawing;',
45
+ '$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds;',
46
+ '$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height;',
47
+ '$gfx = [System.Drawing.Graphics]::FromImage($bitmap);',
48
+ '$gfx.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size);',
49
+ `$bitmap.Save('${psQuoted}', [System.Drawing.Imaging.ImageFormat]::Png);`,
50
+ '$gfx.Dispose(); $bitmap.Dispose();',
51
+ `Write-Output '${psQuoted}';`,
52
+ ].join(' ');
53
+ }
54
+ exports.screenshotTool = {
55
+ schema: {
56
+ name: 'screenshot',
57
+ description: 'Capture the current primary-monitor desktop as a PNG. Returns the absolute path of the saved file. Reads whatever is currently visible on screen — privacy-sensitive; only invoke when the user explicitly asks for a screenshot or screen share. Windows-only in v4.1.2.',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {},
61
+ },
62
+ },
63
+ category: 'read',
64
+ mutates: false,
65
+ toolset: 'system',
66
+ async execute(_args, ctx) {
67
+ if (!(0, _psHelpers_1.isWindows)())
68
+ return (0, _psHelpers_1.windowsOnlyError)('screenshot');
69
+ if (!ctx.paths) {
70
+ return { success: false, error: 'aiden paths not wired (test mode?)' };
71
+ }
72
+ try {
73
+ const dir = node_path_1.default.join(ctx.paths.root, 'screenshots');
74
+ await node_fs_1.promises.mkdir(dir, { recursive: true });
75
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
76
+ const outPath = node_path_1.default.join(dir, `${stamp}.png`);
77
+ const { stdout } = await (0, _psHelpers_1.runPowerShell)(buildScreenshotPs(outPath), {
78
+ timeoutMs: 30000,
79
+ });
80
+ // Verify the file actually landed on disk — PowerShell can exit 0
81
+ // and have written nothing on an exotic display configuration.
82
+ try {
83
+ const stat = await node_fs_1.promises.stat(outPath);
84
+ return {
85
+ success: true,
86
+ path: outPath,
87
+ size: stat.size,
88
+ // For Telegram / Discord adapters — they can attach via path.
89
+ attachAs: 'image/png',
90
+ };
91
+ }
92
+ catch {
93
+ return {
94
+ success: false,
95
+ error: `screenshot script ran but file not found at ${outPath} (stdout=${stdout.trim().slice(0, 120)})`,
96
+ };
97
+ }
98
+ }
99
+ catch (e) {
100
+ return {
101
+ success: false,
102
+ error: e instanceof Error ? e.message : String(e),
103
+ };
104
+ }
105
+ },
106
+ };
@@ -0,0 +1,157 @@
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
+ * tools/v4/system/volumeSet.ts — `volume_set` tool.
10
+ *
11
+ * Set the Windows master-volume level to a percentage (0-100), or
12
+ * toggle mute. Uses the Shell.Application COM object's appCommand
13
+ * SendKeys for up/down nudges; for absolute level setting, talks to
14
+ * `IAudioEndpointVolume` via PowerShell inline C# (Add-Type) — no
15
+ * external binary (no nircmd, no soundvolumeview).
16
+ *
17
+ * The inline-C# approach is well-trodden Windows lore: the
18
+ * IAudioEndpointVolume COM interface is part of the Core Audio APIs
19
+ * available since Vista. We declare the interop types in PowerShell,
20
+ * invoke `SetMasterVolumeLevelScalar`, and clean up. Adds ~1-2s
21
+ * cold-start (PowerShell Add-Type compilation) — fine for an
22
+ * occasionally-invoked control.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.volumeSetTool = void 0;
26
+ const _psHelpers_1 = require("./_psHelpers");
27
+ const ADD_TYPE_AUDIO = `
28
+ Add-Type -TypeDefinition @'
29
+ using System;
30
+ using System.Runtime.InteropServices;
31
+ [Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
32
+ interface IAudioEndpointVolume {
33
+ int RegisterControlChangeNotify(IntPtr cb);
34
+ int UnregisterControlChangeNotify(IntPtr cb);
35
+ int GetChannelCount(out uint count);
36
+ int SetMasterVolumeLevel(float level, Guid ctx);
37
+ int SetMasterVolumeLevelScalar(float level, Guid ctx);
38
+ int GetMasterVolumeLevel(out float level);
39
+ int GetMasterVolumeLevelScalar(out float level);
40
+ int SetChannelVolumeLevel(uint ch, float level, Guid ctx);
41
+ int SetChannelVolumeLevelScalar(uint ch, float level, Guid ctx);
42
+ int GetChannelVolumeLevel(uint ch, out float level);
43
+ int GetChannelVolumeLevelScalar(uint ch, out float level);
44
+ int SetMute(bool mute, Guid ctx);
45
+ int GetMute(out bool mute);
46
+ int GetVolumeStepInfo(out uint step, out uint count);
47
+ int VolumeStepUp(Guid ctx);
48
+ int VolumeStepDown(Guid ctx);
49
+ int QueryHardwareSupport(out uint mask);
50
+ int GetVolumeRange(out float min, out float max, out float inc);
51
+ }
52
+ [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
53
+ interface IMMDeviceEnumerator {
54
+ int NotImpl0();
55
+ int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice ep);
56
+ }
57
+ [Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
58
+ interface IMMDevice {
59
+ int Activate(ref Guid id, int clsCtx, IntPtr pa, [MarshalAs(UnmanagedType.IUnknown)] out object o);
60
+ }
61
+ [ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
62
+ class MMDeviceEnumeratorComObject { }
63
+ public static class Audio {
64
+ static IAudioEndpointVolume Vol() {
65
+ var enumerator = new MMDeviceEnumeratorComObject() as IMMDeviceEnumerator;
66
+ IMMDevice dev = null;
67
+ Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(0, 1, out dev));
68
+ Guid epvid = typeof(IAudioEndpointVolume).GUID;
69
+ object o;
70
+ Marshal.ThrowExceptionForHR(dev.Activate(ref epvid, 7, IntPtr.Zero, out o));
71
+ return o as IAudioEndpointVolume;
72
+ }
73
+ public static float GetLevel() { float l; Marshal.ThrowExceptionForHR(Vol().GetMasterVolumeLevelScalar(out l)); return l; }
74
+ public static void SetLevel(float level) { Marshal.ThrowExceptionForHR(Vol().SetMasterVolumeLevelScalar(level, Guid.Empty)); }
75
+ public static bool GetMute() { bool m; Marshal.ThrowExceptionForHR(Vol().GetMute(out m)); return m; }
76
+ public static void SetMute(bool mute) { Marshal.ThrowExceptionForHR(Vol().SetMute(mute, Guid.Empty)); }
77
+ }
78
+ '@;
79
+ `;
80
+ function buildPs(action, percent) {
81
+ if (action === 'set' && typeof percent === 'number') {
82
+ const scalar = (Math.max(0, Math.min(100, percent)) / 100).toFixed(4);
83
+ return [
84
+ ADD_TYPE_AUDIO,
85
+ `[Audio]::SetLevel([float]${scalar});`,
86
+ `$level = [Audio]::GetLevel();`,
87
+ `Write-Output ([math]::Round($level * 100, 0));`,
88
+ ].join('\n');
89
+ }
90
+ if (action === 'mute') {
91
+ return [ADD_TYPE_AUDIO, `[Audio]::SetMute($true); Write-Output 'muted';`].join('\n');
92
+ }
93
+ if (action === 'unmute') {
94
+ return [ADD_TYPE_AUDIO, `[Audio]::SetMute($false); Write-Output 'unmuted';`].join('\n');
95
+ }
96
+ // toggle_mute
97
+ return [
98
+ ADD_TYPE_AUDIO,
99
+ `$cur = [Audio]::GetMute(); [Audio]::SetMute(-not $cur);`,
100
+ `Write-Output (if (-not $cur) {'muted'} else {'unmuted'});`,
101
+ ].join('\n');
102
+ }
103
+ exports.volumeSetTool = {
104
+ schema: {
105
+ name: 'volume_set',
106
+ description: 'Set Windows master volume to a percentage (0-100), or mute / unmute / toggle mute. Operates on the default audio endpoint. Windows-only in v4.1.2.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ action: {
111
+ type: 'string',
112
+ enum: ['set', 'mute', 'unmute', 'toggle_mute'],
113
+ description: "'set' requires `percent`. 'mute' / 'unmute' force the state. 'toggle_mute' flips it.",
114
+ },
115
+ percent: {
116
+ type: 'number',
117
+ description: 'Target volume 0-100 (only used when action="set"). Values outside the range are clamped.',
118
+ },
119
+ },
120
+ required: ['action'],
121
+ },
122
+ },
123
+ category: 'execute',
124
+ mutates: true,
125
+ toolset: 'system',
126
+ async execute(args, _ctx) {
127
+ if (!(0, _psHelpers_1.isWindows)())
128
+ return (0, _psHelpers_1.windowsOnlyError)('volume_set');
129
+ const action = args.action;
130
+ if (!['set', 'mute', 'unmute', 'toggle_mute'].includes(action)) {
131
+ return {
132
+ success: false,
133
+ error: `Unknown volume action: ${String(args.action)}. ` +
134
+ `Valid: set, mute, unmute, toggle_mute`,
135
+ };
136
+ }
137
+ const percent = typeof args.percent === 'number' ? args.percent : undefined;
138
+ if (action === 'set' && percent === undefined) {
139
+ return {
140
+ success: false,
141
+ error: "action='set' requires a numeric `percent` (0-100).",
142
+ };
143
+ }
144
+ try {
145
+ const { stdout } = await (0, _psHelpers_1.runPowerShell)(buildPs(action, percent), {
146
+ timeoutMs: 10000,
147
+ });
148
+ return { success: true, action, result: stdout.trim() };
149
+ }
150
+ catch (e) {
151
+ return {
152
+ success: false,
153
+ error: e instanceof Error ? e.message : String(e),
154
+ };
155
+ }
156
+ },
157
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-runtime",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -77,6 +77,9 @@
77
77
  "dist:linux": "node scripts/prepare-electron.js && electron-builder --linux --x64 --publish never && node -e \"const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.main='./dist/index.js';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n');console.log(' main restored')\"",
78
78
  "test": "vitest run",
79
79
  "test:ui": "vitest --ui",
80
+ "eval": "ts-node evals/cli.ts",
81
+ "eval:honesty": "ts-node evals/cli.ts --suite honesty",
82
+ "eval:scenario": "ts-node evals/cli.ts --scenario",
80
83
  "stress-test": "npx ts-node tests/stressTest.ts",
81
84
  "test:all": "npx ts-node tests/e2e/masterTestSuite.ts",
82
85
  "test:unit": "npx ts-node tests/e2e/masterTestSuite.ts --part1",