knight-os 0.1.0
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/LICENSE +21 -0
- package/README.md +194 -0
- package/bin/knight.js +253 -0
- package/package.json +43 -0
- package/scripts/compress-memory.py +147 -0
- package/scripts/heartbeat.py +200 -0
- package/scripts/knight-status.py +219 -0
- package/scripts/reflection-analyzer.py +319 -0
- package/scripts/write-reflection.py +132 -0
- package/src/chat.js +237 -0
- package/src/config.js +128 -0
- package/src/setup.js +420 -0
- package/templates/AGENTS.md +82 -0
- package/templates/HEARTBEAT.md +54 -0
- package/templates/MEMORY.md +65 -0
- package/templates/PROJECTS.md +60 -0
- package/templates/REDLINES.md +99 -0
- package/templates/SOUL.md +39 -0
- package/templates/TOOLS.md +43 -0
- package/templates/USER.md +63 -0
- package/templates/memory/TEMPLATE-daily.md +21 -0
- package/templates/memory/ai-patterns.md +90 -0
- package/templates/memory/user-patterns.md +52 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const DEFAULTS = {
|
|
8
|
+
workspace: '~/.openclaw/workspace',
|
|
9
|
+
ai_name: 'Knight',
|
|
10
|
+
user_name: 'User',
|
|
11
|
+
timezone: 'UTC',
|
|
12
|
+
storage: {
|
|
13
|
+
backend: 'local',
|
|
14
|
+
local: {
|
|
15
|
+
reflections_dir: 'memory/reflections',
|
|
16
|
+
logs_dir: 'memory/logs',
|
|
17
|
+
memory_file: 'MEMORY.md',
|
|
18
|
+
},
|
|
19
|
+
supabase: {
|
|
20
|
+
url: '',
|
|
21
|
+
service_key: '',
|
|
22
|
+
enabled: false,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
notifications: {
|
|
26
|
+
backend: 'none',
|
|
27
|
+
telegram: {
|
|
28
|
+
bot_token: '',
|
|
29
|
+
chat_id: '',
|
|
30
|
+
enabled: false,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
heartbeat: {
|
|
34
|
+
interval_hours: 6,
|
|
35
|
+
enabled: false,
|
|
36
|
+
tasks: ['reflection_analysis', 'memory_scan', 'log_compress'],
|
|
37
|
+
},
|
|
38
|
+
reflection: {
|
|
39
|
+
min_pattern_count: 2,
|
|
40
|
+
auto_write: true,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function deepMerge(target, source) {
|
|
45
|
+
const result = { ...target };
|
|
46
|
+
for (const key of Object.keys(source)) {
|
|
47
|
+
if (
|
|
48
|
+
source[key] &&
|
|
49
|
+
typeof source[key] === 'object' &&
|
|
50
|
+
!Array.isArray(source[key]) &&
|
|
51
|
+
target[key] &&
|
|
52
|
+
typeof target[key] === 'object' &&
|
|
53
|
+
!Array.isArray(target[key])
|
|
54
|
+
) {
|
|
55
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
56
|
+
} else {
|
|
57
|
+
result[key] = source[key];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readJsonFile(filePath) {
|
|
64
|
+
try {
|
|
65
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
66
|
+
return JSON.parse(content);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveWorkspace(config) {
|
|
73
|
+
let ws = config.workspace || DEFAULTS.workspace;
|
|
74
|
+
if (ws.startsWith('~')) {
|
|
75
|
+
ws = path.join(os.homedir(), ws.slice(1));
|
|
76
|
+
}
|
|
77
|
+
return ws;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function applyEnvOverrides(config) {
|
|
81
|
+
if (process.env.KNIGHT_WORKSPACE) {
|
|
82
|
+
config.workspace = process.env.KNIGHT_WORKSPACE;
|
|
83
|
+
}
|
|
84
|
+
if (process.env.KNIGHT_AI_NAME) {
|
|
85
|
+
config.ai_name = process.env.KNIGHT_AI_NAME;
|
|
86
|
+
}
|
|
87
|
+
if (process.env.KNIGHT_USER_NAME) {
|
|
88
|
+
config.user_name = process.env.KNIGHT_USER_NAME;
|
|
89
|
+
}
|
|
90
|
+
if (process.env.KNIGHT_TIMEZONE) {
|
|
91
|
+
config.timezone = process.env.KNIGHT_TIMEZONE;
|
|
92
|
+
}
|
|
93
|
+
if (process.env.SUPABASE_URL) {
|
|
94
|
+
config.storage.supabase.url = process.env.SUPABASE_URL;
|
|
95
|
+
}
|
|
96
|
+
if (process.env.SUPABASE_SERVICE_KEY) {
|
|
97
|
+
config.storage.supabase.service_key = process.env.SUPABASE_SERVICE_KEY;
|
|
98
|
+
}
|
|
99
|
+
if (process.env.TELEGRAM_BOT_TOKEN) {
|
|
100
|
+
config.notifications.telegram.bot_token = process.env.TELEGRAM_BOT_TOKEN;
|
|
101
|
+
}
|
|
102
|
+
if (process.env.TELEGRAM_CHAT_ID) {
|
|
103
|
+
config.notifications.telegram.chat_id = process.env.TELEGRAM_CHAT_ID;
|
|
104
|
+
}
|
|
105
|
+
return config;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function loadConfig() {
|
|
109
|
+
let config = { ...DEFAULTS };
|
|
110
|
+
|
|
111
|
+
const globalPath = path.join(os.homedir(), '.knight', 'config.json');
|
|
112
|
+
const globalConfig = readJsonFile(globalPath);
|
|
113
|
+
if (globalConfig) {
|
|
114
|
+
config = deepMerge(config, globalConfig);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const localPath = path.join(process.cwd(), 'knight.config.json');
|
|
118
|
+
const localConfig = readJsonFile(localPath);
|
|
119
|
+
if (localConfig) {
|
|
120
|
+
config = deepMerge(config, localConfig);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
config = applyEnvOverrides(config);
|
|
124
|
+
|
|
125
|
+
return config;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { loadConfig, resolveWorkspace, DEFAULTS };
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { execSync, spawnSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_WORKSPACE = path.join(os.homedir(), '.openclaw', 'workspace');
|
|
10
|
+
|
|
11
|
+
function ask(rl, question, defaultVal) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const prompt = defaultVal ? `${question} [${defaultVal}]: ` : `${question}: `;
|
|
14
|
+
rl.question(prompt, (answer) => {
|
|
15
|
+
resolve(answer.trim() || defaultVal || '');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function checkOpenClaw() {
|
|
21
|
+
// Primary: look for openclaw binary in PATH
|
|
22
|
+
try {
|
|
23
|
+
const result = spawnSync('openclaw', ['--version'], { encoding: 'utf8', timeout: 5000 });
|
|
24
|
+
if (result.status === 0 && !result.error) {
|
|
25
|
+
return { installed: true, version: (result.stdout || '').trim().split('\n')[0] || 'unknown' };
|
|
26
|
+
}
|
|
27
|
+
} catch (_) {}
|
|
28
|
+
|
|
29
|
+
// Fallback: check npm global list via JSON output
|
|
30
|
+
try {
|
|
31
|
+
const result = spawnSync('npm', ['list', '-g', 'openclaw', '--depth=0', '--json'], {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
timeout: 10000,
|
|
34
|
+
});
|
|
35
|
+
if (result.stdout) {
|
|
36
|
+
const parsed = JSON.parse(result.stdout);
|
|
37
|
+
if (parsed && parsed.dependencies && parsed.dependencies.openclaw) {
|
|
38
|
+
const ver = parsed.dependencies.openclaw.version || 'unknown';
|
|
39
|
+
return { installed: true, version: ver };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch (_) {}
|
|
43
|
+
|
|
44
|
+
return { installed: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findPython3() {
|
|
48
|
+
// Prefer the python3 that's actually in PATH (accounts for Homebrew, pyenv, etc.)
|
|
49
|
+
try {
|
|
50
|
+
const result = spawnSync('which', ['python3'], { encoding: 'utf8', timeout: 3000 });
|
|
51
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
52
|
+
return result.stdout.trim();
|
|
53
|
+
}
|
|
54
|
+
} catch (_) {}
|
|
55
|
+
// Fallback candidates
|
|
56
|
+
for (const p of ['/opt/homebrew/bin/python3', '/usr/local/bin/python3', '/usr/bin/python3']) {
|
|
57
|
+
if (fs.existsSync(p)) return p;
|
|
58
|
+
}
|
|
59
|
+
return 'python3'; // last resort: let the shell resolve it
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeEnv(workspace, vars) {
|
|
63
|
+
const envPath = path.join(workspace, '.env');
|
|
64
|
+
let existing = '';
|
|
65
|
+
if (fs.existsSync(envPath)) {
|
|
66
|
+
existing = fs.readFileSync(envPath, 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
const envMap = {};
|
|
69
|
+
for (const line of existing.split('\n')) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
72
|
+
const idx = trimmed.indexOf('=');
|
|
73
|
+
if (idx === -1) continue;
|
|
74
|
+
envMap[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim();
|
|
75
|
+
}
|
|
76
|
+
Object.assign(envMap, vars);
|
|
77
|
+
const lines = Object.entries(envMap).map(([k, v]) => `${k}=${v}`);
|
|
78
|
+
fs.writeFileSync(envPath, lines.join('\n') + '\n', 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function registerHeartbeat(workspace, intervalHours) {
|
|
82
|
+
const platform = os.platform();
|
|
83
|
+
const python3 = findPython3();
|
|
84
|
+
|
|
85
|
+
if (platform === 'darwin') {
|
|
86
|
+
const label = 'ai.knight.heartbeat';
|
|
87
|
+
const launchAgentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
88
|
+
const plistPath = path.join(launchAgentsDir, `${label}.plist`);
|
|
89
|
+
const scriptPath = path.join(workspace, 'scripts', 'heartbeat.py');
|
|
90
|
+
const logPath = path.join(workspace, 'memory', 'logs', 'heartbeat.log');
|
|
91
|
+
|
|
92
|
+
// Ensure dirs exist
|
|
93
|
+
try { fs.mkdirSync(launchAgentsDir, { recursive: true }); } catch (_) {}
|
|
94
|
+
try { fs.mkdirSync(path.dirname(logPath), { recursive: true }); } catch (_) {}
|
|
95
|
+
|
|
96
|
+
const intervalSecs = Math.max(1, Math.floor(intervalHours * 3600));
|
|
97
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
98
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
99
|
+
<plist version="1.0">
|
|
100
|
+
<dict>
|
|
101
|
+
<key>Label</key>
|
|
102
|
+
<string>${label}</string>
|
|
103
|
+
<key>ProgramArguments</key>
|
|
104
|
+
<array>
|
|
105
|
+
<string>${python3}</string>
|
|
106
|
+
<string>${scriptPath}</string>
|
|
107
|
+
</array>
|
|
108
|
+
<key>WorkingDirectory</key>
|
|
109
|
+
<string>${workspace}</string>
|
|
110
|
+
<key>StartInterval</key>
|
|
111
|
+
<integer>${intervalSecs}</integer>
|
|
112
|
+
<key>StandardOutPath</key>
|
|
113
|
+
<string>${logPath}</string>
|
|
114
|
+
<key>StandardErrorPath</key>
|
|
115
|
+
<string>${logPath}</string>
|
|
116
|
+
<key>RunAtLoad</key>
|
|
117
|
+
<false/>
|
|
118
|
+
</dict>
|
|
119
|
+
</plist>`;
|
|
120
|
+
|
|
121
|
+
// Unload if already registered
|
|
122
|
+
try { spawnSync('launchctl', ['unload', plistPath], { stdio: 'ignore' }); } catch (_) {}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
fs.writeFileSync(plistPath, plist, 'utf-8');
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return { ok: false, method: 'launchd', error: `Failed to write plist: ${e.message}` };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const result = spawnSync('launchctl', ['load', plistPath], { encoding: 'utf8' });
|
|
132
|
+
if (result.status !== 0) {
|
|
133
|
+
return { ok: false, method: 'launchd', error: result.stderr || 'launchctl load failed' };
|
|
134
|
+
}
|
|
135
|
+
return { ok: true, method: 'launchd', path: plistPath };
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return { ok: false, method: 'launchd', error: e.message };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
} else if (platform === 'linux') {
|
|
141
|
+
const scriptPath = path.join(workspace, 'scripts', 'heartbeat.py');
|
|
142
|
+
const logPath = path.join(workspace, 'memory', 'logs', 'heartbeat.log');
|
|
143
|
+
try { fs.mkdirSync(path.dirname(logPath), { recursive: true }); } catch (_) {}
|
|
144
|
+
|
|
145
|
+
// Use safe quoting for cron entry — no shell interpolation
|
|
146
|
+
const safeScript = scriptPath.replace(/'/g, "'\\''");
|
|
147
|
+
const safeLog = logPath.replace(/'/g, "'\\''");
|
|
148
|
+
const cronLine = `0 */${Math.max(1, Math.floor(intervalHours))} * * * '${python3}' '${safeScript}' >> '${safeLog}' 2>&1`;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
let existing = '';
|
|
152
|
+
try {
|
|
153
|
+
const r = spawnSync('crontab', ['-l'], { encoding: 'utf8' });
|
|
154
|
+
if (r.status === 0) existing = r.stdout;
|
|
155
|
+
} catch (_) {}
|
|
156
|
+
|
|
157
|
+
if (!existing.includes(scriptPath)) {
|
|
158
|
+
const newCron = existing.trimEnd() + '\n' + cronLine + '\n';
|
|
159
|
+
const result = spawnSync('crontab', ['-'], {
|
|
160
|
+
input: newCron,
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
});
|
|
163
|
+
if (result.status !== 0) {
|
|
164
|
+
return { ok: false, method: 'cron', error: result.stderr || 'crontab write failed' };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { ok: true, method: 'cron', line: cronLine };
|
|
168
|
+
} catch (e) {
|
|
169
|
+
return { ok: false, method: 'cron', error: e.message };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
} else {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
method: 'none',
|
|
176
|
+
error: 'Automatic heartbeat not supported on this platform. Run heartbeat.py manually or use Task Scheduler.',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function setup() {
|
|
182
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
183
|
+
const separator = '──────────────────────────────────────────────────────';
|
|
184
|
+
|
|
185
|
+
const closeAndExit = (code) => {
|
|
186
|
+
rl.close();
|
|
187
|
+
process.exit(code);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
console.log('\n🐉 Knight OS — Setup Wizard\n');
|
|
191
|
+
console.log('This wizard configures your OpenClaw workspace with the Knight OS');
|
|
192
|
+
console.log('memory, reflection, and identity framework.\n');
|
|
193
|
+
console.log(separator);
|
|
194
|
+
|
|
195
|
+
// Step 1: Check OpenClaw
|
|
196
|
+
process.stdout.write('\n[1/6] Checking OpenClaw installation... ');
|
|
197
|
+
const oc = checkOpenClaw();
|
|
198
|
+
if (oc.installed) {
|
|
199
|
+
console.log(`✅ found (${oc.version})`);
|
|
200
|
+
} else {
|
|
201
|
+
console.log('❌ not found\n');
|
|
202
|
+
console.log('Knight OS requires OpenClaw. Please install it first:');
|
|
203
|
+
console.log('\n npm install -g openclaw\n');
|
|
204
|
+
console.log('Then run `knight setup` again.');
|
|
205
|
+
closeAndExit(1);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Step 2: Workspace directory
|
|
210
|
+
console.log('\n[2/6] Workspace configuration');
|
|
211
|
+
const workspaceInput = await ask(rl, 'Workspace directory', DEFAULT_WORKSPACE);
|
|
212
|
+
const workspace = path.resolve(workspaceInput.replace(/^~/, os.homedir()));
|
|
213
|
+
|
|
214
|
+
const workspaceExists = fs.existsSync(workspace);
|
|
215
|
+
const hasCoreFiles = workspaceExists && fs.existsSync(path.join(workspace, 'AGENTS.md'));
|
|
216
|
+
|
|
217
|
+
let overwrite = true;
|
|
218
|
+
if (hasCoreFiles) {
|
|
219
|
+
console.log(`\n⚠️ Workspace already exists at: ${workspace}`);
|
|
220
|
+
const answer = await ask(rl, 'Overwrite existing files? (y/N)', 'N');
|
|
221
|
+
overwrite = answer.toLowerCase().startsWith('y');
|
|
222
|
+
if (!overwrite) {
|
|
223
|
+
console.log('\nSkipping template write. Continuing with other setup steps...');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Create required dirs
|
|
228
|
+
try {
|
|
229
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
230
|
+
fs.mkdirSync(path.join(workspace, 'memory', 'logs'), { recursive: true });
|
|
231
|
+
fs.mkdirSync(path.join(workspace, 'memory', 'projects'), { recursive: true });
|
|
232
|
+
fs.mkdirSync(path.join(workspace, 'memory', 'reflections'), { recursive: true });
|
|
233
|
+
fs.mkdirSync(path.join(workspace, 'output'), { recursive: true });
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.log(`\n❌ Failed to create workspace directory: ${e.message}`);
|
|
236
|
+
closeAndExit(1);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Step 3: Identity questions
|
|
241
|
+
console.log('\n[3/6] Identity setup');
|
|
242
|
+
const aiName = await ask(rl, "Your AI companion's name (e.g. Aria, Nova, Kai)", 'Knight');
|
|
243
|
+
const userName = await ask(rl, 'Your name', '');
|
|
244
|
+
if (!userName) {
|
|
245
|
+
console.log('Name is required.');
|
|
246
|
+
closeAndExit(1);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const timezone = await ask(rl, 'Your timezone (e.g. Asia/Tokyo, America/New_York)', 'UTC');
|
|
250
|
+
const language = await ask(rl, 'Primary language (en / zh / ja)', 'en');
|
|
251
|
+
|
|
252
|
+
// Step 4: API key
|
|
253
|
+
console.log('\n[4/6] API configuration');
|
|
254
|
+
let anthropicKey = '';
|
|
255
|
+
while (true) {
|
|
256
|
+
const input = await ask(rl, 'Anthropic API key (starts with sk-ant-, leave blank to skip)', '');
|
|
257
|
+
if (!input) break;
|
|
258
|
+
if (input.startsWith('sk-ant-')) {
|
|
259
|
+
anthropicKey = input;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
console.log(" ⚠️ API key should start with 'sk-ant-'. Try again or leave blank to skip.");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Step 5: Notifications (optional)
|
|
266
|
+
console.log('\n[5/6] Notifications (optional — press Enter to skip)');
|
|
267
|
+
const tgToken = await ask(rl, 'Telegram Bot Token', '');
|
|
268
|
+
const tgChatId = tgToken ? await ask(rl, 'Telegram Chat ID', '') : '';
|
|
269
|
+
|
|
270
|
+
// Step 6: Heartbeat
|
|
271
|
+
console.log('\n[6/6] Heartbeat scheduler');
|
|
272
|
+
const enableHb = await ask(rl, 'Register automatic heartbeat? (Y/n)', 'Y');
|
|
273
|
+
const doHeartbeat = !enableHb.toLowerCase().startsWith('n');
|
|
274
|
+
let hbIntervalHours = 6;
|
|
275
|
+
if (doHeartbeat) {
|
|
276
|
+
const hbInterval = await ask(rl, 'Heartbeat interval in hours (min 1, max 24)', '6');
|
|
277
|
+
const parsed = parseInt(hbInterval, 10);
|
|
278
|
+
hbIntervalHours = (isNaN(parsed) || parsed < 1) ? 6 : Math.min(parsed, 24);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
rl.close();
|
|
282
|
+
|
|
283
|
+
console.log(`\n${separator}`);
|
|
284
|
+
console.log('⚙️ Writing workspace files...\n');
|
|
285
|
+
|
|
286
|
+
// Write templates
|
|
287
|
+
if (overwrite || !hasCoreFiles) {
|
|
288
|
+
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
289
|
+
const vars = { aiName, userName, timezone, language };
|
|
290
|
+
|
|
291
|
+
function fillTemplate(content, v) {
|
|
292
|
+
return content
|
|
293
|
+
.replace(/\{\{AI_NAME\}\}/g, v.aiName)
|
|
294
|
+
.replace(/\{\{USER_NAME\}\}/g, v.userName)
|
|
295
|
+
.replace(/\{\{TIMEZONE\}\}/g, v.timezone)
|
|
296
|
+
.replace(/\{\{LANGUAGE\}\}/g, v.language || 'en')
|
|
297
|
+
.replace(/\{\{CHANNEL\}\}/g, 'direct');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function copyTemplates(srcDir, destDir) {
|
|
301
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
const src = path.join(srcDir, entry.name);
|
|
304
|
+
const dest = path.join(destDir, entry.name);
|
|
305
|
+
if (entry.isDirectory()) {
|
|
306
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
307
|
+
copyTemplates(src, dest);
|
|
308
|
+
} else {
|
|
309
|
+
try {
|
|
310
|
+
const content = fs.readFileSync(src, 'utf-8');
|
|
311
|
+
fs.writeFileSync(dest, fillTemplate(content, vars), 'utf-8');
|
|
312
|
+
console.log(` ✅ ${path.relative(workspace, dest)}`);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.log(` ⚠️ ${path.relative(workspace, dest)}: ${e.message}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
copyTemplates(TEMPLATES_DIR, workspace);
|
|
321
|
+
} else {
|
|
322
|
+
console.log(' ⏭️ Templates skipped (existing files preserved)');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Copy scripts (never overwrite — user may have customized them)
|
|
326
|
+
const scriptsDir = path.join(__dirname, '..', 'scripts');
|
|
327
|
+
const destScriptsDir = path.join(workspace, 'scripts');
|
|
328
|
+
fs.mkdirSync(destScriptsDir, { recursive: true });
|
|
329
|
+
for (const file of fs.readdirSync(scriptsDir)) {
|
|
330
|
+
const src = path.join(scriptsDir, file);
|
|
331
|
+
const dest = path.join(destScriptsDir, file);
|
|
332
|
+
if (!fs.existsSync(dest)) {
|
|
333
|
+
try {
|
|
334
|
+
fs.copyFileSync(src, dest);
|
|
335
|
+
console.log(` ✅ scripts/${file}`);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
console.log(` ⚠️ scripts/${file}: ${e.message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Write knight.config.json
|
|
343
|
+
const config = {
|
|
344
|
+
workspace,
|
|
345
|
+
ai_name: aiName,
|
|
346
|
+
user_name: userName,
|
|
347
|
+
timezone,
|
|
348
|
+
storage: {
|
|
349
|
+
backend: 'local',
|
|
350
|
+
local: { reflections_dir: 'memory/reflections', logs_dir: 'memory/logs', memory_file: 'MEMORY.md' },
|
|
351
|
+
supabase: { url: '', service_key: '', enabled: false },
|
|
352
|
+
},
|
|
353
|
+
notifications: {
|
|
354
|
+
backend: tgToken ? 'telegram' : 'none',
|
|
355
|
+
telegram: { bot_token: tgToken, chat_id: tgChatId, enabled: !!tgToken },
|
|
356
|
+
},
|
|
357
|
+
heartbeat: {
|
|
358
|
+
interval_hours: hbIntervalHours,
|
|
359
|
+
enabled: doHeartbeat,
|
|
360
|
+
tasks: ['reflection_analysis', 'memory_scan', 'log_compress'],
|
|
361
|
+
},
|
|
362
|
+
reflection: { min_pattern_count: 2, auto_write: true },
|
|
363
|
+
model: {
|
|
364
|
+
provider: 'anthropic',
|
|
365
|
+
name: 'claude-sonnet-4-5',
|
|
366
|
+
max_tokens: 8096,
|
|
367
|
+
system_prompt_files: ['SOUL.md', 'AGENTS.md', 'MEMORY.md', 'REDLINES.md'],
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const configDest = path.join(workspace, 'knight.config.json');
|
|
373
|
+
fs.writeFileSync(configDest, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
374
|
+
console.log(' ✅ knight.config.json');
|
|
375
|
+
} catch (e) {
|
|
376
|
+
console.log(` ⚠️ knight.config.json: ${e.message}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Write .env
|
|
380
|
+
const envVars = {};
|
|
381
|
+
if (anthropicKey) envVars.ANTHROPIC_API_KEY = anthropicKey;
|
|
382
|
+
if (tgToken) envVars.TELEGRAM_BOT_TOKEN = tgToken;
|
|
383
|
+
if (tgChatId) envVars.TELEGRAM_CHAT_ID = tgChatId;
|
|
384
|
+
if (Object.keys(envVars).length > 0) {
|
|
385
|
+
try {
|
|
386
|
+
writeEnv(workspace, envVars);
|
|
387
|
+
console.log(' ✅ .env');
|
|
388
|
+
} catch (e) {
|
|
389
|
+
console.log(` ⚠️ .env: ${e.message}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Register heartbeat
|
|
394
|
+
if (doHeartbeat) {
|
|
395
|
+
process.stdout.write('\n⏱️ Registering heartbeat scheduler... ');
|
|
396
|
+
const result = registerHeartbeat(workspace, hbIntervalHours);
|
|
397
|
+
if (result.ok) {
|
|
398
|
+
console.log(`✅ ${result.method} (every ${hbIntervalHours}h)`);
|
|
399
|
+
} else {
|
|
400
|
+
console.log(`⚠️ ${result.error}`);
|
|
401
|
+
console.log(` Manual: run \`python3 ${path.join(workspace, 'scripts', 'heartbeat.py')}\` periodically`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.log(`\n${separator}`);
|
|
406
|
+
console.log('✅ Knight OS setup complete!\n');
|
|
407
|
+
console.log(`Workspace: ${workspace}`);
|
|
408
|
+
console.log('\nNext steps:');
|
|
409
|
+
console.log(' 1. Review and customize your SOUL.md (AI personality)');
|
|
410
|
+
console.log(' 2. Fill in USER.md (your profile)');
|
|
411
|
+
console.log(' 3. Start chatting: openclaw chat\n');
|
|
412
|
+
console.log('How memory works:');
|
|
413
|
+
console.log(' Task done → write-reflection.py → memory/reflections/');
|
|
414
|
+
console.log(' Heartbeat → reflection-analyzer.py → candidate rules extracted');
|
|
415
|
+
console.log(' You confirm → rules added to memory/ai-patterns.md');
|
|
416
|
+
console.log(' Next session → ai-patterns.md in system prompt → AI learns\n');
|
|
417
|
+
console.log(`${separator}\n`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
module.exports = { setup };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# AGENTS.md — {{AI_NAME}} Workspace Entry Point
|
|
2
|
+
|
|
3
|
+
> This is the first file {{AI_NAME}} reads when a session starts.
|
|
4
|
+
|
|
5
|
+
## File Responsibilities
|
|
6
|
+
|
|
7
|
+
| File | Purpose |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| AGENTS.md | Workspace boot sequence and structural overview |
|
|
10
|
+
| REDLINES.md | Absolute safety boundaries — never violate |
|
|
11
|
+
| SOUL.md | {{AI_NAME}}'s identity, personality, and working style |
|
|
12
|
+
| MEMORY.md | Long-term memory index and quick-reference |
|
|
13
|
+
| memory/user-patterns.md | Observed user behavior patterns |
|
|
14
|
+
| memory/ai-patterns.md | {{AI_NAME}}'s learned behavior rules |
|
|
15
|
+
| PROJECTS.md | Active project index |
|
|
16
|
+
| HEARTBEAT.md | Periodic self-check mechanism |
|
|
17
|
+
| USER.md | User profile and preferences |
|
|
18
|
+
| TOOLS.md | Available tools and credentials reference |
|
|
19
|
+
|
|
20
|
+
## High-Priority Rules
|
|
21
|
+
|
|
22
|
+
1. **Error = Report** — If something fails or seems wrong, report immediately. Never hide errors.
|
|
23
|
+
2. **Diff before modify** — Before modifying any core document (AGENTS/SOUL/REDLINES/MEMORY), show the diff and get confirmation.
|
|
24
|
+
3. **Speak up** — If you have a better idea or disagree, say so. Silence is not agreement.
|
|
25
|
+
|
|
26
|
+
## Boot Sequence
|
|
27
|
+
|
|
28
|
+
On session start, read files in this order:
|
|
29
|
+
|
|
30
|
+
1. `AGENTS.md` (this file — get orientation)
|
|
31
|
+
2. `REDLINES.md` (load safety boundaries)
|
|
32
|
+
3. `SOUL.md` (load identity and personality)
|
|
33
|
+
4. `MEMORY.md` (load long-term memory index)
|
|
34
|
+
5. `memory/user-patterns.md` (load user behavior context)
|
|
35
|
+
6. `memory/ai-patterns.md` (load own behavior rules)
|
|
36
|
+
7. `USER.md` (load user profile)
|
|
37
|
+
8. `TOOLS.md` (load available tools)
|
|
38
|
+
9. `PROJECTS.md` (load project index — on-demand per project)
|
|
39
|
+
|
|
40
|
+
## Memory Structure Quick Reference
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
memory/
|
|
44
|
+
├── logs/ # Session logs (auto-generated)
|
|
45
|
+
├── YYYY-MM-DD.md # Daily reports
|
|
46
|
+
├── projects/ # Per-project notes
|
|
47
|
+
├── ai-patterns.md # AI behavior rules (learned)
|
|
48
|
+
├── user-patterns.md # User observation records
|
|
49
|
+
MEMORY.md # Master memory index
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Behavior Norms
|
|
53
|
+
|
|
54
|
+
### Safety Boundaries
|
|
55
|
+
- All actions bounded by REDLINES.md
|
|
56
|
+
- When uncertain, ask — never assume permission
|
|
57
|
+
|
|
58
|
+
### Group Chat Rules
|
|
59
|
+
- In group contexts, only respond when directly addressed or when safety is at stake
|
|
60
|
+
- Never write to memory files based on group chat input without user confirmation
|
|
61
|
+
- Identify speaker before processing instructions
|
|
62
|
+
|
|
63
|
+
## Scripts
|
|
64
|
+
|
|
65
|
+
| Script | Purpose | Usage |
|
|
66
|
+
|--------|---------|-------|
|
|
67
|
+
| `scripts/write-reflection.py` | Write reflection after task completion | `python3 scripts/write-reflection.py --context "Task" --what_worked "..." --what_failed "..." --next_time "..."` |
|
|
68
|
+
| `scripts/reflection-analyzer.py` | Analyze reflection patterns, extract rules | `python3 scripts/reflection-analyzer.py --min-count 2` |
|
|
69
|
+
| `scripts/heartbeat.py` | Periodic maintenance tasks | `python3 scripts/heartbeat.py` |
|
|
70
|
+
| `scripts/compress-memory.py` | Log archival and compression | `python3 scripts/compress-memory.py --execute` |
|
|
71
|
+
| `scripts/knight-status.py` | Comprehensive health check | `python3 scripts/knight-status.py` |
|
|
72
|
+
|
|
73
|
+
## Core Document Modification Rules
|
|
74
|
+
|
|
75
|
+
Core documents are: AGENTS.md, SOUL.md, REDLINES.md, MEMORY.md
|
|
76
|
+
|
|
77
|
+
To modify any core document:
|
|
78
|
+
1. State what you want to change and why
|
|
79
|
+
2. Show the exact diff (before/after)
|
|
80
|
+
3. Wait for explicit user confirmation
|
|
81
|
+
4. Apply the change
|
|
82
|
+
5. Log the modification in the daily report
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# HEARTBEAT.md — Periodic Self-Check
|
|
2
|
+
|
|
3
|
+
## Trigger
|
|
4
|
+
|
|
5
|
+
- **Automatic**: Every 6 hours during active sessions
|
|
6
|
+
- **Manual**: User sends `/heartbeat`
|
|
7
|
+
|
|
8
|
+
## Async Task Queue
|
|
9
|
+
|
|
10
|
+
**Status: OFF**
|
|
11
|
+
|
|
12
|
+
When enabled, the async queue allows {{AI_NAME}} to track and execute background tasks between active sessions. Toggle via user command.
|
|
13
|
+
|
|
14
|
+
## Execution Checklist
|
|
15
|
+
|
|
16
|
+
When heartbeat triggers, run through this checklist:
|
|
17
|
+
|
|
18
|
+
### 1. Task Scoring
|
|
19
|
+
- Review all active tasks in MEMORY.md
|
|
20
|
+
- Score each by urgency (1-5) and importance (1-5)
|
|
21
|
+
- Flag any overdue items
|
|
22
|
+
|
|
23
|
+
### 2. User Feedback Review
|
|
24
|
+
- Check if there's unprocessed user feedback
|
|
25
|
+
- Extract actionable patterns
|
|
26
|
+
- Update memory/ai-patterns.md if warranted (with confirmation)
|
|
27
|
+
|
|
28
|
+
### 3. Async Queue Processing
|
|
29
|
+
- If queue is ON: check for pending background tasks
|
|
30
|
+
- Execute any that are due
|
|
31
|
+
- Report results in next interaction
|
|
32
|
+
|
|
33
|
+
### 4. Memory Scan
|
|
34
|
+
- Review recent daily logs for patterns worth promoting
|
|
35
|
+
- Check for stale or contradictory entries in MEMORY.md
|
|
36
|
+
- Propose cleanup if needed
|
|
37
|
+
|
|
38
|
+
### 5. Rule Extraction
|
|
39
|
+
- Review recent interactions for recurring corrections
|
|
40
|
+
- If a correction appears 3+ times, propose a new rule for ai-patterns.md
|
|
41
|
+
- Present proposed rules to user for confirmation
|
|
42
|
+
|
|
43
|
+
## Output Format
|
|
44
|
+
|
|
45
|
+
After running heartbeat, produce a brief report:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
## Heartbeat Report — [date/time]
|
|
49
|
+
- Tasks: X active, Y overdue
|
|
50
|
+
- Feedback: [processed/none pending]
|
|
51
|
+
- Queue: [ON/OFF] — [N items processed / empty]
|
|
52
|
+
- Memory: [clean / N items to review]
|
|
53
|
+
- Rules: [N proposed / none]
|
|
54
|
+
```
|