knight-os 0.1.0 β 0.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/bin/knight.js +57 -0
- package/package.json +1 -1
- package/src/migrate.js +306 -0
- package/src/setup.js +58 -9
- package/templates/AGENTS.md +24 -1
- package/templates/PROJECTS.md +38 -19
package/bin/knight.js
CHANGED
|
@@ -8,6 +8,13 @@ const readline = require('readline');
|
|
|
8
8
|
const { loadConfig, resolveWorkspace } = require('../src/config');
|
|
9
9
|
const { chat } = require('../src/chat');
|
|
10
10
|
const { setup } = require('../src/setup');
|
|
11
|
+
const {
|
|
12
|
+
runMigrations,
|
|
13
|
+
checkVersion,
|
|
14
|
+
refreshTemplates,
|
|
15
|
+
backupWorkspace,
|
|
16
|
+
CURRENT_DATA_VERSION,
|
|
17
|
+
} = require('../src/migrate');
|
|
11
18
|
|
|
12
19
|
const VERSION = '0.1.0';
|
|
13
20
|
const DEFAULT_WORKSPACE = path.join(process.env.HOME || '~', '.openclaw', 'workspace');
|
|
@@ -213,6 +220,52 @@ function commandVersion() {
|
|
|
213
220
|
console.log(`knight-os v${VERSION}`);
|
|
214
221
|
}
|
|
215
222
|
|
|
223
|
+
async function commandUpgrade() {
|
|
224
|
+
const workspace = DEFAULT_WORKSPACE;
|
|
225
|
+
|
|
226
|
+
console.log(`\nπ Knight OS β Upgrade Check`);
|
|
227
|
+
console.log(` Workspace: ${workspace}\n`);
|
|
228
|
+
|
|
229
|
+
if (!fs.existsSync(workspace)) {
|
|
230
|
+
console.log(' β Workspace not found. Run "knight setup" first.\n');
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 1. Run data migrations
|
|
235
|
+
const { migrated, backupPath, error } = runMigrations(workspace);
|
|
236
|
+
if (error) {
|
|
237
|
+
console.error(`\nβ Upgrade failed: ${error.message}\n`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!migrated) {
|
|
242
|
+
const { currentVersion } = checkVersion(workspace);
|
|
243
|
+
console.log(` β
Already up to date (data v${currentVersion}).\n`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 2. Refresh non-protected template files (add new ones, skip existing)
|
|
247
|
+
console.log(' Checking for new template filesβ¦');
|
|
248
|
+
const { added, skipped } = refreshTemplates(workspace, TEMPLATES_DIR);
|
|
249
|
+
if (added.length > 0) {
|
|
250
|
+
console.log(` β
Added ${added.length} new file(s):`);
|
|
251
|
+
added.forEach((f) => console.log(` + ${f}`));
|
|
252
|
+
} else {
|
|
253
|
+
console.log(' β
No new template files.');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const protectedSkipped = skipped.filter((s) => s.includes('(protected)'));
|
|
257
|
+
if (protectedSkipped.length > 0) {
|
|
258
|
+
console.log(`\n π Protected files untouched (your personal data is safe):`);
|
|
259
|
+
protectedSkipped.forEach((f) => console.log(` ${f}`));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (backupPath) {
|
|
263
|
+
console.log(`\n π¦ Backup kept at:\n ${backupPath}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log(`\nβ
Upgrade complete. Workspace is at data v${CURRENT_DATA_VERSION}.\n`);
|
|
267
|
+
}
|
|
268
|
+
|
|
216
269
|
async function commandChat() {
|
|
217
270
|
const config = loadConfig();
|
|
218
271
|
const workspace = resolveWorkspace(config);
|
|
@@ -234,6 +287,9 @@ switch (command) {
|
|
|
234
287
|
case 'status':
|
|
235
288
|
commandStatus();
|
|
236
289
|
break;
|
|
290
|
+
case 'upgrade':
|
|
291
|
+
commandUpgrade();
|
|
292
|
+
break;
|
|
237
293
|
case 'version':
|
|
238
294
|
case '--version':
|
|
239
295
|
case '-v':
|
|
@@ -247,6 +303,7 @@ switch (command) {
|
|
|
247
303
|
console.log(' init Initialize a new workspace (standalone, no OpenClaw required)');
|
|
248
304
|
console.log(' chat Start interactive AI chat session');
|
|
249
305
|
console.log(' status Check workspace file status');
|
|
306
|
+
console.log(' upgrade Migrate workspace data + refresh template files safely');
|
|
250
307
|
console.log(' version Show version number');
|
|
251
308
|
console.log('');
|
|
252
309
|
break;
|
package/package.json
CHANGED
package/src/migrate.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* migrate.js β Safe upgrade framework for knight-os
|
|
5
|
+
*
|
|
6
|
+
* Design principles:
|
|
7
|
+
* 1. Data and code live in different places β npm upgrades never touch user data
|
|
8
|
+
* 2. Version file (.knight-version) tracks the data format version
|
|
9
|
+
* 3. Before any migration: full backup to .knight-backups/<timestamp>/
|
|
10
|
+
* 4. Migrations only ADD or TRANSFORM β never delete user content
|
|
11
|
+
* 5. Protected files (SOUL/MEMORY/USER/REDLINES) are never touched
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
18
|
+
// Constants
|
|
19
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
20
|
+
|
|
21
|
+
/** Current data format version expected by this version of knight-os */
|
|
22
|
+
const CURRENT_DATA_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
/** Version file stored in the workspace root */
|
|
25
|
+
const VERSION_FILE = '.knight-version';
|
|
26
|
+
|
|
27
|
+
/** Backup directory inside the workspace */
|
|
28
|
+
const BACKUP_DIR = '.knight-backups';
|
|
29
|
+
|
|
30
|
+
/** Files that must never be overwritten during migration */
|
|
31
|
+
const PROTECTED_FILES = ['SOUL.md', 'MEMORY.md', 'USER.md', 'REDLINES.md'];
|
|
32
|
+
|
|
33
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
34
|
+
// Version helpers
|
|
35
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read the data version from the workspace.
|
|
39
|
+
* Returns 0 if the file doesn't exist (pre-versioning install).
|
|
40
|
+
*/
|
|
41
|
+
function readDataVersion(workspace) {
|
|
42
|
+
const versionPath = path.join(workspace, VERSION_FILE);
|
|
43
|
+
if (!fs.existsSync(versionPath)) return 0;
|
|
44
|
+
const raw = fs.readFileSync(versionPath, 'utf8').trim();
|
|
45
|
+
const parsed = parseInt(raw, 10);
|
|
46
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Write the data version to the workspace.
|
|
51
|
+
*/
|
|
52
|
+
function writeDataVersion(workspace, version) {
|
|
53
|
+
const versionPath = path.join(workspace, VERSION_FILE);
|
|
54
|
+
fs.writeFileSync(versionPath, String(version) + '\n', 'utf8');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
58
|
+
// Backup
|
|
59
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Recursively copy files from src to dst, skipping .knight-backups itself.
|
|
63
|
+
*/
|
|
64
|
+
function copyDirRecursive(src, dst) {
|
|
65
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
66
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (entry.name === BACKUP_DIR) continue; // don't backup backups
|
|
69
|
+
const srcPath = path.join(src, entry.name);
|
|
70
|
+
const dstPath = path.join(dst, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
copyDirRecursive(srcPath, dstPath);
|
|
73
|
+
} else {
|
|
74
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a timestamped backup of the entire workspace.
|
|
81
|
+
* Returns the backup path so callers can report it to the user.
|
|
82
|
+
*/
|
|
83
|
+
function backupWorkspace(workspace) {
|
|
84
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
85
|
+
const backupPath = path.join(workspace, BACKUP_DIR, timestamp);
|
|
86
|
+
console.log(` π¦ Backing up workspace to ${backupPath} β¦`);
|
|
87
|
+
copyDirRecursive(workspace, backupPath);
|
|
88
|
+
console.log(` β
Backup complete.`);
|
|
89
|
+
return backupPath;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
93
|
+
// Migration registry
|
|
94
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Each migration:
|
|
98
|
+
* from β data version before this migration
|
|
99
|
+
* to β data version after this migration
|
|
100
|
+
* desc β human-readable description
|
|
101
|
+
* run(workspace) β the actual migration function; must not throw on clean workspaces
|
|
102
|
+
*/
|
|
103
|
+
const MIGRATIONS = [
|
|
104
|
+
{
|
|
105
|
+
from: 0,
|
|
106
|
+
to: 1,
|
|
107
|
+
desc: 'Bootstrap versioning β record baseline data version for existing installs',
|
|
108
|
+
run(workspace) {
|
|
109
|
+
// Ensure memory subdirectories exist (previously optional)
|
|
110
|
+
const memoryDirs = [
|
|
111
|
+
'memory/logs',
|
|
112
|
+
'memory/projects',
|
|
113
|
+
'memory/templates',
|
|
114
|
+
'memory/references',
|
|
115
|
+
];
|
|
116
|
+
for (const dir of memoryDirs) {
|
|
117
|
+
const fullPath = path.join(workspace, dir);
|
|
118
|
+
if (!fs.existsSync(fullPath)) {
|
|
119
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
120
|
+
console.log(` π Created missing directory: ${dir}/`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add UPGRADE.md so users know migration ran
|
|
125
|
+
const upgradePath = path.join(workspace, 'UPGRADE.md');
|
|
126
|
+
if (!fs.existsSync(upgradePath)) {
|
|
127
|
+
fs.writeFileSync(
|
|
128
|
+
upgradePath,
|
|
129
|
+
[
|
|
130
|
+
'# Upgrade Log',
|
|
131
|
+
'',
|
|
132
|
+
'knight-os upgrade history for this workspace.',
|
|
133
|
+
'This file is auto-maintained β do not edit.',
|
|
134
|
+
'',
|
|
135
|
+
].join('\n'),
|
|
136
|
+
'utf8'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
// Append an entry
|
|
140
|
+
const entry = `\n## v1 β ${new Date().toISOString().slice(0, 10)}\n- Baseline version established\n- memory/ subdirectories ensured\n`;
|
|
141
|
+
fs.appendFileSync(upgradePath, entry, 'utf8');
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// ββ Future migrations go here ββββββββββββββββββββββββββββββ
|
|
146
|
+
//
|
|
147
|
+
// Example v1 β v2:
|
|
148
|
+
// {
|
|
149
|
+
// from: 1,
|
|
150
|
+
// to: 2,
|
|
151
|
+
// desc: 'Rename ai-patterns.md β noa-patterns.md',
|
|
152
|
+
// run(workspace) {
|
|
153
|
+
// const oldPath = path.join(workspace, 'memory', 'ai-patterns.md');
|
|
154
|
+
// const newPath = path.join(workspace, 'memory', 'noa-patterns.md');
|
|
155
|
+
// if (fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
|
|
156
|
+
// fs.renameSync(oldPath, newPath);
|
|
157
|
+
// }
|
|
158
|
+
// },
|
|
159
|
+
// },
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
163
|
+
// Migration runner
|
|
164
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if the workspace data is up to date.
|
|
168
|
+
* Returns { needsMigration: bool, currentVersion: number, targetVersion: number }
|
|
169
|
+
*/
|
|
170
|
+
function checkVersion(workspace) {
|
|
171
|
+
const currentVersion = readDataVersion(workspace);
|
|
172
|
+
return {
|
|
173
|
+
needsMigration: currentVersion < CURRENT_DATA_VERSION,
|
|
174
|
+
currentVersion,
|
|
175
|
+
targetVersion: CURRENT_DATA_VERSION,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Run all pending migrations for the workspace.
|
|
181
|
+
*
|
|
182
|
+
* - Skips silently if already up to date.
|
|
183
|
+
* - Creates a backup before running any migrations.
|
|
184
|
+
* - Runs migrations in order, updating the version file after each one.
|
|
185
|
+
* - If a migration throws, stops immediately (version file reflects last successful step).
|
|
186
|
+
*
|
|
187
|
+
* Returns { migrated: bool, backupPath: string|null, error: Error|null }
|
|
188
|
+
*/
|
|
189
|
+
function runMigrations(workspace) {
|
|
190
|
+
if (!fs.existsSync(workspace)) {
|
|
191
|
+
return { migrated: false, backupPath: null, error: null };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const { needsMigration, currentVersion, targetVersion } = checkVersion(workspace);
|
|
195
|
+
|
|
196
|
+
if (!needsMigration) {
|
|
197
|
+
return { migrated: false, backupPath: null, error: null };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const pending = MIGRATIONS.filter(
|
|
201
|
+
(m) => m.from >= currentVersion && m.to <= targetVersion
|
|
202
|
+
).sort((a, b) => a.from - b.from);
|
|
203
|
+
|
|
204
|
+
if (pending.length === 0) {
|
|
205
|
+
// No migration steps defined yet β just bump the version
|
|
206
|
+
writeDataVersion(workspace, targetVersion);
|
|
207
|
+
return { migrated: true, backupPath: null, error: null };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log(`\nπ knight-os: workspace needs upgrade (v${currentVersion} β v${targetVersion})`);
|
|
211
|
+
|
|
212
|
+
// Backup before touching anything
|
|
213
|
+
let backupPath = null;
|
|
214
|
+
try {
|
|
215
|
+
backupPath = backupWorkspace(workspace);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return {
|
|
218
|
+
migrated: false,
|
|
219
|
+
backupPath: null,
|
|
220
|
+
error: new Error(`Backup failed, aborting migration: ${err.message}`),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Run each pending migration
|
|
225
|
+
for (const migration of pending) {
|
|
226
|
+
console.log(` βοΈ Migration ${migration.from}β${migration.to}: ${migration.desc}`);
|
|
227
|
+
try {
|
|
228
|
+
migration.run(workspace);
|
|
229
|
+
writeDataVersion(workspace, migration.to);
|
|
230
|
+
console.log(` β
Done.`);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
return {
|
|
233
|
+
migrated: false,
|
|
234
|
+
backupPath,
|
|
235
|
+
error: new Error(
|
|
236
|
+
`Migration ${migration.from}β${migration.to} failed: ${err.message}\n` +
|
|
237
|
+
`Your data is backed up at: ${backupPath}`
|
|
238
|
+
),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log(`\nβ
Workspace upgraded to v${targetVersion}. Backup kept at:\n ${backupPath}\n`);
|
|
244
|
+
return { migrated: true, backupPath, error: null };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
248
|
+
// Template refresh (for `knight upgrade` command)
|
|
249
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Refresh non-protected template files in the workspace.
|
|
253
|
+
* Protected files (SOUL/MEMORY/USER/REDLINES) are always skipped.
|
|
254
|
+
* For all other files: only write if the file doesn't exist yet (safe default).
|
|
255
|
+
* Pass { force: true } to overwrite non-protected existing files.
|
|
256
|
+
*
|
|
257
|
+
* Returns { added: string[], skipped: string[] }
|
|
258
|
+
*/
|
|
259
|
+
function refreshTemplates(workspace, templatesDir, opts) {
|
|
260
|
+
opts = opts || {};
|
|
261
|
+
const added = [];
|
|
262
|
+
const skipped = [];
|
|
263
|
+
|
|
264
|
+
function walk(dir, base) {
|
|
265
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
const relPath = path.relative(base, path.join(dir, entry.name));
|
|
268
|
+
if (entry.isDirectory()) {
|
|
269
|
+
walk(path.join(dir, entry.name), base);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const isRoot = !relPath.includes(path.sep);
|
|
273
|
+
const isProtected = isRoot && PROTECTED_FILES.includes(entry.name);
|
|
274
|
+
if (isProtected) {
|
|
275
|
+
skipped.push(relPath + ' (protected)');
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const dest = path.join(workspace, relPath);
|
|
279
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
280
|
+
if (!fs.existsSync(dest) || opts.force) {
|
|
281
|
+
fs.copyFileSync(path.join(dir, entry.name), dest);
|
|
282
|
+
added.push(relPath);
|
|
283
|
+
} else {
|
|
284
|
+
skipped.push(relPath);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
walk(templatesDir, templatesDir);
|
|
290
|
+
return { added, skipped };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
294
|
+
// Exports
|
|
295
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
CURRENT_DATA_VERSION,
|
|
299
|
+
PROTECTED_FILES,
|
|
300
|
+
readDataVersion,
|
|
301
|
+
writeDataVersion,
|
|
302
|
+
backupWorkspace,
|
|
303
|
+
checkVersion,
|
|
304
|
+
runMigrations,
|
|
305
|
+
refreshTemplates,
|
|
306
|
+
};
|
package/src/setup.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const readline = require('readline');
|
|
7
7
|
const { execSync, spawnSync } = require('child_process');
|
|
8
|
+
const { runMigrations, writeDataVersion, CURRENT_DATA_VERSION } = require('./migrate');
|
|
8
9
|
|
|
9
10
|
const DEFAULT_WORKSPACE = path.join(os.homedir(), '.openclaw', 'workspace');
|
|
10
11
|
|
|
@@ -214,14 +215,42 @@ async function setup() {
|
|
|
214
215
|
const workspaceExists = fs.existsSync(workspace);
|
|
215
216
|
const hasCoreFiles = workspaceExists && fs.existsSync(path.join(workspace, 'AGENTS.md'));
|
|
216
217
|
|
|
217
|
-
|
|
218
|
+
// Files that contain user's personal memory/identity β never overwrite by default
|
|
219
|
+
const PROTECTED_FILES = ['SOUL.md', 'MEMORY.md', 'USER.md', 'REDLINES.md'];
|
|
220
|
+
const hasPersonalMemory = hasCoreFiles && PROTECTED_FILES.some(
|
|
221
|
+
f => fs.existsSync(path.join(workspace, f))
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
let overwrite = false;
|
|
225
|
+
let overwriteProtected = false;
|
|
226
|
+
|
|
218
227
|
if (hasCoreFiles) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
console.log('
|
|
228
|
+
if (hasPersonalMemory) {
|
|
229
|
+
console.log(`\nβ οΈ Existing OpenClaw workspace detected at: ${workspace}`);
|
|
230
|
+
console.log(' Protected files found: SOUL.md, MEMORY.md, USER.md, REDLINES.md');
|
|
231
|
+
console.log(' These contain your personal memory and identity.\n');
|
|
232
|
+
console.log(' Knight OS will add missing files and update scripts/templates.');
|
|
233
|
+
console.log(' Your existing memory files will NOT be touched.\n');
|
|
234
|
+
const answer = await ask(rl, 'Also overwrite protected files? (y/N)', 'N');
|
|
235
|
+
overwriteProtected = answer.toLowerCase().startsWith('y');
|
|
236
|
+
if (overwriteProtected) {
|
|
237
|
+
console.log('\n β οΈ Protected files WILL be overwritten. Existing content will be lost.');
|
|
238
|
+
} else {
|
|
239
|
+
console.log('\n β
Protected files preserved. Only missing/new files will be added.');
|
|
240
|
+
}
|
|
241
|
+
overwrite = true; // always write non-protected files (scripts, AGENTS.md, HEARTBEAT.md, PROJECTS.md)
|
|
242
|
+
} else {
|
|
243
|
+
console.log(`\nβ οΈ Workspace already exists at: ${workspace}`);
|
|
244
|
+
const answer = await ask(rl, 'Overwrite existing files? (y/N)', 'N');
|
|
245
|
+
overwrite = answer.toLowerCase().startsWith('y');
|
|
246
|
+
overwriteProtected = overwrite;
|
|
247
|
+
if (!overwrite) {
|
|
248
|
+
console.log('\nSkipping template write. Continuing with other setup steps...');
|
|
249
|
+
}
|
|
224
250
|
}
|
|
251
|
+
} else {
|
|
252
|
+
overwrite = true;
|
|
253
|
+
overwriteProtected = true;
|
|
225
254
|
}
|
|
226
255
|
|
|
227
256
|
// Create required dirs
|
|
@@ -297,15 +326,32 @@ async function setup() {
|
|
|
297
326
|
.replace(/\{\{CHANNEL\}\}/g, 'direct');
|
|
298
327
|
}
|
|
299
328
|
|
|
300
|
-
function copyTemplates(srcDir, destDir) {
|
|
329
|
+
function copyTemplates(srcDir, destDir, isRoot) {
|
|
301
330
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
302
331
|
for (const entry of entries) {
|
|
303
332
|
const src = path.join(srcDir, entry.name);
|
|
304
333
|
const dest = path.join(destDir, entry.name);
|
|
305
334
|
if (entry.isDirectory()) {
|
|
306
335
|
fs.mkdirSync(dest, { recursive: true });
|
|
307
|
-
copyTemplates(src, dest);
|
|
336
|
+
copyTemplates(src, dest, false);
|
|
308
337
|
} else {
|
|
338
|
+
// Check if this is a protected file (root level only)
|
|
339
|
+
const isProtected = isRoot && PROTECTED_FILES.includes(entry.name);
|
|
340
|
+
if (isProtected && !overwriteProtected) {
|
|
341
|
+
if (!fs.existsSync(dest)) {
|
|
342
|
+
// File doesn't exist yet β safe to create
|
|
343
|
+
try {
|
|
344
|
+
const content = fs.readFileSync(src, 'utf-8');
|
|
345
|
+
fs.writeFileSync(dest, fillTemplate(content, vars), 'utf-8');
|
|
346
|
+
console.log(` β
${path.relative(workspace, dest)}`);
|
|
347
|
+
} catch (e) {
|
|
348
|
+
console.log(` β οΈ ${path.relative(workspace, dest)}: ${e.message}`);
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
console.log(` π ${path.relative(workspace, dest)} (protected, skipped)`);
|
|
352
|
+
}
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
309
355
|
try {
|
|
310
356
|
const content = fs.readFileSync(src, 'utf-8');
|
|
311
357
|
fs.writeFileSync(dest, fillTemplate(content, vars), 'utf-8');
|
|
@@ -317,7 +363,7 @@ async function setup() {
|
|
|
317
363
|
}
|
|
318
364
|
}
|
|
319
365
|
|
|
320
|
-
copyTemplates(TEMPLATES_DIR, workspace);
|
|
366
|
+
copyTemplates(TEMPLATES_DIR, workspace, true);
|
|
321
367
|
} else {
|
|
322
368
|
console.log(' βοΈ Templates skipped (existing files preserved)');
|
|
323
369
|
}
|
|
@@ -402,6 +448,9 @@ async function setup() {
|
|
|
402
448
|
}
|
|
403
449
|
}
|
|
404
450
|
|
|
451
|
+
// Record the data version so future upgrades know where to start
|
|
452
|
+
writeDataVersion(workspace, CURRENT_DATA_VERSION);
|
|
453
|
+
|
|
405
454
|
console.log(`\n${separator}`);
|
|
406
455
|
console.log('β
Knight OS setup complete!\n');
|
|
407
456
|
console.log(`Workspace: ${workspace}`);
|
package/templates/AGENTS.md
CHANGED
|
@@ -35,7 +35,30 @@ On session start, read files in this order:
|
|
|
35
35
|
6. `memory/ai-patterns.md` (load own behavior rules)
|
|
36
36
|
7. `USER.md` (load user profile)
|
|
37
37
|
8. `TOOLS.md` (load available tools)
|
|
38
|
-
9. `
|
|
38
|
+
9. `memory/YYYY-MM-DD.md` for today + yesterday (load recent context; skip if file doesn't exist)
|
|
39
|
+
10. `PROJECTS.md` (load project index β on-demand per project)
|
|
40
|
+
|
|
41
|
+
> **Why daily logs?** Without reading recent logs, the AI starts each session with no memory of what happened yesterday. Always load today + yesterday at boot.
|
|
42
|
+
|
|
43
|
+
## On-Demand Loading Trigger Table
|
|
44
|
+
|
|
45
|
+
Do NOT load everything at boot. Load these files only when the matching situation arises:
|
|
46
|
+
|
|
47
|
+
| Trigger | Load |
|
|
48
|
+
|---------|------|
|
|
49
|
+
| Replying to a message / adjusting tone | `memory/ai-patterns.md` chat section |
|
|
50
|
+
| Before executing a task | `memory/ai-patterns.md` exec section |
|
|
51
|
+
| User mentions a project by name | `memory/projects/<name>/main.md` |
|
|
52
|
+
| Executing a task tied to a project | `memory/projects/<name>/main.md` + latest log |
|
|
53
|
+
| Heartbeat / daily review | `PROJECTS.md` index only (no main.md) |
|
|
54
|
+
| Writing daily report | Update main.md β Current Sprint with today's progress |
|
|
55
|
+
| Writing to memory / log / daily report | Check `memory/ai-patterns.md` memory section |
|
|
56
|
+
| Received group message / someone @-mentioned | group handling rules |
|
|
57
|
+
| Involves code / development / PR | `memory/ai-patterns.md` code section |
|
|
58
|
+
| Using scripts / external tools | `memory/ai-patterns.md` tool section |
|
|
59
|
+
| Writing copy / articles / presentations | `memory/user-patterns.md` writing style section |}
|
|
60
|
+
|
|
61
|
+
> **Principle:** Static identity + rules β system prompt (always present). Long-term memory β load at session start. Project details + situational rules β lazy-load on demand. Per-turn context β conversation history only.
|
|
39
62
|
|
|
40
63
|
## Memory Structure Quick Reference
|
|
41
64
|
|
package/templates/PROJECTS.md
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
# PROJECTS.md β {{AI_NAME}} Project Overview
|
|
2
2
|
|
|
3
|
-
> Active projects index.
|
|
4
|
-
>
|
|
3
|
+
> Active projects index. Load this file at every session start β keep it short (target: under 40 lines).
|
|
4
|
+
> Full context lives in `memory/projects/<name>/main.md` β load on demand when the project is discussed.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Active Projects
|
|
9
9
|
|
|
10
|
-
|
|
|
11
|
-
|
|
12
|
-
|
|
|
10
|
+
| Name | Status | Priority | One-liner |
|
|
11
|
+
|------|--------|----------|-----------|
|
|
12
|
+
| _(add your first project)_ | π’ | β | _(what is this?)_ |
|
|
13
|
+
|
|
14
|
+
Status: π’ Active / π‘ On Hold / π΄ Blocked / β
Done
|
|
13
15
|
|
|
14
16
|
---
|
|
15
17
|
|
|
16
|
-
##
|
|
18
|
+
## Loading Rules
|
|
19
|
+
|
|
20
|
+
{{AI_NAME}} follows these rules for project context:
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
| When | Do |
|
|
23
|
+
|------|----|
|
|
24
|
+
| User mentions a project name | Load `memory/projects/<name>/main.md` |
|
|
25
|
+
| Executing a task related to a project | Load main.md + most recent project log |
|
|
26
|
+
| Heartbeat / daily review | Scan PROJECTS.md index only (no main.md) |
|
|
27
|
+
| Writing daily report | Update main.md β Current Sprint section with today's progress |
|
|
28
|
+
| Project not mentioned in session | Do NOT load main.md (save tokens) |
|
|
21
29
|
|
|
22
30
|
---
|
|
23
31
|
|
|
@@ -32,8 +40,8 @@ _(Move completed or abandoned projects here)_
|
|
|
32
40
|
```
|
|
33
41
|
memory/projects/
|
|
34
42
|
βββ <project-name>/
|
|
35
|
-
β βββ main.md #
|
|
36
|
-
β βββ logs/ #
|
|
43
|
+
β βββ main.md # Project "workbench" β goals, current sprint, blockers, decisions
|
|
44
|
+
β βββ logs/ # Deep history β load only when reviewing past decisions
|
|
37
45
|
```
|
|
38
46
|
|
|
39
47
|
### main.md template
|
|
@@ -43,18 +51,29 @@ memory/projects/
|
|
|
43
51
|
|
|
44
52
|
**Status:** π’ Active
|
|
45
53
|
**Started:** YYYY-MM-DD
|
|
46
|
-
**Goal:** One sentence.
|
|
54
|
+
**Goal:** One sentence. What does success look like?
|
|
47
55
|
|
|
48
|
-
##
|
|
49
|
-
[
|
|
56
|
+
## Current Sprint / This Week
|
|
57
|
+
- [ ] Task 1
|
|
58
|
+
- [ ] Task 2
|
|
59
|
+
_(Update this section at the end of each working session)_
|
|
60
|
+
|
|
61
|
+
## Open Questions / Blockers
|
|
62
|
+
- [YYYY-MM-DD] Question or blocker β owner or resolution
|
|
63
|
+
|
|
64
|
+
## Next Actions (Top 3)
|
|
65
|
+
1.
|
|
66
|
+
2.
|
|
67
|
+
3.
|
|
50
68
|
|
|
51
69
|
## Key Decisions
|
|
52
70
|
- YYYY-MM-DD: [Decision and rationale]
|
|
53
71
|
|
|
54
|
-
##
|
|
55
|
-
|
|
56
|
-
- [ ] M2: [Description]
|
|
72
|
+
## Context
|
|
73
|
+
[What is this project? Why does it matter? Who is it for?]
|
|
57
74
|
|
|
58
|
-
## Notes
|
|
59
|
-
[Anything
|
|
75
|
+
## Notes for {{AI_NAME}}
|
|
76
|
+
[Anything the AI must remember between sessions β constraints, preferences, gotchas]
|
|
60
77
|
```
|
|
78
|
+
|
|
79
|
+
> Keep main.md under 100 lines. If it grows longer, move older decisions/context to `logs/archive.md`.
|