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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) {{YEAR}} {{AUTHOR}}
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# knight-os
|
|
2
|
+
|
|
3
|
+
AI companion OS for OpenClaw — memory, reflection, and identity framework.
|
|
4
|
+
|
|
5
|
+
Give your OpenClaw AI a name, a personality, and the ability to learn from experience.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
Install [OpenClaw](https://github.com/openclaw/openclaw) first:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g openclaw
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g knight-os
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
knight setup
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The setup wizard will:
|
|
28
|
+
|
|
29
|
+
1. Verify OpenClaw is installed
|
|
30
|
+
2. Ask for your AI's name, your name, and timezone
|
|
31
|
+
3. Write all framework files into your OpenClaw workspace
|
|
32
|
+
4. Optionally configure Telegram notifications
|
|
33
|
+
5. Register the Heartbeat scheduler (macOS/Linux)
|
|
34
|
+
|
|
35
|
+
After setup, start chatting via OpenClaw:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
openclaw chat
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Custom workspace path
|
|
42
|
+
|
|
43
|
+
If your OpenClaw workspace is not at the default `~/.openclaw/workspace`, enter your path when prompted:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Workspace directory [~/.openclaw/workspace]: /workspace/projects
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Knight OS will write all files there and OpenClaw will pick them up automatically.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## How Memory Works
|
|
54
|
+
|
|
55
|
+
This is the core of what knight-os adds. Your AI learns from experience through a simple loop:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
You finish a task
|
|
59
|
+
↓
|
|
60
|
+
write-reflection.py → memory/reflections/YYYY-MM-DD.jsonl
|
|
61
|
+
↓
|
|
62
|
+
Heartbeat runs → reflection-analyzer.py → candidate rules extracted
|
|
63
|
+
↓
|
|
64
|
+
You confirm → rules written to memory/ai-patterns.md
|
|
65
|
+
↓
|
|
66
|
+
Next session → ai-patterns.md loaded in system prompt → AI behaves better
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**In practice:**
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# After completing any task, run:
|
|
73
|
+
python3 ~/.openclaw/workspace/scripts/write-reflection.py \
|
|
74
|
+
--context "Deployed new feature" \
|
|
75
|
+
--what_worked "Clear requirements helped" \
|
|
76
|
+
--what_failed "Forgot to update tests" \
|
|
77
|
+
--next_time "Write tests first, then implement"
|
|
78
|
+
|
|
79
|
+
# Every 6 hours (automatic), the heartbeat:
|
|
80
|
+
# 1. Scans reflections for repeated failure patterns
|
|
81
|
+
# 2. Extracts candidate rules
|
|
82
|
+
# 3. Notifies you (if Telegram configured)
|
|
83
|
+
|
|
84
|
+
# You review and add confirmed rules to:
|
|
85
|
+
# ~/.openclaw/workspace/memory/ai-patterns.md
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Over time, `ai-patterns.md` accumulates rules your AI uses automatically in every session.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Memory File Structure
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
~/.openclaw/workspace/
|
|
96
|
+
├── SOUL.md # AI identity and personality
|
|
97
|
+
├── AGENTS.md # Boot sequence, behavior norms, script reference
|
|
98
|
+
├── MEMORY.md # Long-term memory index
|
|
99
|
+
├── REDLINES.md # Safety boundaries
|
|
100
|
+
├── USER.md # Your profile and preferences
|
|
101
|
+
├── TOOLS.md # Tool reference and credentials map
|
|
102
|
+
├── PROJECTS.md # Active project index
|
|
103
|
+
├── HEARTBEAT.md # Heartbeat task configuration
|
|
104
|
+
├── memory/
|
|
105
|
+
│ ├── ai-patterns.md # Learned behavior rules (grows over time)
|
|
106
|
+
│ ├── user-patterns.md # Observed user behavior
|
|
107
|
+
│ ├── reflections/ # Task reflection logs (JSONL)
|
|
108
|
+
│ ├── logs/ # Session logs
|
|
109
|
+
│ └── projects/<name>/ # Per-project context
|
|
110
|
+
└── scripts/
|
|
111
|
+
├── write-reflection.py # Log a reflection after task completion
|
|
112
|
+
├── reflection-analyzer.py # Extract rules from reflection patterns
|
|
113
|
+
├── heartbeat.py # Periodic maintenance tasks
|
|
114
|
+
├── compress-memory.py # Archive old logs
|
|
115
|
+
└── knight-status.py # Workspace health check
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Commands
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
knight setup # Configure Knight OS (requires OpenClaw installed)
|
|
124
|
+
knight init # Initialize workspace standalone (no OpenClaw check)
|
|
125
|
+
knight chat # Interactive AI chat (Anthropic API directly)
|
|
126
|
+
knight status # Check workspace file status
|
|
127
|
+
knight version # Show version
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Standalone chat (`knight chat`)
|
|
131
|
+
|
|
132
|
+
If you want to chat without OpenClaw, you can use the built-in chat command.
|
|
133
|
+
Requires `ANTHROPIC_API_KEY` in your workspace `.env`.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Runtime Scripts
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Log a reflection after completing a task
|
|
141
|
+
python3 scripts/write-reflection.py \
|
|
142
|
+
--context "Task title" \
|
|
143
|
+
--what_worked "What went well" \
|
|
144
|
+
--what_failed "What did not work" \
|
|
145
|
+
--next_time "How to improve"
|
|
146
|
+
|
|
147
|
+
# Analyze reflection patterns (run by heartbeat automatically)
|
|
148
|
+
python3 scripts/reflection-analyzer.py --min-count 2
|
|
149
|
+
|
|
150
|
+
# Check workspace health
|
|
151
|
+
python3 scripts/knight-status.py
|
|
152
|
+
|
|
153
|
+
# Archive old logs
|
|
154
|
+
python3 scripts/compress-memory.py --execute
|
|
155
|
+
|
|
156
|
+
# Run heartbeat manually
|
|
157
|
+
python3 scripts/heartbeat.py
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Core Principles
|
|
163
|
+
|
|
164
|
+
### Framework & Content Separation
|
|
165
|
+
|
|
166
|
+
knight-os provides the **structure** — the files, the rules, the mechanisms.
|
|
167
|
+
You provide the **content** — your AI's personality, your preferences, your specific tools.
|
|
168
|
+
|
|
169
|
+
### Learning from Feedback
|
|
170
|
+
|
|
171
|
+
The system evolves. Corrections become rules (`ai-patterns.md`), observations become understanding (`user-patterns.md`), decisions become memory (`MEMORY.md`). Nothing is static.
|
|
172
|
+
|
|
173
|
+
### Memory Layering
|
|
174
|
+
|
|
175
|
+
| Layer | Location | When promoted |
|
|
176
|
+
|-------|----------|--------------|
|
|
177
|
+
| Working | In-context (current session) | — |
|
|
178
|
+
| Short-term | `memory/YYYY-MM-DD.md` | End of session |
|
|
179
|
+
| Long-term | `MEMORY.md` | Pattern repeats 3+ times or user confirms |
|
|
180
|
+
| Patterns | `memory/ai-patterns.md` | After reflection analysis + confirmation |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Contributing
|
|
185
|
+
|
|
186
|
+
Contributions welcome. Keep templates generic — no personal data or tool credentials.
|
|
187
|
+
|
|
188
|
+
1. Fork this repository
|
|
189
|
+
2. Create a feature branch
|
|
190
|
+
3. Submit a pull request
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
package/bin/knight.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const { loadConfig, resolveWorkspace } = require('../src/config');
|
|
9
|
+
const { chat } = require('../src/chat');
|
|
10
|
+
const { setup } = require('../src/setup');
|
|
11
|
+
|
|
12
|
+
const VERSION = '0.1.0';
|
|
13
|
+
const DEFAULT_WORKSPACE = path.join(process.env.HOME || '~', '.openclaw', 'workspace');
|
|
14
|
+
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
15
|
+
|
|
16
|
+
function ask(rl, question) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getAllTemplateFiles(dir, base) {
|
|
23
|
+
base = base || dir;
|
|
24
|
+
let results = [];
|
|
25
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const fullPath = path.join(dir, entry.name);
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
results = results.concat(getAllTemplateFiles(fullPath, base));
|
|
30
|
+
} else {
|
|
31
|
+
results.push(path.relative(base, fullPath));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function replacePlaceholders(content, vars) {
|
|
38
|
+
return content
|
|
39
|
+
.replace(/\{\{AI_NAME\}\}/g, vars.aiName)
|
|
40
|
+
.replace(/\{\{USER_NAME\}\}/g, vars.userName)
|
|
41
|
+
.replace(/\{\{TIMEZONE\}\}/g, vars.timezone)
|
|
42
|
+
.replace(/\{\{LANGUAGE\}\}/g, vars.language || 'en')
|
|
43
|
+
.replace(/\{\{CHANNEL\}\}/g, vars.channel || 'direct');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function commandInit() {
|
|
47
|
+
const rl = readline.createInterface({
|
|
48
|
+
input: process.stdin,
|
|
49
|
+
output: process.stdout,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log('\n🐉 Knight OS — OpenClaw Workspace Initializer\n');
|
|
53
|
+
|
|
54
|
+
const aiName = await ask(rl, "Your AI companion's name? (e.g. Aria, Nova, Kai): ");
|
|
55
|
+
if (!aiName) {
|
|
56
|
+
console.log('AI name is required.');
|
|
57
|
+
rl.close();
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const userName = await ask(rl, 'Your name? (used to personalize the workspace): ');
|
|
62
|
+
if (!userName) {
|
|
63
|
+
console.log('Your name is required.');
|
|
64
|
+
rl.close();
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const timezone = await ask(rl, 'Your timezone? (e.g. Asia/Tokyo, America/New_York): ');
|
|
69
|
+
if (!timezone) {
|
|
70
|
+
console.log('Timezone is required.');
|
|
71
|
+
rl.close();
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const workspaceInput = await ask(rl, `Workspace directory? [${DEFAULT_WORKSPACE}]: `);
|
|
76
|
+
const workspace = workspaceInput || DEFAULT_WORKSPACE;
|
|
77
|
+
|
|
78
|
+
const apiKeyInput = await ask(
|
|
79
|
+
rl,
|
|
80
|
+
'Anthropic API key? (starts with sk-ant-, leave blank to skip): '
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const modelInput = await ask(rl, 'Default model? [claude-sonnet-4-5]: ');
|
|
84
|
+
const modelName = modelInput || 'claude-sonnet-4-5';
|
|
85
|
+
|
|
86
|
+
console.log('\n--- Preview ---');
|
|
87
|
+
console.log(` AI Name: ${aiName}`);
|
|
88
|
+
console.log(` User Name: ${userName}`);
|
|
89
|
+
console.log(` Timezone: ${timezone}`);
|
|
90
|
+
console.log(` Workspace: ${workspace}`);
|
|
91
|
+
console.log(` Model: ${modelName}`);
|
|
92
|
+
if (apiKeyInput) console.log(` API Key: ${apiKeyInput.slice(0, 12)}...`);
|
|
93
|
+
console.log('---------------\n');
|
|
94
|
+
|
|
95
|
+
const confirm = await ask(rl, 'Press Enter to confirm, or type "no" to cancel: ');
|
|
96
|
+
if (confirm.toLowerCase() === 'no') {
|
|
97
|
+
console.log('Cancelled.');
|
|
98
|
+
rl.close();
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
rl.close();
|
|
103
|
+
|
|
104
|
+
const vars = { aiName, userName, timezone };
|
|
105
|
+
const templateFiles = getAllTemplateFiles(TEMPLATES_DIR);
|
|
106
|
+
|
|
107
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
108
|
+
|
|
109
|
+
let written = 0;
|
|
110
|
+
let skipped = 0;
|
|
111
|
+
|
|
112
|
+
for (const relPath of templateFiles) {
|
|
113
|
+
const srcPath = path.join(TEMPLATES_DIR, relPath);
|
|
114
|
+
const destPath = path.join(workspace, relPath);
|
|
115
|
+
const destDir = path.dirname(destPath);
|
|
116
|
+
|
|
117
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
118
|
+
|
|
119
|
+
if (fs.existsSync(destPath)) {
|
|
120
|
+
const rlConfirm = readline.createInterface({
|
|
121
|
+
input: process.stdin,
|
|
122
|
+
output: process.stdout,
|
|
123
|
+
});
|
|
124
|
+
const overwrite = await ask(rlConfirm, ` File exists: ${relPath} — overwrite? [y/N]: `);
|
|
125
|
+
rlConfirm.close();
|
|
126
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
127
|
+
skipped++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const content = fs.readFileSync(srcPath, 'utf-8');
|
|
133
|
+
const processed = replacePlaceholders(content, vars);
|
|
134
|
+
fs.writeFileSync(destPath, processed, 'utf-8');
|
|
135
|
+
written++;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (apiKeyInput) {
|
|
139
|
+
const envPath = path.join(workspace, '.env');
|
|
140
|
+
const envLine = `ANTHROPIC_API_KEY=${apiKeyInput}\n`;
|
|
141
|
+
if (fs.existsSync(envPath)) {
|
|
142
|
+
fs.appendFileSync(envPath, envLine, 'utf-8');
|
|
143
|
+
} else {
|
|
144
|
+
fs.writeFileSync(envPath, envLine, 'utf-8');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const configPath = path.join(workspace, 'knight.config.json');
|
|
149
|
+
let existingConfig = {};
|
|
150
|
+
if (fs.existsSync(configPath)) {
|
|
151
|
+
try {
|
|
152
|
+
existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
existingConfig.model = {
|
|
156
|
+
provider: 'anthropic',
|
|
157
|
+
name: modelName,
|
|
158
|
+
max_tokens: 8096,
|
|
159
|
+
system_prompt_files: ['SOUL.md', 'AGENTS.md', 'MEMORY.md', 'REDLINES.md'],
|
|
160
|
+
};
|
|
161
|
+
fs.writeFileSync(configPath, JSON.stringify(existingConfig, null, 2) + '\n', 'utf-8');
|
|
162
|
+
|
|
163
|
+
console.log(`\n✅ Workspace initialized at ${workspace}`);
|
|
164
|
+
console.log(` ${written} file(s) written, ${skipped} skipped.\n`);
|
|
165
|
+
console.log('Next steps:');
|
|
166
|
+
console.log(` 1. Review your workspace files in ${workspace}`);
|
|
167
|
+
console.log(' 2. Customize SOUL.md and USER.md to match your preferences');
|
|
168
|
+
console.log(' 3. Run \`knight chat\` to start talking to your AI companion');
|
|
169
|
+
console.log('');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function commandStatus() {
|
|
173
|
+
const workspace = DEFAULT_WORKSPACE;
|
|
174
|
+
const requiredFiles = [
|
|
175
|
+
'AGENTS.md',
|
|
176
|
+
'SOUL.md',
|
|
177
|
+
'MEMORY.md',
|
|
178
|
+
'HEARTBEAT.md',
|
|
179
|
+
'REDLINES.md',
|
|
180
|
+
'USER.md',
|
|
181
|
+
'TOOLS.md',
|
|
182
|
+
'memory/ai-patterns.md',
|
|
183
|
+
'memory/user-patterns.md',
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
console.log(`\n📋 Knight OS — Workspace Status`);
|
|
187
|
+
console.log(` Directory: ${workspace}\n`);
|
|
188
|
+
|
|
189
|
+
if (!fs.existsSync(workspace)) {
|
|
190
|
+
console.log(' ❌ Workspace directory does not exist.');
|
|
191
|
+
console.log(' Run "knight init" to create it.\n');
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let ok = 0;
|
|
196
|
+
let missing = 0;
|
|
197
|
+
|
|
198
|
+
for (const file of requiredFiles) {
|
|
199
|
+
const fullPath = path.join(workspace, file);
|
|
200
|
+
if (fs.existsSync(fullPath)) {
|
|
201
|
+
console.log(` ✅ ${file}`);
|
|
202
|
+
ok++;
|
|
203
|
+
} else {
|
|
204
|
+
console.log(` ❌ ${file}`);
|
|
205
|
+
missing++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log(`\n Result: ${ok} present, ${missing} missing.\n`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function commandVersion() {
|
|
213
|
+
console.log(`knight-os v${VERSION}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function commandChat() {
|
|
217
|
+
const config = loadConfig();
|
|
218
|
+
const workspace = resolveWorkspace(config);
|
|
219
|
+
await chat(config, workspace);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const command = process.argv[2];
|
|
223
|
+
|
|
224
|
+
switch (command) {
|
|
225
|
+
case 'setup':
|
|
226
|
+
setup();
|
|
227
|
+
break;
|
|
228
|
+
case 'init':
|
|
229
|
+
commandInit();
|
|
230
|
+
break;
|
|
231
|
+
case 'chat':
|
|
232
|
+
commandChat();
|
|
233
|
+
break;
|
|
234
|
+
case 'status':
|
|
235
|
+
commandStatus();
|
|
236
|
+
break;
|
|
237
|
+
case 'version':
|
|
238
|
+
case '--version':
|
|
239
|
+
case '-v':
|
|
240
|
+
commandVersion();
|
|
241
|
+
break;
|
|
242
|
+
default:
|
|
243
|
+
console.log(`knight-os v${VERSION}`);
|
|
244
|
+
console.log('\nUsage: knight <command>\n');
|
|
245
|
+
console.log('Commands:');
|
|
246
|
+
console.log(' setup Configure Knight OS for an existing OpenClaw installation');
|
|
247
|
+
console.log(' init Initialize a new workspace (standalone, no OpenClaw required)');
|
|
248
|
+
console.log(' chat Start interactive AI chat session');
|
|
249
|
+
console.log(' status Check workspace file status');
|
|
250
|
+
console.log(' version Show version number');
|
|
251
|
+
console.log('');
|
|
252
|
+
break;
|
|
253
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "knight-os",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI companion OS for OpenClaw — memory, reflection, and identity framework",
|
|
5
|
+
"main": "bin/knight.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"knight": "./bin/knight.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node bin/knight.js version"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"openclaw",
|
|
14
|
+
"ai",
|
|
15
|
+
"workspace",
|
|
16
|
+
"agent",
|
|
17
|
+
"memory",
|
|
18
|
+
"reflection",
|
|
19
|
+
"identity",
|
|
20
|
+
"llm"
|
|
21
|
+
],
|
|
22
|
+
"author": "Steve Wang <iloveopt@gmail.com>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/iloveopt/knight-os.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/iloveopt/knight-os#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/iloveopt/knight-os/issues"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=16.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin/",
|
|
37
|
+
"src/",
|
|
38
|
+
"scripts/",
|
|
39
|
+
"templates/",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
compress-memory.py — Archive and compress old log files.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python3 scripts/compress-memory.py # Dry-run by default (report only)
|
|
7
|
+
python3 scripts/compress-memory.py --dry-run # Explicit dry-run
|
|
8
|
+
python3 scripts/compress-memory.py --execute # Actually move files to archive
|
|
9
|
+
python3 scripts/compress-memory.py --days 14 # Keep only last 14 days (default: 30)
|
|
10
|
+
python3 scripts/compress-memory.py --threshold 200 # Line threshold (default: 500)
|
|
11
|
+
|
|
12
|
+
Scans {workspace}/memory/logs/ and archives files older than --days into memory/logs/archive/.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import argparse
|
|
19
|
+
import shutil
|
|
20
|
+
from datetime import datetime, timezone, timedelta
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_config():
|
|
25
|
+
config_paths = [
|
|
26
|
+
Path.cwd() / "knight.config.json",
|
|
27
|
+
Path.home() / ".knight" / "config.json",
|
|
28
|
+
]
|
|
29
|
+
for p in config_paths:
|
|
30
|
+
if p.exists():
|
|
31
|
+
try:
|
|
32
|
+
return json.loads(p.read_text())
|
|
33
|
+
except (json.JSONDecodeError, OSError):
|
|
34
|
+
pass
|
|
35
|
+
return {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_workspace(config):
|
|
39
|
+
ws = config.get("workspace", "~/.openclaw/workspace")
|
|
40
|
+
return Path(ws).expanduser()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def scan_logs(logs_dir: Path):
|
|
44
|
+
"""Scan log files and return stats."""
|
|
45
|
+
files = []
|
|
46
|
+
total_lines = 0
|
|
47
|
+
total_size = 0
|
|
48
|
+
|
|
49
|
+
for f in sorted(logs_dir.iterdir()):
|
|
50
|
+
if f.is_file() and f.suffix in (".md", ".log", ".jsonl", ".txt"):
|
|
51
|
+
size = f.stat().st_size
|
|
52
|
+
mtime = datetime.fromtimestamp(f.stat().st_mtime, tz=timezone.utc)
|
|
53
|
+
try:
|
|
54
|
+
lines = sum(1 for _ in open(f, encoding="utf-8", errors="ignore"))
|
|
55
|
+
except OSError:
|
|
56
|
+
lines = 0
|
|
57
|
+
|
|
58
|
+
files.append({
|
|
59
|
+
"path": f,
|
|
60
|
+
"name": f.name,
|
|
61
|
+
"size": size,
|
|
62
|
+
"lines": lines,
|
|
63
|
+
"mtime": mtime,
|
|
64
|
+
})
|
|
65
|
+
total_lines += lines
|
|
66
|
+
total_size += size
|
|
67
|
+
|
|
68
|
+
return files, total_lines, total_size
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main():
|
|
72
|
+
parser = argparse.ArgumentParser(description="Archive and compress old log files.")
|
|
73
|
+
parser.add_argument("--dry-run", action="store_true", default=True, help="Report only (default)")
|
|
74
|
+
parser.add_argument("--execute", action="store_true", help="Actually archive old files")
|
|
75
|
+
parser.add_argument("--days", type=int, default=30, help="Keep files from last N days (default: 30)")
|
|
76
|
+
parser.add_argument("--threshold", type=int, default=500, help="Line count threshold for warning (default: 500)")
|
|
77
|
+
args = parser.parse_args()
|
|
78
|
+
|
|
79
|
+
if args.execute:
|
|
80
|
+
args.dry_run = False
|
|
81
|
+
|
|
82
|
+
config = load_config()
|
|
83
|
+
workspace = resolve_workspace(config)
|
|
84
|
+
local_cfg = config.get("storage", {}).get("local", {})
|
|
85
|
+
logs_dir = workspace / local_cfg.get("logs_dir", "memory/logs")
|
|
86
|
+
|
|
87
|
+
print(f"[knight] Memory compression — scanning {logs_dir}")
|
|
88
|
+
|
|
89
|
+
if not logs_dir.exists():
|
|
90
|
+
print(f" Logs directory does not exist: {logs_dir}")
|
|
91
|
+
print(" Nothing to compress.")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
files, total_lines, total_size = scan_logs(logs_dir)
|
|
95
|
+
size_mb = total_size / (1024 * 1024)
|
|
96
|
+
|
|
97
|
+
print(f" Found {len(files)} log files")
|
|
98
|
+
print(f" Total: {total_lines} lines, {size_mb:.2f} MB")
|
|
99
|
+
|
|
100
|
+
if total_lines <= args.threshold:
|
|
101
|
+
print(f" Below threshold ({args.threshold} lines) — no compression needed.")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
print(f" Above threshold ({args.threshold} lines) — compression recommended.")
|
|
105
|
+
|
|
106
|
+
cutoff = datetime.now(timezone.utc) - timedelta(days=args.days)
|
|
107
|
+
to_archive = [f for f in files if f["mtime"] < cutoff]
|
|
108
|
+
to_keep = [f for f in files if f["mtime"] >= cutoff]
|
|
109
|
+
|
|
110
|
+
archive_lines = sum(f["lines"] for f in to_archive)
|
|
111
|
+
archive_size = sum(f["size"] for f in to_archive)
|
|
112
|
+
|
|
113
|
+
print(f"\n Archive candidates (older than {args.days} days):")
|
|
114
|
+
print(f" {len(to_archive)} files, {archive_lines} lines, {archive_size / 1024:.1f} KB")
|
|
115
|
+
print(f" Keeping (recent {args.days} days):")
|
|
116
|
+
print(f" {len(to_keep)} files, {sum(f['lines'] for f in to_keep)} lines")
|
|
117
|
+
|
|
118
|
+
if not to_archive:
|
|
119
|
+
print("\n No files old enough to archive.")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if args.dry_run:
|
|
123
|
+
print("\n [dry-run] Files that would be archived:")
|
|
124
|
+
for f in to_archive[:10]:
|
|
125
|
+
print(f" {f['name']} ({f['lines']} lines, {f['mtime'].strftime('%Y-%m-%d')})")
|
|
126
|
+
if len(to_archive) > 10:
|
|
127
|
+
print(f" ... and {len(to_archive) - 10} more")
|
|
128
|
+
print("\n Run with --execute to perform archival.")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
archive_dir = logs_dir / "archive"
|
|
132
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
|
|
134
|
+
moved = 0
|
|
135
|
+
for f in to_archive:
|
|
136
|
+
dest = archive_dir / f["name"]
|
|
137
|
+
if dest.exists():
|
|
138
|
+
dest = archive_dir / f"{f['path'].stem}_{f['mtime'].strftime('%Y%m%d')}{f['path'].suffix}"
|
|
139
|
+
shutil.move(str(f["path"]), str(dest))
|
|
140
|
+
moved += 1
|
|
141
|
+
|
|
142
|
+
print(f"\n Archived {moved} files to {archive_dir}")
|
|
143
|
+
print(f" Freed {archive_lines} lines, {archive_size / 1024:.1f} KB")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
main()
|