aiden-runtime 3.18.0 → 3.19.4
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 +153 -24
- package/config/devos.config.backup.json +225 -0
- package/config/devos.config.json +69 -33
- package/config/hardware.json +2 -2
- package/dist/api/server.js +126 -83
- package/dist/cli/commandCatalog.js +344 -0
- package/dist/core/actionVerbDetector.js +65 -0
- package/dist/core/agentLoop.js +279 -112
- package/dist/core/aidenPersonality.js +11 -36
- package/dist/core/auxiliaryClient.js +1 -0
- package/dist/core/computerControl.js +35 -17
- package/dist/core/contextHandoff.js +39 -0
- package/dist/core/diagnosticError.js +20 -0
- package/dist/core/fastPathExpansion.js +7 -0
- package/dist/core/memoryIds.js +16 -0
- package/dist/core/pluginLoader.js +8 -5
- package/dist/core/protectedContext.js +112 -0
- package/dist/core/skillTeacher.js +63 -0
- package/dist/core/slashAsTool.js +37 -0
- package/dist/core/toolRegistry.js +825 -54
- package/dist/core/tools/nowPlaying.js +66 -0
- package/dist/core/version.js +1 -1
- package/dist/providers/index.js +12 -0
- package/dist/providers/mistral.js +121 -0
- package/dist/providers/router.js +4 -2
- package/dist-bundle/cli.js +48052 -46832
- package/dist-bundle/index.js +37216 -22645
- package/package.json +9 -2
- package/scripts/uninstall.ps1 +147 -0
|
@@ -3,39 +3,19 @@
|
|
|
3
3
|
// DevOS — Autonomous AI Execution System
|
|
4
4
|
// Copyright (c) 2026 Shiva Deore. All rights reserved.
|
|
5
5
|
// ============================================================
|
|
6
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
-
};
|
|
9
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.AIDEN_RESPONDER_SYSTEM = exports.
|
|
7
|
+
exports.AIDEN_RESPONDER_SYSTEM = exports.AIDEN_IDENTITY = exports.AIDEN_REAL_TOOLS = void 0;
|
|
8
|
+
exports.getLiveSoul = getLiveSoul;
|
|
11
9
|
// core/aidenPersonality.ts — Unified Aiden identity + system prompts
|
|
12
10
|
//
|
|
13
11
|
// Single source of truth for personality, tone, and capability declarations.
|
|
14
12
|
// All system prompts across agentLoop.ts and server.ts reference this.
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const SOUL_WORKSPACE_ROOT = process.env.AIDEN_USER_DATA || process.cwd();
|
|
21
|
-
// ── Load SOUL.md at startup ────────────────────────────────────
|
|
22
|
-
function loadSoul() {
|
|
23
|
-
try {
|
|
24
|
-
// Primary: workspace/SOUL.md (installed Electron app location)
|
|
25
|
-
const wsPath = path_1.default.join(SOUL_WORKSPACE_ROOT, 'workspace', 'SOUL.md');
|
|
26
|
-
if (fs_1.default.existsSync(wsPath)) {
|
|
27
|
-
return fs_1.default.readFileSync(wsPath, 'utf-8');
|
|
28
|
-
}
|
|
29
|
-
// Fallback: SOUL.md at root (direct npm run dev, cwd = project root)
|
|
30
|
-
const rootPath = path_1.default.join(process.cwd(), 'SOUL.md');
|
|
31
|
-
if (fs_1.default.existsSync(rootPath)) {
|
|
32
|
-
return fs_1.default.readFileSync(rootPath, 'utf-8');
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch { }
|
|
36
|
-
return '';
|
|
13
|
+
const protectedContext_1 = require("./protectedContext");
|
|
14
|
+
// ── Live SOUL.md accessor — reads from hash-cached ProtectedContextManager.
|
|
15
|
+
// Never frozen: always reflects the current on-disk content.
|
|
16
|
+
function getLiveSoul() {
|
|
17
|
+
return protectedContext_1.protectedContextManager.getProtectedContext().soul;
|
|
37
18
|
}
|
|
38
|
-
exports.SOUL = loadSoul();
|
|
39
19
|
// ── Tool list ─────────────────────────────────────────────────
|
|
40
20
|
// Keep in sync with TOOLS in toolRegistry.ts
|
|
41
21
|
exports.AIDEN_REAL_TOOLS = `
|
|
@@ -142,16 +122,10 @@ Good: "I have 48 built-in tools: web_search, file_write, run_python... [lists re
|
|
|
142
122
|
- You're not the user's voice — be careful in
|
|
143
123
|
group chats and external communications.
|
|
144
124
|
`.trim();
|
|
145
|
-
// ── Stream chat system prompt (no tools available) ────────────
|
|
146
|
-
exports.AIDEN_STREAM_SYSTEM = `${exports.SOUL ? exports.SOUL + '\n\n' : ''}${exports.AIDEN_IDENTITY}
|
|
147
|
-
|
|
148
|
-
You are in direct chat mode — no tools are running right now.
|
|
149
|
-
Answer from your knowledge. Be concise and direct.
|
|
150
|
-
If the question needs real-time data (weather, stocks, news) — tell the user to
|
|
151
|
-
rephrase as a task (e.g. "search for..." or "get me the latest...") and you will
|
|
152
|
-
execute the right tool automatically.`;
|
|
153
125
|
// ── Responder system prompt (post-execution) ──────────────────
|
|
154
|
-
const AIDEN_RESPONDER_SYSTEM = (userName, date) =>
|
|
126
|
+
const AIDEN_RESPONDER_SYSTEM = (userName, date) => {
|
|
127
|
+
const soul = getLiveSoul();
|
|
128
|
+
return `${soul ? soul + '\n\n' : ''}${exports.AIDEN_IDENTITY}
|
|
155
129
|
|
|
156
130
|
You just executed real tools and have their actual output.
|
|
157
131
|
Current date: ${date}
|
|
@@ -163,4 +137,5 @@ REPORT RESULTS:
|
|
|
163
137
|
- If multiple steps ran: summarize the outcome, not each individual step
|
|
164
138
|
- If a step failed: acknowledge it clearly and explain what worked
|
|
165
139
|
- For research tasks: analyze and synthesize — don't just re-paste the raw data`;
|
|
140
|
+
};
|
|
166
141
|
exports.AIDEN_RESPONDER_SYSTEM = AIDEN_RESPONDER_SYSTEM;
|
|
@@ -23,6 +23,7 @@ const AUX_ENDPOINTS = {
|
|
|
23
23
|
nvidia: 'https://integrate.api.nvidia.com/v1/chat/completions',
|
|
24
24
|
github: 'https://models.inference.ai.azure.com/chat/completions',
|
|
25
25
|
boa: 'https://api.boa.ai/v1/chat/completions',
|
|
26
|
+
mistral: 'https://api.mistral.ai/v1/chat/completions',
|
|
26
27
|
};
|
|
27
28
|
const DEFAULT_AUX_CONFIG = {
|
|
28
29
|
preferLocal: true,
|
|
@@ -11,6 +11,7 @@ exports.moveMouse = moveMouse;
|
|
|
11
11
|
exports.clickMouse = clickMouse;
|
|
12
12
|
exports.typeText = typeText;
|
|
13
13
|
exports.pressKey = pressKey;
|
|
14
|
+
exports.resolveScreenshotPath = resolveScreenshotPath;
|
|
14
15
|
exports.takeScreenshot = takeScreenshot;
|
|
15
16
|
exports.readScreen = readScreen;
|
|
16
17
|
exports.openBrowser = openBrowser;
|
|
@@ -161,10 +162,25 @@ $wsh.SendKeys('${mapped}')
|
|
|
161
162
|
return `Pressed: ${key}`;
|
|
162
163
|
}
|
|
163
164
|
// ── SCREENSHOT ─────────────────────────────────────────────────
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Pure path resolver — exported for unit tests.
|
|
167
|
+
* Returns the absolute path where the screenshot should be saved.
|
|
168
|
+
* Throws if outputPath is provided but is not absolute.
|
|
169
|
+
*/
|
|
170
|
+
function resolveScreenshotPath(outputPath) {
|
|
171
|
+
if (outputPath !== undefined && outputPath !== '') {
|
|
172
|
+
const isAbsolute = /^[A-Za-z]:[/\\]/.test(outputPath) || outputPath.startsWith('/');
|
|
173
|
+
if (!isAbsolute)
|
|
174
|
+
throw new Error(`outputPath must be an absolute path, got: ${outputPath}`);
|
|
175
|
+
return outputPath;
|
|
176
|
+
}
|
|
177
|
+
return path_1.default.join(SCREENSHOTS_DIR, `screenshot_${Date.now()}.png`);
|
|
178
|
+
}
|
|
179
|
+
async function takeScreenshot(opts) {
|
|
180
|
+
const filepath = resolveScreenshotPath(opts?.outputPath);
|
|
181
|
+
const useDefault = !opts?.outputPath;
|
|
182
|
+
// C3b: No backslash escaping — PS single-quoted strings are literal, \\ would be passed verbatim to .NET
|
|
183
|
+
const escaped = filepath;
|
|
168
184
|
await psFile(`
|
|
169
185
|
Add-Type -AssemblyName System.Windows.Forms
|
|
170
186
|
Add-Type -AssemblyName System.Drawing
|
|
@@ -176,21 +192,23 @@ $bitmap.Save('${escaped}')
|
|
|
176
192
|
$graphics.Dispose()
|
|
177
193
|
$bitmap.Dispose()
|
|
178
194
|
`);
|
|
179
|
-
// Trim old screenshots — keep only last 10
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
// Trim old screenshots — keep only last 10 (default dir only)
|
|
196
|
+
if (useDefault) {
|
|
197
|
+
try {
|
|
198
|
+
const files = fs_1.default.readdirSync(SCREENSHOTS_DIR)
|
|
199
|
+
.filter(f => f.endsWith('.png'))
|
|
200
|
+
.sort();
|
|
201
|
+
if (files.length > 10) {
|
|
202
|
+
files.slice(0, files.length - 10).forEach(f => {
|
|
203
|
+
try {
|
|
204
|
+
fs_1.default.unlinkSync(path_1.default.join(SCREENSHOTS_DIR, f));
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
});
|
|
208
|
+
}
|
|
191
209
|
}
|
|
210
|
+
catch { }
|
|
192
211
|
}
|
|
193
|
-
catch { }
|
|
194
212
|
if (fs_1.default.existsSync(filepath))
|
|
195
213
|
return filepath;
|
|
196
214
|
throw new Error('Screenshot failed — file not created');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildProtectedContextBlock = buildProtectedContextBlock;
|
|
4
|
+
function buildProtectedContextBlock(ctx, previousHash, sessionId) {
|
|
5
|
+
const sections = [];
|
|
6
|
+
const soulUnchanged = previousHash !== undefined && ctx.hash === previousHash;
|
|
7
|
+
if (soulUnchanged) {
|
|
8
|
+
sections.push(`Identity (SOUL.md): [unchanged from previous turn, hash: ${ctx.hash}]`);
|
|
9
|
+
}
|
|
10
|
+
else if (ctx.soul) {
|
|
11
|
+
sections.push(`Identity (SOUL.md):\n${ctx.soul}`);
|
|
12
|
+
}
|
|
13
|
+
if (ctx.user)
|
|
14
|
+
sections.push(`User (USER.md):\n${ctx.user}`);
|
|
15
|
+
if (ctx.goals)
|
|
16
|
+
sections.push(`Active Goals (GOALS.md):\n${ctx.goals}`);
|
|
17
|
+
if (ctx.standingOrders)
|
|
18
|
+
sections.push(`Standing Orders (STANDING_ORDERS.md):\n${ctx.standingOrders}`);
|
|
19
|
+
if (ctx.lessons)
|
|
20
|
+
sections.push(`Lessons Learned (LESSONS.md):\n${ctx.lessons}`);
|
|
21
|
+
if (sections.length === 0)
|
|
22
|
+
return '';
|
|
23
|
+
const block = [
|
|
24
|
+
'[PROTECTED CONTEXT — AUTHORITATIVE, REFRESHED THIS TURN]',
|
|
25
|
+
'',
|
|
26
|
+
sections.join('\n\n'),
|
|
27
|
+
'',
|
|
28
|
+
'[END PROTECTED CONTEXT]',
|
|
29
|
+
].join('\n');
|
|
30
|
+
// C4: structured per-turn protected-context metrics (always-on, stderr only)
|
|
31
|
+
const sidShort = sessionId ? sessionId.slice(0, 8) : 'nosess ';
|
|
32
|
+
const soulMode = soulUnchanged ? 'REF' : (ctx.soul ? 'FULL' : 'EMPTY');
|
|
33
|
+
const tokens = Math.round(block.length / 4);
|
|
34
|
+
const hashShort = ctx.hash.slice(0, 8);
|
|
35
|
+
const files = ctx.changedFiles.length > 0 ? ctx.changedFiles.join(',') : 'none';
|
|
36
|
+
process.stderr.write(`[ProtectedCtx] sessionId=${sidShort} cacheHit=${soulUnchanged}` +
|
|
37
|
+
` soul=${soulMode} tokens=${tokens} hash=${hashShort} files=${files}\n`);
|
|
38
|
+
return block;
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// DevOS — Autonomous AI Execution System
|
|
4
|
+
// Copyright (c) 2026 Shiva Deore. All rights reserved.
|
|
5
|
+
// ============================================================
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.buildDiagnostic = buildDiagnostic;
|
|
8
|
+
function buildDiagnostic(info) {
|
|
9
|
+
const lines = [
|
|
10
|
+
`Couldn't ${info.tool}: ${info.error}`,
|
|
11
|
+
`Provider: ${info.provider ?? 'unknown'}, retries: ${info.retries}`,
|
|
12
|
+
];
|
|
13
|
+
if (info.fallbackTried) {
|
|
14
|
+
lines.push(`Fell back to ${info.fallbackTried.name} → ${info.fallbackTried.outcome}`);
|
|
15
|
+
}
|
|
16
|
+
if (info.suggestion) {
|
|
17
|
+
lines.push(`Suggestion: ${info.suggestion}`);
|
|
18
|
+
}
|
|
19
|
+
return lines.join('\n');
|
|
20
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.matchFastPath = matchFastPath;
|
|
8
8
|
// core/fastPathExpansion.ts — Phase 5 of Prompt 10.
|
|
9
|
+
// v3.19 C3: action intents are never fast-pathed (they need the planner).
|
|
9
10
|
//
|
|
10
11
|
// Classifies messages that can be answered directly by streamChat without
|
|
11
12
|
// going through the planner, cutting 2–8s of planner latency for ~60% of
|
|
@@ -17,6 +18,7 @@ exports.matchFastPath = matchFastPath;
|
|
|
17
18
|
// - Asks for short creative output (joke, quote, haiku)
|
|
18
19
|
// - Does NOT contain signals for file I/O, code execution, web search,
|
|
19
20
|
// git, screen control, or multi-step planning keywords.
|
|
21
|
+
const actionVerbDetector_1 = require("./actionVerbDetector");
|
|
20
22
|
// ── Patterns that indicate a tool-less, plannable-free response ──────────────
|
|
21
23
|
/** Positive patterns: message can likely be answered without any tools. */
|
|
22
24
|
const FAST_PATH_PATTERNS = [
|
|
@@ -54,6 +56,8 @@ const PLANNER_REQUIRED_PATTERNS = [
|
|
|
54
56
|
/\b(then|after that|next|first .+ then|step by step|automate|workflow|pipeline|schedule|every (day|hour|minute)|on a schedule)\b/i,
|
|
55
57
|
// Email / calendar
|
|
56
58
|
/\b(send (email|mail|message)|check (email|calendar)|schedule (meeting|call)|remind me)\b/i,
|
|
59
|
+
// Music / media playback state — needs now_playing tool
|
|
60
|
+
/\b(now playing|currently playing|what (music|song|track|artist|album)|am i playing|is (playing|on))\b/i,
|
|
57
61
|
];
|
|
58
62
|
/**
|
|
59
63
|
* Returns true if this message can skip the planner and go directly to streamChat.
|
|
@@ -61,6 +65,9 @@ const PLANNER_REQUIRED_PATTERNS = [
|
|
|
61
65
|
*/
|
|
62
66
|
function matchFastPath(message) {
|
|
63
67
|
const trimmed = message.trim();
|
|
68
|
+
// Action intents always need the planner — never fast-path them
|
|
69
|
+
if ((0, actionVerbDetector_1.isActionIntent)(trimmed))
|
|
70
|
+
return false;
|
|
64
71
|
// Very short messages are usually conversational
|
|
65
72
|
if (trimmed.length < 20 && !/\b(run|exec|create|write|search|find|git)\b/i.test(trimmed)) {
|
|
66
73
|
return true;
|
package/dist/core/memoryIds.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.nextId = nextId;
|
|
|
41
41
|
exports.appendRecord = appendRecord;
|
|
42
42
|
exports.loadAllRecords = loadAllRecords;
|
|
43
43
|
exports.loadRecordById = loadRecordById;
|
|
44
|
+
exports.removeRecords = removeRecords;
|
|
44
45
|
exports.assignId = assignId;
|
|
45
46
|
exports.runMigrationIfNeeded = runMigrationIfNeeded;
|
|
46
47
|
/**
|
|
@@ -109,6 +110,21 @@ function loadRecordById(id) {
|
|
|
109
110
|
const all = loadAllRecords();
|
|
110
111
|
return all.find(r => r.id === id) ?? null;
|
|
111
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* C11: Remove records matching a predicate from records.jsonl.
|
|
115
|
+
* Rewrites the file with only the non-matching records.
|
|
116
|
+
* Returns the count of removed entries.
|
|
117
|
+
*/
|
|
118
|
+
function removeRecords(predicate) {
|
|
119
|
+
const all = loadAllRecords();
|
|
120
|
+
const kept = all.filter(r => !predicate(r));
|
|
121
|
+
const removed = all.length - kept.length;
|
|
122
|
+
if (removed > 0) {
|
|
123
|
+
_ensureDir();
|
|
124
|
+
fs.writeFileSync(RECORDS_FILE, kept.map(r => JSON.stringify(r)).join('\n') + (kept.length ? '\n' : ''), 'utf-8');
|
|
125
|
+
}
|
|
126
|
+
return removed;
|
|
127
|
+
}
|
|
112
128
|
// ── ID assignment helper ────────────────────────────────────────────────────
|
|
113
129
|
function assignId(partial) {
|
|
114
130
|
const record = {
|
|
@@ -57,7 +57,7 @@ exports.pluginHooks = {
|
|
|
57
57
|
};
|
|
58
58
|
exports.loadedFlatPlugins = [];
|
|
59
59
|
// ── Plugin context (passed to init / onLoad) ──────────────────
|
|
60
|
-
function makeContext(pluginName) {
|
|
60
|
+
function makeContext(pluginName, services = {}) {
|
|
61
61
|
return {
|
|
62
62
|
registerTool(def) {
|
|
63
63
|
(0, toolRegistry_1.registerExternalTool)(def.name, def.execute, pluginName);
|
|
@@ -76,10 +76,13 @@ function makeContext(pluginName) {
|
|
|
76
76
|
log(...args) {
|
|
77
77
|
console.log(`[Plugin:${pluginName}]`, ...args);
|
|
78
78
|
},
|
|
79
|
+
// Slash-command registry — register/unregister commands that appear in the
|
|
80
|
+
// Tab-dropdown. See PluginServices JSDoc above for full usage.
|
|
81
|
+
commandCatalog: services.commandCatalog ?? null,
|
|
79
82
|
};
|
|
80
83
|
}
|
|
81
84
|
// ── Loader ────────────────────────────────────────────────────
|
|
82
|
-
async function loadPlugins(pluginDir) {
|
|
85
|
+
async function loadPlugins(pluginDir, services = {}) {
|
|
83
86
|
if (!fs.existsSync(pluginDir)) {
|
|
84
87
|
fs.mkdirSync(pluginDir, { recursive: true });
|
|
85
88
|
return;
|
|
@@ -106,7 +109,7 @@ async function loadPlugins(pluginDir) {
|
|
|
106
109
|
console.warn(`[PluginLoader] ${file}: no init() — skipping`);
|
|
107
110
|
continue;
|
|
108
111
|
}
|
|
109
|
-
const ctx = makeContext(name);
|
|
112
|
+
const ctx = makeContext(name, services);
|
|
110
113
|
const dispose = await initFn.call(def.default ?? def, ctx);
|
|
111
114
|
exports.loadedFlatPlugins.push({
|
|
112
115
|
name,
|
|
@@ -127,7 +130,7 @@ async function loadPlugins(pluginDir) {
|
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
// ── Reload — dispose old plugins then reload all flat plugins ─
|
|
130
|
-
async function reloadPlugins(pluginDir) {
|
|
133
|
+
async function reloadPlugins(pluginDir, services = {}) {
|
|
131
134
|
// Dispose in reverse order
|
|
132
135
|
for (let i = exports.loadedFlatPlugins.length - 1; i >= 0; i--) {
|
|
133
136
|
const p = exports.loadedFlatPlugins[i];
|
|
@@ -145,7 +148,7 @@ async function reloadPlugins(pluginDir) {
|
|
|
145
148
|
exports.pluginHooks.postTool.length = 0;
|
|
146
149
|
exports.pluginHooks.onSessionStart.length = 0;
|
|
147
150
|
exports.pluginHooks.onSessionEnd.length = 0;
|
|
148
|
-
await loadPlugins(pluginDir);
|
|
151
|
+
await loadPlugins(pluginDir, services);
|
|
149
152
|
}
|
|
150
153
|
// ── Status ────────────────────────────────────────────────────
|
|
151
154
|
function listFlatPlugins() {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.protectedContextManager = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const WORKSPACE_ROOT = process.env.AIDEN_USER_DATA || process.cwd();
|
|
11
|
+
const WORKSPACE_DIR = path_1.default.join(WORKSPACE_ROOT, 'workspace');
|
|
12
|
+
// SOUL.md has two possible locations; try workspace first, fall back to root.
|
|
13
|
+
const SOUL_PATHS = [
|
|
14
|
+
path_1.default.join(WORKSPACE_DIR, 'SOUL.md'),
|
|
15
|
+
path_1.default.join(process.cwd(), 'SOUL.md'),
|
|
16
|
+
];
|
|
17
|
+
const PROTECTED_FILES = {
|
|
18
|
+
soul: SOUL_PATHS, // array = try in order
|
|
19
|
+
user: [path_1.default.join(WORKSPACE_DIR, 'USER.md')],
|
|
20
|
+
goals: [path_1.default.join(WORKSPACE_DIR, 'GOALS.md')],
|
|
21
|
+
standingOrders: [path_1.default.join(WORKSPACE_DIR, 'STANDING_ORDERS.md')],
|
|
22
|
+
lessons: [path_1.default.join(WORKSPACE_DIR, 'LESSONS.md')],
|
|
23
|
+
};
|
|
24
|
+
function sha1(s) {
|
|
25
|
+
return crypto_1.default.createHash('sha1').update(s).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
function readFirst(candidates) {
|
|
28
|
+
for (const p of candidates) {
|
|
29
|
+
try {
|
|
30
|
+
if (fs_1.default.existsSync(p))
|
|
31
|
+
return fs_1.default.readFileSync(p, 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
}
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
function diskHash(candidates) {
|
|
38
|
+
return sha1(readFirst(candidates));
|
|
39
|
+
}
|
|
40
|
+
class ProtectedContextManager {
|
|
41
|
+
constructor() {
|
|
42
|
+
this.cache = {
|
|
43
|
+
soul: { content: '', hash: '' },
|
|
44
|
+
user: { content: '', hash: '' },
|
|
45
|
+
goals: { content: '', hash: '' },
|
|
46
|
+
standingOrders: { content: '', hash: '' },
|
|
47
|
+
lessons: { content: '', hash: '' },
|
|
48
|
+
};
|
|
49
|
+
this.compositeHash = '';
|
|
50
|
+
this._totalReads = 0;
|
|
51
|
+
this._cacheHits = 0;
|
|
52
|
+
this._lastRefreshMs = 0;
|
|
53
|
+
this.refresh();
|
|
54
|
+
}
|
|
55
|
+
// Force re-read all 5 files from disk (called at boot).
|
|
56
|
+
refresh() {
|
|
57
|
+
for (const key of Object.keys(PROTECTED_FILES)) {
|
|
58
|
+
const candidates = PROTECTED_FILES[key];
|
|
59
|
+
const content = readFirst(candidates);
|
|
60
|
+
this.cache[key] = { content, hash: sha1(content) };
|
|
61
|
+
}
|
|
62
|
+
this.compositeHash = this._buildComposite();
|
|
63
|
+
this._lastRefreshMs = Date.now();
|
|
64
|
+
}
|
|
65
|
+
// Returns cached context, re-reading only files whose on-disk hash changed.
|
|
66
|
+
getProtectedContext() {
|
|
67
|
+
this._totalReads++;
|
|
68
|
+
const changedFiles = [];
|
|
69
|
+
for (const key of Object.keys(PROTECTED_FILES)) {
|
|
70
|
+
if (this.isStale(key)) {
|
|
71
|
+
const content = readFirst(PROTECTED_FILES[key]);
|
|
72
|
+
this.cache[key] = { content, hash: sha1(content) };
|
|
73
|
+
changedFiles.push(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (changedFiles.length > 0) {
|
|
77
|
+
this.compositeHash = this._buildComposite();
|
|
78
|
+
this._lastRefreshMs = Date.now();
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this._cacheHits++;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
soul: this.cache.soul.content,
|
|
85
|
+
user: this.cache.user.content,
|
|
86
|
+
goals: this.cache.goals.content,
|
|
87
|
+
standingOrders: this.cache.standingOrders.content,
|
|
88
|
+
lessons: this.cache.lessons.content,
|
|
89
|
+
hash: this.compositeHash,
|
|
90
|
+
changedFiles,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
getMetrics() {
|
|
94
|
+
return {
|
|
95
|
+
totalReads: this._totalReads,
|
|
96
|
+
cacheHits: this._cacheHits,
|
|
97
|
+
lastRefreshMs: this._lastRefreshMs,
|
|
98
|
+
currentHash: this.compositeHash,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// True if the on-disk hash for a file differs from our cached hash.
|
|
102
|
+
isStale(key) {
|
|
103
|
+
return diskHash(PROTECTED_FILES[key]) !== this.cache[key].hash;
|
|
104
|
+
}
|
|
105
|
+
_buildComposite() {
|
|
106
|
+
const parts = Object.keys(PROTECTED_FILES)
|
|
107
|
+
.map(k => this.cache[k].hash)
|
|
108
|
+
.join(':');
|
|
109
|
+
return sha1(parts).slice(0, 16);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.protectedContextManager = new ProtectedContextManager();
|
|
@@ -41,6 +41,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
41
41
|
};
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
43
|
exports.skillTeacher = exports.SkillTeacher = void 0;
|
|
44
|
+
exports.validateSkillName = validateSkillName;
|
|
45
|
+
exports.validateSkillTask = validateSkillTask;
|
|
44
46
|
// core/skillTeacher.ts — Self-learning skill generation.
|
|
45
47
|
// After every successful plan execution, records the tool sequence,
|
|
46
48
|
// generates a SKILL.md using the LLM, and promotes to "approved"
|
|
@@ -85,6 +87,45 @@ function extractSkillName(task, tools) {
|
|
|
85
87
|
.slice(0, 3);
|
|
86
88
|
return words.join('_') || 'general_task';
|
|
87
89
|
}
|
|
90
|
+
// ── C12: Skill pollution prevention — exported for testing ──────
|
|
91
|
+
const QUESTION_WORD_RE = /^(what|where|why|when|who|how|can|could|would|should|is|are)_/;
|
|
92
|
+
const PRONOUN_RE = /^(its|im|youre|whats|theyre|were)_/;
|
|
93
|
+
const PERSONAL_ID_RE = /(^|_)(users|shiva|admin|desktop|appdata)(_|$)/;
|
|
94
|
+
/**
|
|
95
|
+
* Validate a candidate skill name. Returns null if valid,
|
|
96
|
+
* or a rejection reason string.
|
|
97
|
+
*/
|
|
98
|
+
function validateSkillName(name) {
|
|
99
|
+
const words = name.split('_');
|
|
100
|
+
if (words.length > 4)
|
|
101
|
+
return `name has >4 underscore-separated words (${words.length})`;
|
|
102
|
+
if (QUESTION_WORD_RE.test(name))
|
|
103
|
+
return 'name starts with question word';
|
|
104
|
+
if (PRONOUN_RE.test(name))
|
|
105
|
+
return 'name starts with pronoun pattern';
|
|
106
|
+
if (PERSONAL_ID_RE.test(name))
|
|
107
|
+
return 'name contains personal identifier';
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Validate a candidate skill task description. Returns null if valid,
|
|
112
|
+
* or a rejection reason string.
|
|
113
|
+
* @param task - The skill's task description
|
|
114
|
+
* @param userMessage - Original user message (for verbatim check)
|
|
115
|
+
*/
|
|
116
|
+
function validateSkillTask(task, userMessage) {
|
|
117
|
+
const norm = task.toLowerCase().trim();
|
|
118
|
+
if (norm.length < 30)
|
|
119
|
+
return `task too short (${norm.length} chars, min 30)`;
|
|
120
|
+
if (norm.endsWith('?'))
|
|
121
|
+
return 'task is a question';
|
|
122
|
+
if (userMessage) {
|
|
123
|
+
const msgNorm = userMessage.toLowerCase().trim();
|
|
124
|
+
if (norm === msgNorm)
|
|
125
|
+
return 'task is verbatim copy of user message';
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
88
129
|
// ── SKILL.md generator ─────────────────────────────────────────
|
|
89
130
|
async function generateSkillContent(skillName, task, tools, duration, llmCaller, apiKey, model, provider) {
|
|
90
131
|
const prompt = `Generate a SKILL.md file for DevOS based on this successful task execution.
|
|
@@ -213,6 +254,28 @@ class SkillTeacher {
|
|
|
213
254
|
console.log(`[SkillTeacher] Rejected low-quality skill: "${skillName}"`);
|
|
214
255
|
return;
|
|
215
256
|
}
|
|
257
|
+
// ── C7: Destructive-skill prevention ──────────────────────
|
|
258
|
+
// Reject any skill that pairs shell_exec with a destructive task description.
|
|
259
|
+
// Prevents poisoned skills like "delete_users_shiva" that accidentally learned
|
|
260
|
+
// from test-triggered or misrouted Delete/Remove operations.
|
|
261
|
+
const DESTRUCTIVE_TASK_RE = /\b(delete|remove|rm\s|del\s|wipe|purge|erase|format|uninstall|drop\s+table|truncate)\b/i;
|
|
262
|
+
const usesShellExec = tools.some(t => t === 'shell_exec');
|
|
263
|
+
if (DESTRUCTIVE_TASK_RE.test(task) && usesShellExec) {
|
|
264
|
+
process.stderr.write(`[SkillTeacher] Rejected destructive skill: "${skillName}" (task="${task.slice(0, 60)}")\n`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// ── C12: Name pollution prevention ──────────────────────────
|
|
268
|
+
const nameRejection = validateSkillName(skillName);
|
|
269
|
+
if (nameRejection) {
|
|
270
|
+
process.stderr.write(`[SkillTeacher] Rejected "${skillName}": ${nameRejection}\n`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// ── C12: Task content validation ────────────────────────────
|
|
274
|
+
const taskRejection = validateSkillTask(task);
|
|
275
|
+
if (taskRejection) {
|
|
276
|
+
process.stderr.write(`[SkillTeacher] Rejected "${skillName}": ${taskRejection}\n`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
216
279
|
// ── Session rate limit — max SESSION_SKILL_LIMIT new skills ─
|
|
217
280
|
if (_sessionSkillsCreated >= SESSION_SKILL_LIMIT) {
|
|
218
281
|
console.log(`[SkillTeacher] Session limit reached (${SESSION_SKILL_LIMIT}), skipping: "${skillName}"`);
|
package/dist/core/slashAsTool.js
CHANGED
|
@@ -53,6 +53,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
53
53
|
const path_1 = __importDefault(require("path"));
|
|
54
54
|
const os_1 = __importDefault(require("os"));
|
|
55
55
|
const toolRegistry_1 = require("./toolRegistry");
|
|
56
|
+
const memoryIds_1 = require("./memoryIds");
|
|
56
57
|
const conversationMemory_1 = require("./conversationMemory");
|
|
57
58
|
const learningMemory_1 = require("./learningMemory");
|
|
58
59
|
const skillLoader_1 = require("./skillLoader");
|
|
@@ -188,6 +189,40 @@ async function toolChannelsStatus(_) {
|
|
|
188
189
|
return { success: true, output: 'Provider status unavailable.' };
|
|
189
190
|
}
|
|
190
191
|
}
|
|
192
|
+
// ── Memory store ──────────────────────────────────────────────────────────────
|
|
193
|
+
async function toolMemoryStore(input) {
|
|
194
|
+
// Accept any reasonable field name the planner might guess
|
|
195
|
+
const fact = String(input?.fact || input?.content || input?.text ||
|
|
196
|
+
input?.preference || input?.value || input?.memory ||
|
|
197
|
+
input?.note || input?.data || input?.information || input?.detail ||
|
|
198
|
+
input?.message || input?.entry || input?.record ||
|
|
199
|
+
(input && typeof input === 'object' ? Object.values(input).find(v => typeof v === 'string' && v.trim().length > 0) : '') || '').trim();
|
|
200
|
+
if (!fact)
|
|
201
|
+
return { success: false, output: 'No fact provided. Pass { fact: "the thing to remember" }' };
|
|
202
|
+
const record = (0, memoryIds_1.assignId)({
|
|
203
|
+
timestamp: new Date().toISOString(),
|
|
204
|
+
type: input?.type ?? 'fact',
|
|
205
|
+
content: fact,
|
|
206
|
+
summary: fact.slice(0, 100),
|
|
207
|
+
tags: Array.isArray(input?.tags) ? input.tags : [],
|
|
208
|
+
});
|
|
209
|
+
return { success: true, output: `Stored as ${record.id}: ${record.summary}` };
|
|
210
|
+
}
|
|
211
|
+
// ── Memory forget (C11) ──────────────────────────────────────────────────────
|
|
212
|
+
async function toolMemoryForget(input) {
|
|
213
|
+
const keyword = String(input?.fact || input?.keyword || input?.content || input?.text ||
|
|
214
|
+
input?.query || input?.topic || input?.subject ||
|
|
215
|
+
(input && typeof input === 'object'
|
|
216
|
+
? Object.values(input).find(v => typeof v === 'string' && v.trim().length > 0)
|
|
217
|
+
: '') || '').trim().toLowerCase();
|
|
218
|
+
if (!keyword)
|
|
219
|
+
return { success: false, output: 'No keyword provided. Pass { fact: "thing to forget" }' };
|
|
220
|
+
const removed = (0, memoryIds_1.removeRecords)(r => r.content.toLowerCase().includes(keyword) ||
|
|
221
|
+
r.summary.toLowerCase().includes(keyword));
|
|
222
|
+
if (removed === 0)
|
|
223
|
+
return { success: true, output: `No memory entries matched "${keyword}".` };
|
|
224
|
+
return { success: true, output: `Removed ${removed} memory entry(s) matching "${keyword}".` };
|
|
225
|
+
}
|
|
191
226
|
// ── Goals tool ────────────────────────────────────────────────────────────────
|
|
192
227
|
async function toolGoals(_) {
|
|
193
228
|
const summary = (0, goalTracker_1.getActiveGoalsSummary)();
|
|
@@ -199,6 +234,8 @@ const MIRROR_TOOLS = [
|
|
|
199
234
|
{ name: 'analytics', description: 'Show learning analytics: task count, success rate', fn: toolAnalytics },
|
|
200
235
|
{ name: 'spend', description: 'Show today\'s token cost and spend by provider', fn: toolSpend },
|
|
201
236
|
{ name: 'memory_show', description: 'Show conversation memory facts and recent history', fn: toolMemoryShow },
|
|
237
|
+
{ name: 'memory_store', description: 'Persist a fact or preference to permanent memory (records.jsonl) right now. Pass { fact: "..." }', fn: toolMemoryStore },
|
|
238
|
+
{ name: 'memory_forget', description: 'Remove a fact or preference from permanent memory (records.jsonl). Pass { fact: "thing to forget" }', fn: toolMemoryForget },
|
|
202
239
|
{ name: 'lessons', description: 'Show permanent failure rules learned from past tasks', fn: toolLessons },
|
|
203
240
|
{ name: 'skills_list', description: 'List all loaded skills with descriptions', fn: toolSkillsList },
|
|
204
241
|
{ name: 'tools_list', description: 'List all registered tool names', fn: toolToolsList },
|