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.
- package/README.md +78 -26
- package/dist/cli/v4/aidenCLI.js +159 -9
- package/dist/cli/v4/callbacks.js +5 -2
- package/dist/cli/v4/chatSession.js +525 -15
- package/dist/cli/v4/commands/auth.js +6 -3
- package/dist/cli/v4/commands/help.js +4 -0
- package/dist/cli/v4/commands/index.js +10 -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.js +28 -10
- package/dist/cli/v4/doctor.js +112 -0
- package/dist/cli/v4/doctorLiveness.js +65 -10
- package/dist/cli/v4/promotionPrompt.js +202 -0
- package/dist/cli/v4/providerBootSelector.js +144 -0
- package/dist/cli/v4/sessionSummaryGate.js +66 -0
- package/dist/cli/v4/toolPreview.js +139 -0
- 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 +405 -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/update/executeInstall.js +233 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/memoryGuard.js +111 -0
- package/dist/moat/skillTeacher.js +14 -5
- package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
- package/dist/providers/v4/errors.js +20 -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 +57 -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 +163 -0
- package/dist/tools/v4/sessions/sessionSearch.js +5 -1
- package/dist/tools/v4/system/_psHelpers.js +55 -0
- package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
- package/dist/tools/v4/system/appClose.js +79 -0
- package/dist/tools/v4/system/appLaunch.js +92 -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 +78 -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 +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
|
|
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
|
+
};
|