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,218 @@
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/appLaunch.ts — `app_launch` tool.
10
+ *
11
+ * Start a Windows application by executable name or absolute path via
12
+ * PowerShell `Start-Process`. Resolves bare names through PATH and
13
+ * `App Paths` registry (so `spotify`, `notepad`, `chrome` all work
14
+ * without the user supplying the full path).
15
+ *
16
+ * Returns the PID of the launched process when available — useful for
17
+ * a subsequent `app_close` invocation or for confirming "did the
18
+ * launch succeed?" without a `window_list` round-trip.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.appLaunchTool = void 0;
22
+ exports.processNameFromApp = processNameFromApp;
23
+ const _psHelpers_1 = require("./_psHelpers");
24
+ /**
25
+ * Derive the bare process-name we expect `Get-Process` to find after
26
+ * launch. Strips path components, lowercases, drops the `.exe` extension.
27
+ * Used by the v4.1.3-essentials launch-verification poll.
28
+ *
29
+ * "C:\\Program Files\\Spotify\\Spotify.exe" → "spotify"
30
+ * "Spotify.exe" → "spotify"
31
+ * "spotify" → "spotify"
32
+ * "notepad++.exe" → "notepad++"
33
+ *
34
+ * Pure helper, exported for unit testing.
35
+ */
36
+ function processNameFromApp(app) {
37
+ // Strip path components (Windows uses \; tolerate / too).
38
+ let bare = app.replace(/\\/g, '/').split('/').pop() ?? app;
39
+ // Drop a single trailing .exe (case-insensitive).
40
+ bare = bare.replace(/\.exe$/i, '');
41
+ return bare.toLowerCase();
42
+ }
43
+ function buildPs(appName, args) {
44
+ // Single-quote escape the app name for PowerShell.
45
+ const safeApp = appName.replace(/'/g, "''");
46
+ // The Get-Process verification probe uses the bare process name
47
+ // (no path, no .exe). Compute it once on the TS side so the PS
48
+ // script doesn't have to do string surgery.
49
+ const procName = processNameFromApp(appName).replace(/'/g, "''");
50
+ const argString = args && args.length > 0
51
+ ? `-ArgumentList @(${args.map((a) => `'${a.replace(/'/g, "''")}'`).join(',')})`
52
+ : '';
53
+ // v4.1.3-essentials launch reliability fix:
54
+ //
55
+ // Primary path: `Start-Process -PassThru` — captures PID for any
56
+ // traditional Win32 exe. Fails for UWP / Microsoft Store apps
57
+ // (Spotify is UWP on most systems) because UWP launches route
58
+ // through ShellExecute which doesn't yield a child-process handle
59
+ // for `-PassThru`.
60
+ //
61
+ // Fallback path: `[System.Diagnostics.Process]::Start($app)` — the
62
+ // direct .NET ShellExecute call. Same App Paths / shell-association
63
+ // resolution as cmd's `start` builtin, but with proper error
64
+ // propagation (Windows popup → .NET exception → PS throw → tool
65
+ // returns success:false) and no quoting hell.
66
+ //
67
+ // Verification: after either path lands "PID=unknown", sleep 300ms
68
+ // and probe `Get-Process` for the bare process name. If the process
69
+ // exists, capture its PID — the launch verifiably succeeded. If not,
70
+ // signal "launched but no matching process appeared" so the tool can
71
+ // surface `success:false` honestly instead of pretending it worked.
72
+ return [
73
+ `$ErrorActionPreference = 'Stop';`,
74
+ `$pid_out = $null;`,
75
+ `try {`,
76
+ ` $p = Start-Process '${safeApp}' ${argString} -PassThru;`,
77
+ ` if ($p -and $p.Id) { $pid_out = $p.Id }`,
78
+ `} catch {`,
79
+ ` try {`,
80
+ ` $p = [System.Diagnostics.Process]::Start('${safeApp}');`,
81
+ ` if ($p -and $p.Id) { $pid_out = $p.Id }`,
82
+ ` } catch {`,
83
+ ` Write-Output ('LAUNCH_FAILED=' + $_.Exception.Message);`,
84
+ ` return;`,
85
+ ` }`,
86
+ `}`,
87
+ // If we got a PID from either Start-Process or .NET Process.Start,
88
+ // we're done — emit it and exit.
89
+ `if ($pid_out) { Write-Output ('PID=' + $pid_out); return };`,
90
+ // Otherwise (UWP path, both layers returned null) verify via
91
+ // Get-Process. 300ms grace; enough for Windows shell to either
92
+ // launch the app or surface the "cannot find" popup.
93
+ `Start-Sleep -Milliseconds 300;`,
94
+ `$found = Get-Process -Name '${procName}' -ErrorAction SilentlyContinue ` +
95
+ `| Select-Object -First 1;`,
96
+ `if ($found) {`,
97
+ ` Write-Output ('PID=' + $found.Id + ' (verified via Get-Process)');`,
98
+ `} else {`,
99
+ ` Write-Output ('LAUNCH_UNVERIFIED=' + '${procName}');`,
100
+ `}`,
101
+ ].join(' ');
102
+ }
103
+ exports.appLaunchTool = {
104
+ schema: {
105
+ name: 'app_launch',
106
+ description: 'Launch a Windows application by exe name, friendly name (resolved via App Paths registry), or absolute path. Returns the launched PID when available. Use for "open Spotify" / "start Chrome" / etc. Windows-only in v4.1.2.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ app: {
111
+ type: 'string',
112
+ description: 'Application identifier. Accepts: bare name (e.g. "spotify", "notepad", "chrome"), exe basename ("notepad.exe"), or absolute path ("C:\\\\Program Files\\\\App\\\\app.exe").',
113
+ },
114
+ args: {
115
+ type: 'array',
116
+ description: 'Optional command-line arguments to pass to the app.',
117
+ items: { type: 'string', description: 'A single CLI argument string.' },
118
+ },
119
+ },
120
+ required: ['app'],
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)('app_launch');
129
+ const app = typeof args.app === 'string' ? args.app.trim() : '';
130
+ if (!app) {
131
+ return { success: false, error: '`app` is required and must be non-empty.' };
132
+ }
133
+ const rawArgs = Array.isArray(args.args) ? args.args : undefined;
134
+ const cliArgs = rawArgs?.filter((a) => typeof a === 'string');
135
+ try {
136
+ const { stdout } = await (0, _psHelpers_1.runPowerShell)(buildPs(app, cliArgs), {
137
+ timeoutMs: 20000,
138
+ });
139
+ const out = stdout.trim();
140
+ // v4.1.3-essentials: the PS script emits exactly ONE of three
141
+ // outcomes. Parse in order of confidence:
142
+ // 1. `LAUNCH_FAILED=<message>` → .NET Process.Start threw;
143
+ // the popup-error class is here.
144
+ // 2. `LAUNCH_UNVERIFIED=<name>` → ShellExecute returned but
145
+ // no matching process appeared
146
+ // within 300ms — silently broken.
147
+ // 3. `PID=<n>` (optional `(verified via Get-Process)` suffix) →
148
+ // verified launch with PID.
149
+ //
150
+ // Outcomes 1 and 2 return `success:false` so the model + user see
151
+ // the honest failure instead of a "launched" lie. Outcome 3 still
152
+ // sets `degraded:true` for the case where Start-Process succeeded
153
+ // but the app might still crash post-init (Spotify "boots" for 21s
154
+ // before stable state) — caller verifies via `os_process_list`.
155
+ const launchFailedMatch = out.match(/LAUNCH_FAILED=(.+)$/m);
156
+ if (launchFailedMatch) {
157
+ return {
158
+ success: false,
159
+ app,
160
+ raw: out,
161
+ error: `Could not launch '${app}': ${launchFailedMatch[1].trim()}. ` +
162
+ `Verify the app is installed and resolvable via App Paths or PATH.`,
163
+ };
164
+ }
165
+ const launchUnverifiedMatch = out.match(/LAUNCH_UNVERIFIED=(.+)$/m);
166
+ if (launchUnverifiedMatch) {
167
+ return {
168
+ success: false,
169
+ app,
170
+ raw: out,
171
+ error: `Launch attempted but no process named '${launchUnverifiedMatch[1].trim()}' ` +
172
+ `appeared within 300ms. Windows may have shown an error dialog, ` +
173
+ `or the app failed to start. Try \`os_process_list\` with a ` +
174
+ `name filter to confirm, or pass an absolute path.`,
175
+ };
176
+ }
177
+ // Extract PID — both bare `PID=12345` and the verified
178
+ // `PID=12345 (verified via Get-Process)` shapes parse the same.
179
+ const pidMatch = out.match(/PID=(\d+)/);
180
+ const pid = pidMatch ? Number(pidMatch[1]) : null;
181
+ const verified = /verified via Get-Process/.test(out);
182
+ if (pid === null) {
183
+ // Shouldn't happen — the PS script always emits one of the
184
+ // three outcome lines. Surface honestly so the model sees the
185
+ // unexpected stdout instead of pretending success.
186
+ return {
187
+ success: false,
188
+ app,
189
+ raw: out,
190
+ error: `Launch returned unexpected stdout (no PID / failure sentinel). ` +
191
+ `Output: ${out.slice(0, 200)}`,
192
+ };
193
+ }
194
+ // Verified launch — still degraded because the app may crash
195
+ // post-init or split into a different process tree (Chrome's
196
+ // multi-process model, Spotify's spawn-and-detach). The honest
197
+ // signal is "we have a PID we can hand off; verify via
198
+ // os_process_list before relying on it".
199
+ return {
200
+ success: true,
201
+ app,
202
+ pid,
203
+ verified,
204
+ raw: out,
205
+ degraded: true,
206
+ degradedReason: verified
207
+ ? `launched (PID ${pid}, verified via Get-Process); call os_process_list to confirm it's still alive`
208
+ : `launched (PID ${pid}); call os_process_list to confirm it's still alive`,
209
+ };
210
+ }
211
+ catch (e) {
212
+ return {
213
+ success: false,
214
+ error: e instanceof Error ? e.message : String(e),
215
+ };
216
+ }
217
+ },
218
+ };
@@ -0,0 +1,54 @@
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/clipboardRead.ts — `clipboard_read` tool.
10
+ *
11
+ * Read the current Windows clipboard contents as text via PowerShell
12
+ * `Get-Clipboard`. Non-text clipboard contents (image, file list, RTF)
13
+ * return an empty string — text-only by design; binary surfaces would
14
+ * need a different rendering contract.
15
+ *
16
+ * Privacy note: clipboard contents can include passwords, OTPs, and
17
+ * personal text. Tool description flags this so the model can warn
18
+ * the user before reading sensitive contexts.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.clipboardReadTool = void 0;
22
+ const _psHelpers_1 = require("./_psHelpers");
23
+ exports.clipboardReadTool = {
24
+ schema: {
25
+ name: 'clipboard_read',
26
+ description: 'Read the current Windows clipboard contents as text. Non-text clipboard data returns an empty string. Privacy-sensitive: clipboard may contain passwords, OTPs, or personal text — only invoke when the user has clearly asked. Windows-only in v4.1.2.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {},
30
+ },
31
+ },
32
+ category: 'read',
33
+ mutates: false,
34
+ toolset: 'system',
35
+ async execute(_args, _ctx) {
36
+ if (!(0, _psHelpers_1.isWindows)())
37
+ return (0, _psHelpers_1.windowsOnlyError)('clipboard_read');
38
+ try {
39
+ // -Raw returns the whole buffer as one string (including newlines)
40
+ // rather than splitting on line breaks.
41
+ const { stdout } = await (0, _psHelpers_1.runPowerShell)('Get-Clipboard -Raw', { timeoutMs: 5000 });
42
+ // PowerShell appends a trailing CRLF — strip ONE trailing newline
43
+ // so the model sees what the user actually copied.
44
+ const text = stdout.replace(/\r?\n$/, '');
45
+ return { success: true, text, length: text.length };
46
+ }
47
+ catch (e) {
48
+ return {
49
+ success: false,
50
+ error: e instanceof Error ? e.message : String(e),
51
+ };
52
+ }
53
+ },
54
+ };
@@ -0,0 +1,84 @@
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/clipboardWrite.ts — `clipboard_write` tool.
10
+ *
11
+ * Write text to the Windows clipboard via PowerShell `Set-Clipboard`.
12
+ * Caller passes the text as a string arg; we route it through stdin to
13
+ * the PowerShell process to side-step shell-argument quoting issues
14
+ * with newlines / special chars (Aiden's existing `shellInterpolation`
15
+ * pattern doesn't apply to tool args, but stdin is still the safest
16
+ * conduit for arbitrary text).
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.clipboardWriteTool = void 0;
20
+ const node_child_process_1 = require("node:child_process");
21
+ const _psHelpers_1 = require("./_psHelpers");
22
+ /**
23
+ * Spawn `powershell.exe Set-Clipboard` with the text piped on stdin.
24
+ * Wrapper Promise so the tool's `execute` can `await` it.
25
+ */
26
+ function setClipboardViaStdin(text, timeoutMs) {
27
+ return new Promise((resolve, reject) => {
28
+ const ps = (0, node_child_process_1.exec)(
29
+ // -Command - reads the script from stdin; but we want to PIPE the
30
+ // *value* not the script. Cleanest cross-version PowerShell path:
31
+ // read stdin in PowerShell and pass to Set-Clipboard.
32
+ 'powershell.exe -NoProfile -Command "$input | Set-Clipboard"', { timeout: timeoutMs, windowsHide: true }, (err) => {
33
+ if (err)
34
+ reject(err);
35
+ else
36
+ resolve();
37
+ });
38
+ if (!ps.stdin) {
39
+ reject(new Error('PowerShell child has no stdin'));
40
+ return;
41
+ }
42
+ ps.stdin.write(text);
43
+ ps.stdin.end();
44
+ });
45
+ }
46
+ exports.clipboardWriteTool = {
47
+ schema: {
48
+ name: 'clipboard_write',
49
+ description: 'Write text to the Windows clipboard. Replaces existing clipboard contents. Handles multi-line strings and special characters safely (text routed via stdin). Windows-only in v4.1.2.',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ text: {
54
+ type: 'string',
55
+ description: 'Text to place on the clipboard. Replaces whatever is currently there.',
56
+ },
57
+ },
58
+ required: ['text'],
59
+ },
60
+ },
61
+ category: 'execute',
62
+ mutates: true,
63
+ toolset: 'system',
64
+ async execute(args, _ctx) {
65
+ if (!(0, _psHelpers_1.isWindows)())
66
+ return (0, _psHelpers_1.windowsOnlyError)('clipboard_write');
67
+ const text = typeof args.text === 'string' ? args.text : '';
68
+ // Empty string IS valid — it clears the clipboard. Distinguished
69
+ // from "no arg supplied" by the explicit type check.
70
+ if (typeof args.text !== 'string') {
71
+ return { success: false, error: '`text` is required and must be a string.' };
72
+ }
73
+ try {
74
+ await setClipboardViaStdin(text, 5000);
75
+ return { success: true, length: text.length };
76
+ }
77
+ catch (e) {
78
+ return {
79
+ success: false,
80
+ error: e instanceof Error ? e.message : String(e),
81
+ };
82
+ }
83
+ },
84
+ };
@@ -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
+ * tools/v4/system/mediaKey.ts — `media_key` tool.
10
+ *
11
+ * Send Windows media-control keys (play/pause, next, previous, stop)
12
+ * via PowerShell `System.Windows.Forms.SendKeys`. Works against any
13
+ * app that registers with SMTC (Spotify, YouTube in browser, Windows
14
+ * Media Player, Apple Music for Windows, VLC with MediaKey plugin,
15
+ * etc.) — the OS routes the keypress to the currently-active media
16
+ * session, so no per-app integration is required.
17
+ *
18
+ * Pairs with `now_playing` (read-only probe): the model reads what's
19
+ * playing, then issues the right media_key to control it.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.mediaKeyTool = void 0;
23
+ const _psHelpers_1 = require("./_psHelpers");
24
+ const ACTION_KEYS = {
25
+ play_pause: '{MEDIA_PLAY_PAUSE}',
26
+ next: '{MEDIA_NEXT_TRACK}',
27
+ previous: '{MEDIA_PREV_TRACK}',
28
+ stop: '{MEDIA_STOP}',
29
+ };
30
+ exports.mediaKeyTool = {
31
+ schema: {
32
+ name: 'media_key',
33
+ description: 'FALLBACK ONLY — prefer `media_transport(action, target)` for verified ' +
34
+ 'control of named apps (Spotify, YouTube, etc.). Use `media_key` only ' +
35
+ 'when (1) the target app is unknown / not registered with the OS media ' +
36
+ 'bus, or (2) `media_transport` returned `NoSession`. Blind global ' +
37
+ 'keystroke (VK_MEDIA_PLAY_PAUSE and friends) — Windows doesn\'t surface ' +
38
+ 'routing outcome, so this tool always reports `degraded:true`. Pair ' +
39
+ 'with `now_playing` to inspect state first. Windows-only.',
40
+ inputSchema: {
41
+ type: 'object',
42
+ properties: {
43
+ action: {
44
+ type: 'string',
45
+ enum: ['play_pause', 'next', 'previous', 'stop'],
46
+ description: "'play_pause' toggles play/pause on the active media session. " +
47
+ "'next' / 'previous' skip tracks. 'stop' halts playback.",
48
+ },
49
+ },
50
+ required: ['action'],
51
+ },
52
+ },
53
+ category: 'execute',
54
+ mutates: true,
55
+ toolset: 'system',
56
+ async execute(args, _ctx) {
57
+ if (!(0, _psHelpers_1.isWindows)()) {
58
+ return (0, _psHelpers_1.windowsOnlyError)('media_key', {
59
+ canStill: [
60
+ '`shell_exec` with `xdotool key XF86AudioPlay` on Linux X11',
61
+ '`shell_exec` with `osascript -e \'tell application "Spotify" to playpause\'` on macOS',
62
+ 'Use `media_transport` if a layer-1 skill (Spotify Web API) is installed',
63
+ ],
64
+ cannotReliably: [
65
+ 'Blind global VK_MEDIA_PLAY_PAUSE keystroke via SendKeys',
66
+ ],
67
+ fix: 'Run Aiden on Windows for direct media-key emission, or use the ' +
68
+ 'platform-native helpers above via `shell_exec`.',
69
+ });
70
+ }
71
+ const action = args.action;
72
+ if (!ACTION_KEYS[action]) {
73
+ return {
74
+ success: false,
75
+ error: `Unknown media action: ${String(args.action)}. ` +
76
+ `Valid: ${Object.keys(ACTION_KEYS).join(', ')}`,
77
+ };
78
+ }
79
+ const sendkey = ACTION_KEYS[action];
80
+ const script = [
81
+ 'Add-Type -AssemblyName System.Windows.Forms;',
82
+ `[System.Windows.Forms.SendKeys]::SendWait('${sendkey}');`,
83
+ `Write-Output 'sent:${action}';`,
84
+ ].join(' ');
85
+ try {
86
+ await (0, _psHelpers_1.runPowerShell)(script, { timeoutMs: 5000 });
87
+ // v4.1.3-repl-polish: SendKeys returns 0 whether or not any
88
+ // media-aware app received the keystroke — Windows doesn't
89
+ // surface the SMTC routing outcome to user-mode. We could
90
+ // scan `osProcessListImpl` for known media apps, but that's
91
+ // a cross-tool dep that distorts mediaKey's surface area. The
92
+ // honest answer is "we don't know if it landed"; the trail
93
+ // row renders yellow to signal that to the user without
94
+ // affecting the model's read of the result.
95
+ return {
96
+ success: true,
97
+ action,
98
+ degraded: true,
99
+ degradedReason: 'media key sent; cannot verify any app received it',
100
+ };
101
+ }
102
+ catch (e) {
103
+ return {
104
+ success: false,
105
+ error: e instanceof Error ? e.message : String(e),
106
+ };
107
+ }
108
+ },
109
+ };
@@ -0,0 +1,163 @@
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/mediaSessions.ts — `media_sessions` tool. v4.1.4-media.
10
+ *
11
+ * Enumerate every Windows GSMTC (GlobalSystemMediaTransportControls) media
12
+ * session — one entry per app that has registered with the OS media bus
13
+ * (Spotify, YouTube in browser, Windows Media Player, Apple Music for
14
+ * Windows, VLC with the SMTC plugin, etc.).
15
+ *
16
+ * Layer 2 of the three-layer media-control hierarchy v4.1.4 establishes:
17
+ * 1. Semantic API (Spotify Web API when authed) — out of this slice
18
+ * 2. OS media-session API (GSMTC) ← this tool reads, mediaTransport writes
19
+ * 3. Global media keys (mediaKey tool) — blind fallback
20
+ *
21
+ * Pairs with `media_transport` (write tool) — the model calls
22
+ * `media_sessions` to see what's available, then `media_transport`
23
+ * with a target string ("spotify", "chrome", etc.) to act. Distinct
24
+ * from `now_playing` which only returns the SINGLE active session.
25
+ *
26
+ * Read-only. Windows-only in v4.1.4 (consistent with the rest of the
27
+ * computer-control family).
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.__friendlyAppName = exports.mediaSessionsTool = void 0;
31
+ const _psHelpers_1 = require("./_psHelpers");
32
+ /** Map a Windows AppUserModelId to a friendly display name. Mirror of
33
+ * the normalization in core/tools/nowPlaying.ts; kept in sync so the
34
+ * two tools talk about the same app the same way. */
35
+ function friendlyAppName(aumid) {
36
+ if (!aumid)
37
+ return 'unknown';
38
+ const id = aumid.toLowerCase();
39
+ if (id.includes('spotify'))
40
+ return 'Spotify';
41
+ if (id.includes('msedge'))
42
+ return 'Microsoft Edge';
43
+ if (id.includes('chrome'))
44
+ return 'Google Chrome';
45
+ if (id.includes('firefox'))
46
+ return 'Firefox';
47
+ if (id.includes('vlc'))
48
+ return 'VLC';
49
+ if (id.includes('groove'))
50
+ return 'Groove Music';
51
+ if (id.includes('mediaplay'))
52
+ return 'Windows Media Player';
53
+ if (id.includes('apple'))
54
+ return 'Apple Music';
55
+ return aumid;
56
+ }
57
+ /**
58
+ * Build the PowerShell snippet. Enumerates every session via
59
+ * `GetSessions()`, marks the current one (the OS-routed-keypress
60
+ * target), and returns a JSON array. Each session's media properties
61
+ * are awaited individually — TryGetMediaPropertiesAsync can return
62
+ * null on transient state (track-skip mid-call) which we surface as
63
+ * empty fields rather than failing the whole enumeration.
64
+ */
65
+ function buildPs() {
66
+ return `
67
+ ${(0, _psHelpers_1.winRtAwaitPreamble)()}
68
+ $mgType = [Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control,ContentType=WindowsRuntime]
69
+ $pType = [Windows.Media.Control.GlobalSystemMediaTransportControlsSessionMediaProperties,Windows.Media.Control,ContentType=WindowsRuntime]
70
+ $mgr = Await ($mgType::RequestAsync()) $mgType
71
+ $current = $mgr.GetCurrentSession()
72
+ $currentId = if ($current) { $current.SourceAppUserModelId } else { '' }
73
+ $sessions = $mgr.GetSessions()
74
+ $out = @()
75
+ foreach ($s in $sessions) {
76
+ $p = $null
77
+ try { $p = Await ($s.TryGetMediaPropertiesAsync()) $pType } catch { $p = $null }
78
+ $pb = $s.GetPlaybackInfo()
79
+ $row = @{
80
+ appUserModelId = $s.SourceAppUserModelId
81
+ isCurrent = ($s.SourceAppUserModelId -eq $currentId)
82
+ playbackStatus = $pb.PlaybackStatus.ToString()
83
+ title = if ($p) { $p.Title } else { $null }
84
+ artist = if ($p) { $p.Artist } else { $null }
85
+ album = if ($p) { $p.AlbumTitle } else { $null }
86
+ }
87
+ $out += $row
88
+ }
89
+ if ($out.Count -eq 0) {
90
+ '[]'
91
+ } else {
92
+ $out | ConvertTo-Json -Compress -Depth 3
93
+ }
94
+ `.trim();
95
+ }
96
+ exports.mediaSessionsTool = {
97
+ schema: {
98
+ name: 'media_sessions',
99
+ description: 'List active Windows MEDIA PLAYBACK sessions (audio/video apps — ' +
100
+ 'Spotify, YouTube in browser, VLC, etc.). NOT for past conversation ' +
101
+ 'history — call `session_search` for chat-message search or ' +
102
+ '`recall_session` for past-session topic recall. One entry per app, ' +
103
+ 'including which one is the OS-routed target for global media keys. ' +
104
+ 'Use this BEFORE `media_transport` when you need to pick a specific ' +
105
+ 'app rather than blindly toggling the current session. Distinct from ' +
106
+ '`now_playing` which returns only the single current session. ' +
107
+ 'Windows-only in v4.1.4.',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: {},
111
+ },
112
+ },
113
+ category: 'read',
114
+ mutates: false,
115
+ toolset: 'system',
116
+ async execute(_args, _ctx) {
117
+ if (!(0, _psHelpers_1.isWindows)()) {
118
+ return (0, _psHelpers_1.windowsOnlyError)('media_sessions', {
119
+ canStill: [
120
+ 'Call `now_playing` if a Spotify Web API skill exposes that surface',
121
+ 'Use `os_process_list` with a media-app filter (spotify, vlc, chrome) for coarse presence detection',
122
+ '`shell_exec` with `playerctl --list-all` on Linux to enumerate MPRIS clients',
123
+ ],
124
+ cannotReliably: [
125
+ 'OS-level enumeration of every media-bus-registered app',
126
+ 'Distinguishing the OS-routed "current" session from inactive ones',
127
+ ],
128
+ fix: 'Run Aiden on Windows for GSMTC enumeration, or wrap your platform\'s ' +
129
+ 'native media-control bus (MPRIS / NowPlaying) in a skill.',
130
+ });
131
+ }
132
+ try {
133
+ const { stdout } = await (0, _psHelpers_1.runPowerShell)(buildPs(), { timeoutMs: 8000 });
134
+ const trimmed = stdout.trim();
135
+ if (trimmed.length === 0 || trimmed === '[]') {
136
+ return { success: true, sessions: [], count: 0 };
137
+ }
138
+ const parsed = JSON.parse(trimmed);
139
+ // ConvertTo-Json emits an object (single result) or array (multiple).
140
+ // Normalise to array, then attach friendlyApp.
141
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
142
+ const sessions = rows.map((row) => ({
143
+ appUserModelId: String(row.appUserModelId ?? ''),
144
+ friendlyApp: friendlyAppName(row.appUserModelId),
145
+ isCurrent: row.isCurrent === true,
146
+ playbackStatus: String(row.playbackStatus ?? 'Unknown'),
147
+ title: typeof row.title === 'string' ? row.title : undefined,
148
+ artist: typeof row.artist === 'string' ? row.artist : undefined,
149
+ album: typeof row.album === 'string' ? row.album : undefined,
150
+ }));
151
+ return { success: true, sessions, count: sessions.length };
152
+ }
153
+ catch (e) {
154
+ return {
155
+ success: false,
156
+ error: e instanceof Error ? e.message : String(e),
157
+ };
158
+ }
159
+ },
160
+ };
161
+ // Re-export the friendly-app mapper so mediaTransport can use the same
162
+ // normalization for target-string matching.
163
+ exports.__friendlyAppName = friendlyAppName;