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,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.
|
|
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",
|