aiden-runtime 4.1.1 → 4.1.2

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 (55) hide show
  1. package/README.md +78 -26
  2. package/dist/cli/v4/aidenCLI.js +159 -9
  3. package/dist/cli/v4/callbacks.js +5 -2
  4. package/dist/cli/v4/chatSession.js +525 -15
  5. package/dist/cli/v4/commands/auth.js +6 -3
  6. package/dist/cli/v4/commands/help.js +4 -0
  7. package/dist/cli/v4/commands/index.js +10 -1
  8. package/dist/cli/v4/commands/reloadSoul.js +37 -0
  9. package/dist/cli/v4/commands/update.js +102 -0
  10. package/dist/cli/v4/defaultSoul.js +68 -2
  11. package/dist/cli/v4/display.js +28 -10
  12. package/dist/cli/v4/doctor.js +112 -0
  13. package/dist/cli/v4/doctorLiveness.js +65 -10
  14. package/dist/cli/v4/promotionPrompt.js +202 -0
  15. package/dist/cli/v4/providerBootSelector.js +144 -0
  16. package/dist/cli/v4/sessionSummaryGate.js +66 -0
  17. package/dist/cli/v4/toolPreview.js +139 -0
  18. package/dist/core/v4/aidenAgent.js +91 -29
  19. package/dist/core/v4/capabilities.js +89 -0
  20. package/dist/core/v4/contextCompressor.js +25 -8
  21. package/dist/core/v4/distillationIndex.js +167 -0
  22. package/dist/core/v4/distillationStore.js +98 -0
  23. package/dist/core/v4/logger/logger.js +40 -9
  24. package/dist/core/v4/promotionCandidates.js +234 -0
  25. package/dist/core/v4/promptBuilder.js +145 -1
  26. package/dist/core/v4/sessionDistiller.js +405 -0
  27. package/dist/core/v4/skillMining/skillMiner.js +43 -6
  28. package/dist/core/v4/skillOutcomeTracker.js +323 -0
  29. package/dist/core/v4/subsystemHealth.js +143 -0
  30. package/dist/core/v4/update/executeInstall.js +233 -0
  31. package/dist/core/version.js +1 -1
  32. package/dist/moat/memoryGuard.js +111 -0
  33. package/dist/moat/skillTeacher.js +14 -5
  34. package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
  35. package/dist/providers/v4/errors.js +20 -4
  36. package/dist/providers/v4/modelDefaults.js +65 -0
  37. package/dist/providers/v4/registry.js +9 -2
  38. package/dist/providers/v4/runtimeResolver.js +6 -0
  39. package/dist/tools/v4/index.js +57 -1
  40. package/dist/tools/v4/memory/memoryRemove.js +57 -2
  41. package/dist/tools/v4/memory/sessionSummary.js +151 -0
  42. package/dist/tools/v4/sessions/recallSession.js +163 -0
  43. package/dist/tools/v4/sessions/sessionSearch.js +5 -1
  44. package/dist/tools/v4/system/_psHelpers.js +55 -0
  45. package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
  46. package/dist/tools/v4/system/appClose.js +79 -0
  47. package/dist/tools/v4/system/appLaunch.js +92 -0
  48. package/dist/tools/v4/system/clipboardRead.js +54 -0
  49. package/dist/tools/v4/system/clipboardWrite.js +84 -0
  50. package/dist/tools/v4/system/mediaKey.js +78 -0
  51. package/dist/tools/v4/system/osProcessList.js +99 -0
  52. package/dist/tools/v4/system/screenshot.js +106 -0
  53. package/dist/tools/v4/system/volumeSet.js +157 -0
  54. package/package.json +4 -1
  55. package/skills/system_control.md +135 -69
