knight-os 0.1.1 β†’ 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knight-os",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "AI companion OS for OpenClaw β€” memory, reflection, and identity framework",
5
5
  "main": "bin/knight.js",
6
6
  "bin": {
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
 
@@ -447,6 +448,9 @@ async function setup() {
447
448
  }
448
449
  }
449
450
 
451
+ // Record the data version so future upgrades know where to start
452
+ writeDataVersion(workspace, CURRENT_DATA_VERSION);
453
+
450
454
  console.log(`\n${separator}`);
451
455
  console.log('βœ… Knight OS setup complete!\n');
452
456
  console.log(`Workspace: ${workspace}`);
@@ -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. `PROJECTS.md` (load project index β€” on-demand per project)
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
 
@@ -1,23 +1,31 @@
1
1
  # PROJECTS.md β€” {{AI_NAME}} Project Overview
2
2
 
3
- > Active projects index. Update when starting or closing a project.
4
- > Detailed notes β†’ `memory/projects/<name>/main.md`
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
- | ID | Name | Status | Priority | Started | Note |
11
- |----|------|--------|----------|---------|------|
12
- | β€” | _(add your first project)_ | β€” | β€” | β€” | β€” |
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
- ## How to Use This File
18
+ ## Loading Rules
19
+
20
+ {{AI_NAME}} follows these rules for project context:
17
21
 
18
- - One row per project. Keep it scannable.
19
- - Detail goes in `memory/projects/<name>/main.md`
20
- - Status: 🟒 Active / 🟑 On Hold / πŸ”΄ Blocked / βœ… Done
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 # Full project context (goals, decisions, roadmap)
36
- β”‚ └── logs/ # Session logs specific to this project
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
- ## Context
49
- [What is this? Why does it matter?]
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
- ## Milestones
55
- - [ ] M1: [Description]
56
- - [ ] M2: [Description]
72
+ ## Context
73
+ [What is this project? Why does it matter? Who is it for?]
57
74
 
58
- ## Notes
59
- [Anything {{AI_NAME}} should remember between sessions]
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`.