@@ -0,0 +1,151 @@
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/memory/sessionSummary.ts — Phase v4.1.2 alive-core.
10
+ *
11
+ * `session_summary` — append a five-bullet summary of the current
12
+ * session to MEMORY.md under a `## Recent sessions` section.
13
+ *
14
+ * Why this exists: until v4.1.2, "what did we work on last session?"
15
+ * was unanswerable — MEMORY.md held durable facts but no rolling
16
+ * conversation log. After this tool runs at /quit (or on demand), the
17
+ * NEXT session's PromptBuilder injects MEMORY.md as a slot, and the
18
+ * model can read the summary back as ambient context.
19
+ *
20
+ * Design:
21
+ *
22
+ * - The model is responsible for generating the five bullets. It
23
+ * already has the full conversation context in its message
24
+ * history, so this tool's job is *persistence* — not LLM dispatch.
25
+ * This avoids threading the AuxiliaryClient into ToolContext just
26
+ * for one tool and keeps the verify-on-disk contract clean.
27
+ *
28
+ * - Section rotation: append the new entry at the top of the
29
+ * section (most-recent-first), keep the most recent 10, drop the
30
+ * rest. Bound the size so MEMORY.md doesn't grow indefinitely.
31
+ *
32
+ * - Write goes through `MemoryGuard.replaceSection`, which preserves
33
+ * the standard `verified: true` contract that
34
+ * HonestyEnforcement relies on.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.sessionSummaryTool = void 0;
38
+ const RECENT_SESSIONS_HEADER = '## Recent sessions';
39
+ const MAX_RECENT_ENTRIES = 10;
40
+ /**
41
+ * Render one summary entry: timestamp header + bullets. Trims and
42
+ * normalises so two adjacent entries don't collide on whitespace.
43
+ */
44
+ function formatEntry(bullets, when) {
45
+ const stamp = when.toISOString().replace(/\.\d+Z$/, 'Z'); // second precision
46
+ const cleaned = bullets
47
+ .map((b) => b.trim())
48
+ .filter((b) => b.length > 0)
49
+ .map((b) => (b.startsWith('-') ? b : `- ${b}`));
50
+ return [`### ${stamp}`, ...cleaned].join('\n');
51
+ }
52
+ /**
53
+ * Split an existing Recent-sessions body into entries (each headed by
54
+ * a `### ` timestamp line). The most-recent entry is index 0.
55
+ */
56
+ function parseEntries(body) {
57
+ const trimmed = body.trim();
58
+ if (!trimmed)
59
+ return [];
60
+ // Split on `### ` at line start; first chunk is empty when the body
61
+ // starts with the marker, which we filter out.
62
+ const parts = trimmed
63
+ .split(/^### /m)
64
+ .map((p) => p.trim())
65
+ .filter((p) => p.length > 0)
66
+ .map((p) => `### ${p}`);
67
+ return parts;
68
+ }
69
+ exports.sessionSummaryTool = {
70
+ schema: {
71
+ name: 'session_summary',
72
+ description: 'Append a five-bullet summary of the current session to MEMORY.md ' +
73
+ '(under "## Recent sessions"). The next session will see it as ambient ' +
74
+ 'context. Call this at the end of a meaningful session, or right before ' +
75
+ 'the user types /quit. You craft the bullets — be concise and concrete: ' +
76
+ 'what we worked on, decisions made, files changed, problems solved, open items.',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ bullets: {
81
+ type: 'array',
82
+ description: 'Exactly five concise bullets (3-15 words each) summarising the session. ' +
83
+ 'Focus on what will be useful for the next session.',
84
+ items: {
85
+ type: 'string',
86
+ description: 'One bullet. Plain prose; leading "- " optional (added if missing).',
87
+ },
88
+ },
89
+ trigger: {
90
+ type: 'string',
91
+ enum: ['manual', 'auto-quit'],
92
+ description: 'Diagnostic only — whether the model invoked this directly ' +
93
+ '("manual") or the REPL auto-triggered it on /quit ("auto-quit"). ' +
94
+ 'Defaults to "manual" when omitted.',
95
+ },
96
+ },
97
+ required: ['bullets'],
98
+ },
99
+ },
100
+ category: 'write',
101
+ mutates: true,
102
+ toolset: 'memory',
103
+ async execute(args, ctx) {
104
+ if (!ctx.memoryGuard) {
105
+ return { success: false, error: 'memory guard not configured' };
106
+ }
107
+ if (!ctx.memory) {
108
+ return { success: false, error: 'memory manager not configured' };
109
+ }
110
+ const rawBullets = Array.isArray(args.bullets) ? args.bullets : [];
111
+ const bullets = rawBullets
112
+ .filter((b) => typeof b === 'string')
113
+ .map((b) => b.trim())
114
+ .filter((b) => b.length > 0);
115
+ if (bullets.length === 0) {
116
+ return {
117
+ success: false,
118
+ error: 'session_summary requires at least one non-empty bullet',
119
+ };
120
+ }
121
+ const now = new Date();
122
+ const newEntry = formatEntry(bullets, now);
123
+ // Read current MEMORY.md to find existing Recent-sessions body.
124
+ const snap = await ctx.memory.loadSnapshot();
125
+ const memoryMd = snap.memoryMd ?? '';
126
+ // Pull existing section body (if any). The header consumes its
127
+ // line; the capture group grabs everything until the next h2 or
128
+ // EOF. Whitespace-only bodies are captured as empty after trim.
129
+ const headerEscaped = RECENT_SESSIONS_HEADER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
130
+ // Note: NO `m` flag — we want `$` to mean end-of-string, not
131
+ // end-of-line. With `m`, the lookahead `$` matches before every
132
+ // newline and we capture only the first body line instead of the
133
+ // whole section.
134
+ const sectionRe = new RegExp(`${headerEscaped}[^\\n]*\\n([\\s\\S]*?)(?=\\n## |$)`);
135
+ const match = memoryMd.match(sectionRe);
136
+ const existingBody = match ? (match[1] ?? '').trim() : '';
137
+ const existingEntries = parseEntries(existingBody);
138
+ // Most-recent-first ordering, capped to 10.
139
+ const combined = [newEntry, ...existingEntries].slice(0, MAX_RECENT_ENTRIES);
140
+ const newBody = combined.join('\n\n');
141
+ const result = await ctx.memoryGuard.replaceSection('memory', RECENT_SESSIONS_HEADER, newBody);
142
+ return {
143
+ success: result.ok,
144
+ verified: result.verified,
145
+ error: result.ok ? undefined : result.reason,
146
+ entries: combined.length,
147
+ trigger: args.trigger ?? 'manual',
148
+ timestamp: now.toISOString(),
149
+ };
150
+ },
151
+ };
@@ -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/sessions/recallSession.ts — Phase v4.1.2-memory-C.
10
+ *
11
+ * `recall_session` — return ranked SessionDistillation summaries for
12
+ * past sessions matching the user's query (or just the most recent N
13
+ * when no query is supplied).
14
+ *
15
+ * Coexists with `session_search`:
16
+ * - session_search → FTS5 over message TEXT in SessionStore.
17
+ * Returns per-message snippets. Use when the user wants the exact
18
+ * words of a past message.
19
+ * - recall_session → ranked DISTILLATIONS by TOPIC. Returns
20
+ * structured per-session summaries (decisions, open items, files
21
+ * touched). Use when the user wants context on what HAPPENED in
22
+ * past sessions.
23
+ *
24
+ * Index strategy: scan-all. Reads every distillation JSON from
25
+ * `<paths.root>/distillations/` per query. Expected file count is
26
+ * <1000 per user; sub-100ms at that scale. When telemetry shows
27
+ * latency >500ms, the escalation path is direct migration to SQLite
28
+ * FTS5 — JSON-index intermediate is intentionally skipped.
29
+ */
30
+ var __importDefault = (this && this.__importDefault) || function (mod) {
31
+ return (mod && mod.__esModule) ? mod : { "default": mod };
32
+ };
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.__testFs = exports.recallSessionTool = void 0;
35
+ exports.getDistillationsDir = getDistillationsDir;
36
+ const node_path_1 = __importDefault(require("node:path"));
37
+ const node_fs_1 = require("node:fs");
38
+ const distillationStore_1 = require("../../../core/v4/distillationStore");
39
+ const distillationIndex_1 = require("../../../core/v4/distillationIndex");
40
+ const DEFAULT_LIMIT = 5;
41
+ const MAX_LIMIT = 25;
42
+ exports.recallSessionTool = {
43
+ schema: {
44
+ name: 'recall_session',
45
+ description: 'Recall past SESSIONS by topic. Returns ranked summaries — ' +
46
+ 'decisions made, files touched, open items, tool usage — from ' +
47
+ 'previously persisted session distillations. ' +
48
+ 'For the EXACT WORDS of a past message, call `session_search` ' +
49
+ 'instead (FTS5 over message text). For "what happened" / "what ' +
50
+ 'did we work on" / "what was unfinished", use this tool.',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ query: {
55
+ type: 'string',
56
+ description: 'Optional keyword filter. Case-insensitive substring match ' +
57
+ 'across keywords, bullets, decisions, open_items, and ' +
58
+ 'tool names. Omit to get the most recent sessions.',
59
+ },
60
+ limit: {
61
+ type: 'number',
62
+ description: `Maximum number of matches to return. Default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}.`,
63
+ },
64
+ days: {
65
+ type: 'number',
66
+ description: 'Optional recency window in days. Drops distillations older ' +
67
+ 'than this before ranking. Omit for no time filter.',
68
+ },
69
+ include_full: {
70
+ type: 'boolean',
71
+ description: 'When true, each match also carries tools_used + keywords ' +
72
+ '(useful when the agent needs granular tool history). ' +
73
+ 'Default false to keep responses compact.',
74
+ },
75
+ },
76
+ },
77
+ },
78
+ category: 'read',
79
+ mutates: false,
80
+ toolset: 'sessions',
81
+ async execute(args, ctx) {
82
+ if (!ctx.paths?.root) {
83
+ return {
84
+ success: false,
85
+ error: 'recall_session requires resolved aiden paths',
86
+ };
87
+ }
88
+ const dir = node_path_1.default.join(ctx.paths.root, 'distillations');
89
+ // Read everything off disk first. Each failure (malformed JSON,
90
+ // EACCES on individual files) is skipped silently; the diagnostic
91
+ // for "files exist but couldn't be read" is the gap between
92
+ // scanned (id count) and dists.length (parse-success count).
93
+ let ids;
94
+ try {
95
+ ids = await (0, distillationStore_1.listDistillationIds)(dir);
96
+ }
97
+ catch (err) {
98
+ const code = err.code;
99
+ if (code === 'ENOENT') {
100
+ // No distillations directory yet — first-run case is
101
+ // success with zero matches, NOT a failure.
102
+ return {
103
+ success: true,
104
+ query: typeof args.query === 'string' ? args.query : undefined,
105
+ matches: [],
106
+ total_found: 0,
107
+ scanned: 0,
108
+ };
109
+ }
110
+ return {
111
+ success: false,
112
+ error: `Failed to enumerate distillations: ${err.message}`,
113
+ };
114
+ }
115
+ const dists = [];
116
+ for (const id of ids) {
117
+ try {
118
+ const d = await (0, distillationStore_1.readDistillation)(dir, id);
119
+ if (d)
120
+ dists.push(d);
121
+ }
122
+ catch {
123
+ // One bad file shouldn't prevent the agent from seeing the
124
+ // rest. The user can diagnose via `aiden doctor` (the file
125
+ // is still on disk).
126
+ }
127
+ }
128
+ const recallQuery = {
129
+ query: typeof args.query === 'string' ? args.query : undefined,
130
+ limit: typeof args.limit === 'number' ? args.limit : undefined,
131
+ days: typeof args.days === 'number' ? args.days : undefined,
132
+ include_full: args.include_full === true,
133
+ };
134
+ const ranked = (0, distillationIndex_1.rankDistillations)(dists, recallQuery);
135
+ return {
136
+ success: true,
137
+ query: recallQuery.query,
138
+ matches: ranked.matches,
139
+ total_found: ranked.total_found,
140
+ // scanned reflects files we attempted to read — useful diagnostic.
141
+ // If scanned > ranked.total_found AND dists.length < scanned, the
142
+ // delta is malformed files; the agent can suggest running aiden
143
+ // doctor to inspect.
144
+ scanned: ids.length,
145
+ };
146
+ // Note re: subsystem health — wire-up happens at the runtime
147
+ // construction layer (cli/v4/aidenCLI.ts) where the registry is
148
+ // built. The tool itself stays pure of registry-knowledge for
149
+ // testability; the registry caller decides whether to wrap the
150
+ // file-read errors in a tracker.
151
+ },
152
+ };
153
+ // Expose the directory path for runtime wire-up. Tools that want to
154
+ // register recall_session with the slice3 SubsystemHealthRegistry
155
+ // pass this helper to the tracker so they get health snapshots without
156
+ // hard-coding the path in two places.
157
+ function getDistillationsDir(rootDir) {
158
+ return node_path_1.default.join(rootDir, 'distillations');
159
+ }
160
+ // Re-export read-only fs surface used by tests under controlled
161
+ // fixtures. Production code never imports from here; the tool calls
162
+ // fs directly.
163
+ exports.__testFs = node_fs_1.promises;
@@ -22,7 +22,11 @@ const MAX_LIMIT = 50;
22
22
  exports.sessionSearchTool = {
23
23
  schema: {
24
24
  name: 'session_search',
25
- description: 'Search past conversation sessions by keyword (FTS5 full-text). Returns matching message snippets with the session id and timestamp. Use to recall something the user said in an earlier conversation.',
25
+ description: 'Search past conversation MESSAGES by keyword (FTS5 full-text). ' +
26
+ 'Returns matching message SNIPPETS with the session id and timestamp. ' +
27
+ 'Use when you need the exact words a past message contained. ' +
28
+ 'For topic-level recall of what happened in past sessions (decisions, ' +
29
+ 'files touched, open items), call `recall_session` instead.',
26
30
  inputSchema: {
27
31
  type: 'object',
28
32
  properties: {
@@ -0,0 +1,55 @@
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/_psHelpers.ts — Phase v4.1.2-followup-3.
10
+ *
11
+ * Shared utilities for the computer-control tool family. Each tool
12
+ * (screenshot / os_process_list / media_key / volume_set / app_launch /
13
+ * app_close / clipboard_read / clipboard_write) gates on `win32` and
14
+ * shells out to PowerShell. The gate + exec boilerplate is identical
15
+ * across all eight tools — extracted here so the per-tool files stay
16
+ * focused on the one PowerShell snippet that matters.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.isWindows = exports.execAsync = void 0;
20
+ exports.windowsOnlyError = windowsOnlyError;
21
+ exports.runPowerShell = runPowerShell;
22
+ const node_child_process_1 = require("node:child_process");
23
+ const node_util_1 = require("node:util");
24
+ exports.execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
25
+ /**
26
+ * Standard "not supported on this platform" error payload. Surfaces a
27
+ * link the user can file an issue against rather than pretending the
28
+ * call quietly no-op'd.
29
+ */
30
+ function windowsOnlyError(toolName) {
31
+ return {
32
+ success: false,
33
+ error: `Tool '${toolName}' is Windows-only in v4.1.2. macOS/Linux ` +
34
+ `support tracked at github.com/taracodlabs/aiden — please file an ` +
35
+ `issue if needed. (Detected platform: ${process.platform})`,
36
+ };
37
+ }
38
+ /**
39
+ * Run a PowerShell snippet and return stdout. Defaults to a 15-second
40
+ * timeout — caller passes a different one when a slower operation
41
+ * (screenshot, app launch) is expected.
42
+ *
43
+ * Single source of truth for the `shell: 'powershell.exe'` invocation
44
+ * shape so future powershell-CLI / `pwsh` migration is one-line.
45
+ */
46
+ async function runPowerShell(script, options = {}) {
47
+ const opts = {
48
+ shell: 'powershell.exe',
49
+ timeout: options.timeoutMs ?? 15000,
50
+ maxBuffer: (options.maxBufferMb ?? 4) * 1024 * 1024,
51
+ };
52
+ return await (0, exports.execAsync)(script, opts);
53
+ }
54
+ const isWindows = () => process.platform === 'win32';
55
+ exports.isWindows = isWindows;
@@ -0,0 +1,162 @@
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/aidenSelfUpdate.ts — Phase v4.1.2-update.
10
+ *
11
+ * Natural-language entry point for self-update. When the user asks
12
+ * Aiden to update / install latest / upgrade itself, the model calls
13
+ * this tool. Routes to the same shared executor (`executeInstall`)
14
+ * that `/update install` uses — single source of truth for install
15
+ * behavior.
16
+ *
17
+ * Two-step confirmation contract (consent gate):
18
+ * 1. First call with `confirm: false` — returns status + a prompt
19
+ * asking the user to confirm. NEVER spawns.
20
+ * 2. Model surfaces the prompt to the user; waits for explicit
21
+ * agreement ("yes update", "go ahead", "do it").
22
+ * 3. Second call with `confirm: true` — only after explicit user
23
+ * agreement; spawns the install.
24
+ *
25
+ * The contract is enforced via tool DESCRIPTION (model-facing rule).
26
+ * Tool-side, we don't track "did the user actually consent" — that
27
+ * needs a runtime approval object (request_id + fresh-confirmation
28
+ * verification) which is a v4.2+ design. For v4.1.2 the description
29
+ * carries the rule and the tool trusts the model to follow it.
30
+ *
31
+ * Acceptable risk: failure mode of a misbehaving model is "user gets
32
+ * an unwanted install they have to /quit to apply" — not data loss.
33
+ * Phase D's promotion path is the more sensitive consent surface and
34
+ * is user-driven UI.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.aidenSelfUpdateTool = void 0;
38
+ const version_1 = require("../../../core/version");
39
+ const checkUpdate_1 = require("../../../core/v4/update/checkUpdate");
40
+ const executeInstall_1 = require("../../../core/v4/update/executeInstall");
41
+ exports.aidenSelfUpdateTool = {
42
+ schema: {
43
+ name: 'aiden_self_update',
44
+ description: 'Update Aiden to the latest version via npm install -g aiden-runtime@latest. ' +
45
+ 'TWO-STEP CONFIRMATION REQUIRED: first call with confirm:false to check status ' +
46
+ 'and surface to the user; only call with confirm:true AFTER the user explicitly ' +
47
+ 'agrees in their next message ("yes update", "go ahead", "do it"). NEVER call ' +
48
+ 'with confirm:true autonomously. ' +
49
+ 'Call this tool ONLY when the user explicitly asks Aiden to update / install ' +
50
+ 'latest / upgrade itself. Example user phrases that warrant a call: ' +
51
+ '"update yourself", "can you install the latest version?", "upgrade to the latest", ' +
52
+ '"self-update". DO NOT call when: user asks about update status without requesting ' +
53
+ 'action ("are there updates?") — for status queries, just answer from your context; ' +
54
+ 'user mentions updates of OTHER software ("update VSCode"); user has not explicitly ' +
55
+ 'asked Aiden to update itself.',
56
+ inputSchema: {
57
+ type: 'object',
58
+ properties: {
59
+ confirm: {
60
+ type: 'boolean',
61
+ description: 'False on first call (status check, no install). True on second call AFTER ' +
62
+ 'the user explicitly agreed to proceed in their last message.',
63
+ },
64
+ },
65
+ required: ['confirm'],
66
+ },
67
+ },
68
+ category: 'write',
69
+ mutates: true,
70
+ toolset: 'system',
71
+ async execute(args, ctx) {
72
+ if (!ctx.paths) {
73
+ return {
74
+ success: false,
75
+ error: 'aiden_self_update needs Aiden user-data paths — not configured in this context.',
76
+ };
77
+ }
78
+ const confirm = args.confirm === true;
79
+ // Status probe — bypass the 6h boot cache for user-initiated checks.
80
+ const status = await (0, checkUpdate_1.checkForUpdate)({
81
+ paths: ctx.paths,
82
+ installedVersion: version_1.VERSION,
83
+ cacheTtlMs: 0,
84
+ });
85
+ // ── First call: confirm:false → status + prompt. NEVER spawn. ─────
86
+ if (!confirm) {
87
+ if (status.latest === null) {
88
+ return {
89
+ success: true,
90
+ stage: 'status',
91
+ message: "Couldn't check for updates (registry unreachable). " +
92
+ 'Try again in a moment, or run `npm install -g aiden-runtime@latest` manually.',
93
+ installed: status.installed,
94
+ latest: null,
95
+ updateAvailable: false,
96
+ };
97
+ }
98
+ if (!status.updateAvailable) {
99
+ return {
100
+ success: true,
101
+ stage: 'status',
102
+ message: `You're on the latest version (v${status.installed}). Nothing to update.`,
103
+ installed: status.installed,
104
+ latest: status.latest,
105
+ updateAvailable: false,
106
+ };
107
+ }
108
+ return {
109
+ success: true,
110
+ stage: 'status',
111
+ message: `Update available: v${status.installed} → v${status.latest}. ` +
112
+ `Confirm by saying "yes update" or "go ahead". This runs ` +
113
+ `\`npm install -g aiden-runtime@latest\` and you'll need to ` +
114
+ `restart Aiden after.`,
115
+ installed: status.installed,
116
+ latest: status.latest,
117
+ updateAvailable: true,
118
+ };
119
+ }
120
+ // ── Second call: confirm:true → install. ────────────────────────
121
+ if (status.latest === null) {
122
+ return {
123
+ success: false,
124
+ stage: 'install',
125
+ error: "Can't install — registry unreachable. " +
126
+ 'Try again or run `npm install -g aiden-runtime@latest` manually.',
127
+ installed: status.installed,
128
+ };
129
+ }
130
+ if (!status.updateAvailable) {
131
+ return {
132
+ success: true,
133
+ stage: 'install',
134
+ message: `Already on v${status.installed}. Nothing to install.`,
135
+ installed: status.installed,
136
+ latest: status.latest,
137
+ updateAvailable: false,
138
+ };
139
+ }
140
+ const result = await (0, executeInstall_1.executeInstall)();
141
+ if (result.success) {
142
+ const v = result.installedVersion ?? status.latest;
143
+ return {
144
+ success: true,
145
+ stage: 'install',
146
+ message: `✓ aiden-runtime v${v} installed. Restart Aiden to apply: ` +
147
+ `type /quit then re-run \`aiden\`.`,
148
+ installed: status.installed,
149
+ installedVersion: v,
150
+ };
151
+ }
152
+ return {
153
+ success: false,
154
+ stage: 'install',
155
+ error: result.error ?? 'Install failed (no error message).',
156
+ installed: status.installed,
157
+ latestSeen: status.latest,
158
+ // Keep stdout/stderr off the model-visible response to avoid
159
+ // prompt bloat; user-actionable copy-paste is already in error.
160
+ };
161
+ },
162
+ };
@@ -0,0 +1,79 @@
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/appClose.ts — `app_close` tool.
10
+ *
11
+ * Close one or more Windows processes by process name. Accepts the
12
+ * bare name without `.exe` (matches `Stop-Process -Name` semantics).
13
+ * Returns the count of processes successfully terminated.
14
+ *
15
+ * The .exe stripper handles user input like "close notepad.exe" /
16
+ * "close notepad" identically — both resolve to the same call.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.appCloseTool = void 0;
20
+ const _psHelpers_1 = require("./_psHelpers");
21
+ function normalise(name) {
22
+ return name.trim().replace(/\.exe$/i, '');
23
+ }
24
+ function buildPs(processName, force) {
25
+ const safe = processName.replace(/'/g, "''");
26
+ const forceFlag = force ? '-Force' : '';
27
+ return [
28
+ `$procs = Get-Process -Name '${safe}' -ErrorAction SilentlyContinue;`,
29
+ `$count = ($procs | Measure-Object).Count;`,
30
+ `if ($count -gt 0) { $procs | Stop-Process ${forceFlag} -ErrorAction SilentlyContinue; }`,
31
+ `Write-Output ('closed:' + $count);`,
32
+ ].join(' ');
33
+ }
34
+ exports.appCloseTool = {
35
+ schema: {
36
+ name: 'app_close',
37
+ description: 'Close one or more Windows processes by name (with or without the .exe suffix). Matches all running instances of that name. Set `force: true` to skip the app\'s graceful-shutdown prompt. Windows-only in v4.1.2.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ app: {
42
+ type: 'string',
43
+ description: 'Process name (e.g. "notepad", "spotify"). The .exe suffix is stripped automatically. Matches ALL running instances of that name.',
44
+ },
45
+ force: {
46
+ type: 'boolean',
47
+ description: 'If true, terminate without giving the app a chance to save unsaved work. Default false (graceful close).',
48
+ },
49
+ },
50
+ required: ['app'],
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)('app_close');
59
+ const app = typeof args.app === 'string' ? normalise(args.app) : '';
60
+ if (!app) {
61
+ return { success: false, error: '`app` is required and must be non-empty.' };
62
+ }
63
+ const force = args.force === true;
64
+ try {
65
+ const { stdout } = await (0, _psHelpers_1.runPowerShell)(buildPs(app, force), {
66
+ timeoutMs: 10000,
67
+ });
68
+ const m = stdout.trim().match(/closed:(\d+)/);
69
+ const closed = m ? Number(m[1]) : 0;
70
+ return { success: true, app, closed, force };
71
+ }
72
+ catch (e) {
73
+ return {
74
+ success: false,
75
+ error: e instanceof Error ? e.message : String(e),
76
+ };
77
+ }
78
+ },
79
+ };