compass-ai 1.0.0 → 1.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/README.md +207 -0
- package/bin/compass.js +58 -12
- package/migrations/001-initial.js +25 -0
- package/migrations/index.js +316 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Compass
|
|
2
|
+
|
|
3
|
+
**Compass** is an AI chief of staff for startups. It listens to your Telegram conversations, extracts tasks, decisions, blockers, and commitments, stores them in structured files, and posts clear summaries to Slack — automatically.
|
|
4
|
+
|
|
5
|
+
Compass is not a chatbot. It is your execution memory and reporting layer.
|
|
6
|
+
|
|
7
|
+
Built on [OpenClaw](https://openclaw.ai).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What Compass does
|
|
12
|
+
|
|
13
|
+
| Capability | Detail |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Listens to Telegram | Reads group and DM conversations passively |
|
|
16
|
+
| Extracts signal | Tasks, decisions, blockers, commitments, metrics |
|
|
17
|
+
| Maintains memory | Writes structured state to local files |
|
|
18
|
+
| Answers questions | Responds when mentioned directly (`@Compass`) |
|
|
19
|
+
| Posts to Slack | Daily standups and weekly reviews on schedule |
|
|
20
|
+
| Stays quiet | Silent unless mentioned or scheduled |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
| Requirement | Detail |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Linux (Ubuntu 20.04+ / Debian 11+) | Cron scheduling requires Linux |
|
|
29
|
+
| Node.js 18 or higher | Runtime |
|
|
30
|
+
| A Telegram bot token | From [@BotFather](https://t.me/BotFather) on Telegram |
|
|
31
|
+
| Your Telegram user ID | From [@userinfobot](https://t.me/userinfobot) on Telegram |
|
|
32
|
+
| A Slack bot token | From your Slack app settings (starts with `xoxb-`) |
|
|
33
|
+
| An LLM API key | Anthropic or OpenAI |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g compass-ai
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This installs the `compass` CLI and automatically installs OpenClaw as a dependency.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Setup (run once)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
compass init
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You will be asked:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
LLM provider (anthropic / openai) [anthropic]:
|
|
57
|
+
Anthropic API key:
|
|
58
|
+
Telegram bot token:
|
|
59
|
+
Your Telegram user ID:
|
|
60
|
+
Slack bot token:
|
|
61
|
+
Slack report channel [#standup]:
|
|
62
|
+
Daily standup time (HH:MM, 24h) [09:00]:
|
|
63
|
+
Weekly review day (MON-SUN) [MON]:
|
|
64
|
+
Weekly review time (HH:MM, 24h) [08:00]:
|
|
65
|
+
Your name (for Compass memory):
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Press Enter to accept any default shown in brackets.
|
|
69
|
+
|
|
70
|
+
**What `compass init` creates on your machine:**
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
~/.openclaw/
|
|
74
|
+
├── agent.json ← Compass identity
|
|
75
|
+
├── hooks.json ← boot, logger, session memory
|
|
76
|
+
├── channels.json ← Telegram + Slack connection
|
|
77
|
+
├── llm.json ← your LLM provider and key
|
|
78
|
+
└── compass/
|
|
79
|
+
├── boot.md ← Compass system prompt (generated from your answers)
|
|
80
|
+
├── user.md ← your profile and preferences
|
|
81
|
+
├── soul.md ← Compass personality
|
|
82
|
+
├── identity.md ← Compass role definition
|
|
83
|
+
├── tools.md ← allowed tools
|
|
84
|
+
├── skills/ ← extract and report skills
|
|
85
|
+
└── state/
|
|
86
|
+
├── tasks.md
|
|
87
|
+
├── decisions.md
|
|
88
|
+
├── blockers.md
|
|
89
|
+
├── commitments.md
|
|
90
|
+
├── metrics.md
|
|
91
|
+
├── daily-summaries/
|
|
92
|
+
└── weekly-summaries/
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
On Linux, cron jobs are configured automatically for daily and weekly reports.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Start
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
compass start
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Compass connects to Telegram and Slack and runs in the foreground. To run it as a background service, use a process manager like `pm2` or `systemd`.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Commands
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
compass init # First-time setup
|
|
113
|
+
compass start # Start the agent
|
|
114
|
+
compass report --type daily # Trigger a daily standup report now
|
|
115
|
+
compass report --type weekly # Trigger a weekly review report now
|
|
116
|
+
compass # Show help
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Using Compass in Telegram
|
|
122
|
+
|
|
123
|
+
Compass is silent by default. Mention it directly to interact:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
@Compass what's blocked?
|
|
127
|
+
@Compass who owns the landing page?
|
|
128
|
+
@Compass what did we decide about the API?
|
|
129
|
+
@Compass show this week's metrics
|
|
130
|
+
@Compass extract from this conversation
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Slack reports
|
|
136
|
+
|
|
137
|
+
Compass posts automatically on your configured schedule:
|
|
138
|
+
|
|
139
|
+
- **Daily standup** — every day at your configured time
|
|
140
|
+
- **Weekly review** — every Monday (or your configured day) morning
|
|
141
|
+
|
|
142
|
+
To trigger a report immediately:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
compass report --type daily
|
|
146
|
+
compass report --type weekly
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## State files
|
|
152
|
+
|
|
153
|
+
All extracted information is stored as plain markdown files in `~/.openclaw/compass/state/`. You can read, edit, or back them up like any file.
|
|
154
|
+
|
|
155
|
+
| File | What it stores |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `tasks.md` | Active tasks with owner, status, deadline, priority |
|
|
158
|
+
| `decisions.md` | Decision log with what, why, alternatives |
|
|
159
|
+
| `blockers.md` | Open blockers with age tracking |
|
|
160
|
+
| `commitments.md` | Commitments and deadlines |
|
|
161
|
+
| `metrics.md` | Key metrics log |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Re-running setup
|
|
166
|
+
|
|
167
|
+
`compass init` is safe to re-run. It overwrites existing config with your new answers.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
compass init
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Updating
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npm install -g compass-ai@latest
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Troubleshooting
|
|
184
|
+
|
|
185
|
+
| Problem | Fix |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `compass: command not found` | Run `npm install -g compass-ai` again |
|
|
188
|
+
| Telegram bot not responding | Send `/start` to your bot first, then `hi` |
|
|
189
|
+
| Slack not posting | Verify your `SLACK_BOT_TOKEN` and that the bot is added to the channel |
|
|
190
|
+
| Cron not firing | Run `crontab -l` to verify entries; check server timezone |
|
|
191
|
+
| OpenClaw not found after install | Run `npm install -g openclaw` manually |
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## What Compass will not do (Phase 1)
|
|
196
|
+
|
|
197
|
+
- No sending emails
|
|
198
|
+
- No publishing content externally
|
|
199
|
+
- No spending money
|
|
200
|
+
- No modifying outside systems
|
|
201
|
+
- No autonomous decisions without your approval
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT
|
package/bin/compass.js
CHANGED
|
@@ -7,20 +7,27 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const migrations = require('../migrations/index.js');
|
|
11
|
+
const pkg = require('../package.json');
|
|
12
|
+
|
|
13
|
+
const PACKAGE_DIR = path.join(__dirname, '..');
|
|
14
|
+
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
12
15
|
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'compass');
|
|
13
|
-
const STATE_DIR
|
|
14
|
-
const BOOT_MD
|
|
15
|
-
const SKILLS_SRC
|
|
16
|
+
const STATE_DIR = path.join(WORKSPACE_DIR, 'state');
|
|
17
|
+
const BOOT_MD = path.join(WORKSPACE_DIR, 'boot.md');
|
|
18
|
+
const SKILLS_SRC = path.join(PACKAGE_DIR, 'skills');
|
|
16
19
|
|
|
17
20
|
const cmd = process.argv[2];
|
|
18
21
|
|
|
19
22
|
switch (cmd) {
|
|
20
|
-
case 'init':
|
|
21
|
-
case 'start':
|
|
22
|
-
case 'report':
|
|
23
|
-
|
|
23
|
+
case 'init': init().catch(err => { console.error('\n Error:', err.message); process.exit(1); }); break;
|
|
24
|
+
case 'start': start(); break;
|
|
25
|
+
case 'report': report(); break;
|
|
26
|
+
case 'migrate': runMigrate(); break;
|
|
27
|
+
case 'rollback': migrations.rollback(); break;
|
|
28
|
+
case 'doctor': migrations.doctor(); break;
|
|
29
|
+
case 'version': showVersion(); break;
|
|
30
|
+
default: help(); break;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -75,7 +82,7 @@ async function init() {
|
|
|
75
82
|
const dailyTime = await ask(rl, 'Daily standup time (HH:MM, 24h)', '09:00');
|
|
76
83
|
const weeklyDay = await ask(rl, 'Weekly review day (MON-SUN)', 'MON');
|
|
77
84
|
const weeklyTime = await ask(rl, 'Weekly review time (HH:MM, 24h)', '08:00');
|
|
78
|
-
const founderName = await ask(rl, 'Your name (for Compass memory)'
|
|
85
|
+
const founderName = await ask(rl, 'Your name (for Compass memory)');
|
|
79
86
|
|
|
80
87
|
rl.close();
|
|
81
88
|
|
|
@@ -197,6 +204,10 @@ Phase 1: read Telegram, extract signal, maintain state files, report to Slack.
|
|
|
197
204
|
|
|
198
205
|
copyDir(SKILLS_SRC, path.join(WORKSPACE_DIR, 'skills'));
|
|
199
206
|
|
|
207
|
+
// ── Write version file ────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
migrations.initVersion(pkg.version);
|
|
210
|
+
|
|
200
211
|
// ── Cron jobs (Linux only) ────────────────────────────────────────────────
|
|
201
212
|
|
|
202
213
|
if (process.platform === 'linux') {
|
|
@@ -207,17 +218,20 @@ Phase 1: read Telegram, extract signal, maintain state files, report to Slack.
|
|
|
207
218
|
|
|
208
219
|
console.log(' ✅ Config written to', OPENCLAW_DIR);
|
|
209
220
|
console.log(' ✅ State files created in', STATE_DIR);
|
|
221
|
+
console.log(' ✅ Schema version:', migrations.CURRENT_SCHEMA);
|
|
210
222
|
console.log('\n─────────────────────────────────────────');
|
|
211
223
|
console.log(' Compass is ready.\n');
|
|
212
224
|
console.log(' Start: compass start');
|
|
213
225
|
console.log(' Daily report: compass report --type daily');
|
|
214
226
|
console.log(' Weekly report: compass report --type weekly');
|
|
227
|
+
console.log(' Health check: compass doctor');
|
|
215
228
|
console.log('\n Next: open Telegram → find your bot → send /start\n');
|
|
216
229
|
}
|
|
217
230
|
|
|
218
231
|
// ── start ─────────────────────────────────────────────────────────────────────
|
|
219
232
|
|
|
220
233
|
function start() {
|
|
234
|
+
migrations.checkVersion(); // warn if schema is outdated, then continue
|
|
221
235
|
console.log('🧭 Starting Compass...');
|
|
222
236
|
runOpenClaw('start', '--headless');
|
|
223
237
|
}
|
|
@@ -234,6 +248,30 @@ function report() {
|
|
|
234
248
|
runOpenClaw('run', 'compass-report', '--type', type);
|
|
235
249
|
}
|
|
236
250
|
|
|
251
|
+
// ── migrate ───────────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
function runMigrate() {
|
|
254
|
+
const dry = process.argv.includes('--dry');
|
|
255
|
+
migrations.migrate({ dry }).catch(err => {
|
|
256
|
+
console.error('\n Migration error:', err.message, '\n');
|
|
257
|
+
process.exit(1);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── version ───────────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
function showVersion() {
|
|
264
|
+
const versionData = migrations.readVersion();
|
|
265
|
+
console.log(`\n🧭 compass-ai v${pkg.version}`);
|
|
266
|
+
console.log(` Schema: v${versionData ? versionData.schemaVersion : '?'} / current v${migrations.CURRENT_SCHEMA}`);
|
|
267
|
+
console.log(` State: ${STATE_DIR}`);
|
|
268
|
+
console.log(` Config: ${OPENCLAW_DIR}`);
|
|
269
|
+
if (versionData?.lastMigratedAt) {
|
|
270
|
+
console.log(` Migrated: ${versionData.lastMigratedAt}`);
|
|
271
|
+
}
|
|
272
|
+
console.log('');
|
|
273
|
+
}
|
|
274
|
+
|
|
237
275
|
// ── help ──────────────────────────────────────────────────────────────────────
|
|
238
276
|
|
|
239
277
|
function help() {
|
|
@@ -245,6 +283,13 @@ Usage:
|
|
|
245
283
|
compass start Start the Compass agent
|
|
246
284
|
compass report --type daily Trigger a daily standup report
|
|
247
285
|
compass report --type weekly Trigger a weekly review report
|
|
286
|
+
|
|
287
|
+
Maintenance:
|
|
288
|
+
compass migrate Run pending schema migrations
|
|
289
|
+
compass migrate --dry Preview migrations without applying
|
|
290
|
+
compass rollback Restore state from the last backup
|
|
291
|
+
compass doctor Full health check
|
|
292
|
+
compass version Show version and schema info
|
|
248
293
|
`);
|
|
249
294
|
}
|
|
250
295
|
|
|
@@ -318,13 +363,14 @@ Slack:
|
|
|
318
363
|
`;
|
|
319
364
|
}
|
|
320
365
|
|
|
321
|
-
function copyDir(src, dest) {
|
|
366
|
+
function copyDir(src, dest, exclude = []) {
|
|
322
367
|
if (!fs.existsSync(src)) return;
|
|
323
368
|
fs.mkdirSync(dest, { recursive: true });
|
|
324
369
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
370
|
+
if (exclude.includes(entry.name)) continue;
|
|
325
371
|
const srcPath = path.join(src, entry.name);
|
|
326
372
|
const destPath = path.join(dest, entry.name);
|
|
327
|
-
if (entry.isDirectory()) copyDir(srcPath, destPath);
|
|
373
|
+
if (entry.isDirectory()) copyDir(srcPath, destPath, exclude);
|
|
328
374
|
else fs.copyFileSync(srcPath, destPath);
|
|
329
375
|
}
|
|
330
376
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Schema v1 — baseline. No transformations needed.
|
|
4
|
+
// This migration exists only to mark the starting point.
|
|
5
|
+
// All installs using compass init will start at schema v1.
|
|
6
|
+
//
|
|
7
|
+
// How to add the next migration:
|
|
8
|
+
// 1. Create 002-your-description.js with the same shape
|
|
9
|
+
// 2. Bump CURRENT_SCHEMA in migrations/index.js to 2
|
|
10
|
+
// 3. Implement up() to transform state files from v1 → v2
|
|
11
|
+
//
|
|
12
|
+
// Example for adding a column to tasks.md:
|
|
13
|
+
//
|
|
14
|
+
// up: async (stateDir) => {
|
|
15
|
+
// const file = path.join(stateDir, 'tasks.md');
|
|
16
|
+
// const content = fs.readFileSync(file, 'utf8');
|
|
17
|
+
// const updated = addColumn(content, 'Tags', '');
|
|
18
|
+
// fs.writeFileSync(file, updated, 'utf8');
|
|
19
|
+
// }
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
version: 1,
|
|
23
|
+
description: 'Initial schema baseline',
|
|
24
|
+
up: async () => {},
|
|
25
|
+
};
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
// ── Schema version ────────────────────────────────────────────────────────────
|
|
9
|
+
// Bump ONLY when state file structure changes. NOT on every package release.
|
|
10
|
+
const CURRENT_SCHEMA = 1;
|
|
11
|
+
|
|
12
|
+
// ── Paths ─────────────────────────────────────────────────────────────────────
|
|
13
|
+
const WORKSPACE_DIR = path.join(os.homedir(), '.openclaw', 'compass');
|
|
14
|
+
const STATE_DIR = path.join(WORKSPACE_DIR, 'state');
|
|
15
|
+
const VERSION_FILE = path.join(WORKSPACE_DIR, 'version.json');
|
|
16
|
+
const BACKUPS_DIR = path.join(WORKSPACE_DIR, 'backups');
|
|
17
|
+
const BOOT_MD = path.join(WORKSPACE_DIR, 'boot.md');
|
|
18
|
+
|
|
19
|
+
// ── Discover and load migration files ────────────────────────────────────────
|
|
20
|
+
// Files must match NNN-description.js (e.g. 002-add-priority.js)
|
|
21
|
+
const MIGRATIONS = fs.readdirSync(__dirname)
|
|
22
|
+
.filter(f => /^\d{3}-.+\.js$/.test(f))
|
|
23
|
+
.sort()
|
|
24
|
+
.map(f => require(path.join(__dirname, f)));
|
|
25
|
+
|
|
26
|
+
// ── Version file ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function readVersion() {
|
|
29
|
+
if (!fs.existsSync(VERSION_FILE)) return null;
|
|
30
|
+
try { return JSON.parse(fs.readFileSync(VERSION_FILE, 'utf8')); } catch { return null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeVersion(data) {
|
|
34
|
+
fs.mkdirSync(path.dirname(VERSION_FILE), { recursive: true });
|
|
35
|
+
fs.writeFileSync(VERSION_FILE, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Hashing ───────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function hashFile(filePath) {
|
|
41
|
+
if (!fs.existsSync(filePath)) return null;
|
|
42
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
43
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Backup ────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function backup() {
|
|
49
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
50
|
+
const backupDir = path.join(BACKUPS_DIR, `backup-${ts}`);
|
|
51
|
+
|
|
52
|
+
copyDir(WORKSPACE_DIR, backupDir, ['backups']); // never recurse into backups
|
|
53
|
+
|
|
54
|
+
// Keep only the last 5 backups — prune oldest
|
|
55
|
+
if (fs.existsSync(BACKUPS_DIR)) {
|
|
56
|
+
const all = fs.readdirSync(BACKUPS_DIR).sort();
|
|
57
|
+
if (all.length > 5) {
|
|
58
|
+
all.slice(0, all.length - 5).forEach(b => rmDir(path.join(BACKUPS_DIR, b)));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return backupDir;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function latestBackup() {
|
|
66
|
+
if (!fs.existsSync(BACKUPS_DIR)) return null;
|
|
67
|
+
const all = fs.readdirSync(BACKUPS_DIR).sort();
|
|
68
|
+
return all.length ? path.join(BACKUPS_DIR, all[all.length - 1]) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Migrate ───────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
async function migrate(options = {}) {
|
|
74
|
+
const { dry = false } = options;
|
|
75
|
+
|
|
76
|
+
const versionData = readVersion();
|
|
77
|
+
|
|
78
|
+
// Legacy install: has state files but no version.json — treat as schema v0
|
|
79
|
+
const fromSchema = versionData ? versionData.schemaVersion : 0;
|
|
80
|
+
|
|
81
|
+
if (fromSchema === CURRENT_SCHEMA) {
|
|
82
|
+
console.log('\n Already up to date. Schema version:', CURRENT_SCHEMA, '\n');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const pending = MIGRATIONS.filter(m => m.version > fromSchema && m.version <= CURRENT_SCHEMA);
|
|
87
|
+
|
|
88
|
+
if (pending.length === 0) {
|
|
89
|
+
console.log('\n No migrations to run.\n');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`\n Pending migrations (schema v${fromSchema} → v${CURRENT_SCHEMA}):\n`);
|
|
94
|
+
pending.forEach(m => console.log(` • v${m.version}: ${m.description}`));
|
|
95
|
+
|
|
96
|
+
if (dry) {
|
|
97
|
+
console.log('\n Dry run complete — no changes made.\n');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Backup before touching anything
|
|
102
|
+
console.log('\n Creating backup...');
|
|
103
|
+
const backupDir = backup();
|
|
104
|
+
console.log(' Backed up to:', backupDir, '\n');
|
|
105
|
+
|
|
106
|
+
// Run each migration in sequence — abort and rollback on first failure
|
|
107
|
+
let lastApplied = fromSchema;
|
|
108
|
+
|
|
109
|
+
for (const migration of pending) {
|
|
110
|
+
process.stdout.write(` v${migration.version}: ${migration.description}... `);
|
|
111
|
+
try {
|
|
112
|
+
await migration.up(STATE_DIR, WORKSPACE_DIR);
|
|
113
|
+
lastApplied = migration.version;
|
|
114
|
+
console.log('✅');
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.log('❌');
|
|
117
|
+
console.error('\n Migration failed:', err.message);
|
|
118
|
+
console.error(' Rolling back from backup...');
|
|
119
|
+
restoreFromDir(backupDir);
|
|
120
|
+
console.error(' Rollback complete. Schema unchanged.\n');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Update version file
|
|
126
|
+
writeVersion({
|
|
127
|
+
...(versionData || {}),
|
|
128
|
+
schemaVersion: lastApplied,
|
|
129
|
+
packageVersion: require('../package.json').version,
|
|
130
|
+
lastMigratedAt: new Date().toISOString(),
|
|
131
|
+
migrationsApplied: [
|
|
132
|
+
...((versionData || {}).migrationsApplied || []),
|
|
133
|
+
...pending.map(m => m.version),
|
|
134
|
+
],
|
|
135
|
+
bootMdHash: hashFile(BOOT_MD),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log(`\n Migration complete. Schema is now v${lastApplied}.\n`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Rollback ──────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
function rollback() {
|
|
144
|
+
const backupDir = latestBackup();
|
|
145
|
+
if (!backupDir) {
|
|
146
|
+
console.error('\n No backups found in', BACKUPS_DIR, '\n');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
console.log('\n Restoring from:', backupDir);
|
|
150
|
+
restoreFromDir(backupDir);
|
|
151
|
+
console.log(' Rollback complete.\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function restoreFromDir(backupDir) {
|
|
155
|
+
copyDir(backupDir, WORKSPACE_DIR, []);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Version check (called on compass start) ───────────────────────────────────
|
|
159
|
+
|
|
160
|
+
function checkVersion() {
|
|
161
|
+
const versionData = readVersion();
|
|
162
|
+
|
|
163
|
+
// Legacy install: state files exist but no version.json
|
|
164
|
+
if (!versionData) {
|
|
165
|
+
if (fs.existsSync(path.join(STATE_DIR, 'tasks.md'))) {
|
|
166
|
+
console.warn('\n ⚠️ Existing state found but no schema version recorded.');
|
|
167
|
+
console.warn(' Run: compass migrate\n');
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (versionData.schemaVersion < CURRENT_SCHEMA) {
|
|
173
|
+
console.warn(`\n ⚠️ State files are schema v${versionData.schemaVersion}, current is v${CURRENT_SCHEMA}.`);
|
|
174
|
+
console.warn(' Run: compass migrate\n');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Init version (called after compass init) ──────────────────────────────────
|
|
179
|
+
|
|
180
|
+
function initVersion(packageVersion) {
|
|
181
|
+
const existing = readVersion();
|
|
182
|
+
writeVersion({
|
|
183
|
+
schemaVersion: CURRENT_SCHEMA,
|
|
184
|
+
packageVersion,
|
|
185
|
+
initializedAt: existing?.initializedAt || new Date().toISOString(),
|
|
186
|
+
lastMigratedAt: null,
|
|
187
|
+
migrationsApplied: [],
|
|
188
|
+
bootMdHash: hashFile(BOOT_MD),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Doctor ────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function doctor() {
|
|
195
|
+
const pkg = require('../package.json');
|
|
196
|
+
const versionData = readVersion();
|
|
197
|
+
|
|
198
|
+
const check = (label, ok, detail = '') => {
|
|
199
|
+
const icon = ok ? '✅' : '❌';
|
|
200
|
+
const suffix = detail ? ` ${detail}` : '';
|
|
201
|
+
console.log(` ${icon} ${label}${suffix}`);
|
|
202
|
+
return ok;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
console.log(`\n🧭 Compass — Doctor\n`);
|
|
206
|
+
console.log(` Package: compass-ai v${pkg.version}`);
|
|
207
|
+
console.log(` Schema: v${versionData ? versionData.schemaVersion : '?'} / current v${CURRENT_SCHEMA}`);
|
|
208
|
+
console.log('');
|
|
209
|
+
|
|
210
|
+
// Runtime
|
|
211
|
+
console.log(' Runtime');
|
|
212
|
+
const nodeOk = parseInt(process.version.slice(1)) >= 18;
|
|
213
|
+
check('Node.js', nodeOk, process.version);
|
|
214
|
+
|
|
215
|
+
let oclawOk = false;
|
|
216
|
+
try {
|
|
217
|
+
const { execSync } = require('child_process');
|
|
218
|
+
const oclawPath = execSync('which openclaw 2>/dev/null || command -v openclaw', { encoding: 'utf8' }).trim();
|
|
219
|
+
oclawOk = !!oclawPath;
|
|
220
|
+
check('OpenClaw', oclawOk, oclawPath);
|
|
221
|
+
} catch {
|
|
222
|
+
check('OpenClaw', false, 'not found — run: npm install -g openclaw');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Config files
|
|
226
|
+
console.log('\n Config');
|
|
227
|
+
const configDir = path.join(os.homedir(), '.openclaw');
|
|
228
|
+
['agent.json', 'channels.json', 'llm.json', 'hooks.json'].forEach(f => {
|
|
229
|
+
check(f, fs.existsSync(path.join(configDir, f)));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Schema
|
|
233
|
+
console.log('\n Schema');
|
|
234
|
+
if (!versionData) {
|
|
235
|
+
check('version.json', false, 'missing — run: compass migrate');
|
|
236
|
+
} else {
|
|
237
|
+
const current = versionData.schemaVersion === CURRENT_SCHEMA;
|
|
238
|
+
check('version.json', current,
|
|
239
|
+
current ? `v${versionData.schemaVersion} (current)` : `v${versionData.schemaVersion} — run: compass migrate`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// State files
|
|
243
|
+
console.log('\n State files');
|
|
244
|
+
['tasks.md', 'decisions.md', 'blockers.md', 'commitments.md', 'metrics.md'].forEach(f => {
|
|
245
|
+
check(f, fs.existsSync(path.join(STATE_DIR, f)));
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Connections
|
|
249
|
+
console.log('\n Connections');
|
|
250
|
+
try {
|
|
251
|
+
const channels = JSON.parse(fs.readFileSync(path.join(configDir, 'channels.json'), 'utf8'));
|
|
252
|
+
check('Telegram', !!channels.telegram?.token, channels.telegram?.token ? 'configured' : 'missing token');
|
|
253
|
+
check('Slack', !!channels.slack?.token, channels.slack?.token ? 'configured' : 'missing token');
|
|
254
|
+
} catch {
|
|
255
|
+
check('channels.json', false, 'could not read');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Cron (Linux only)
|
|
259
|
+
if (process.platform === 'linux') {
|
|
260
|
+
console.log('\n Cron');
|
|
261
|
+
try {
|
|
262
|
+
const { execSync } = require('child_process');
|
|
263
|
+
const crontab = execSync('crontab -l 2>/dev/null || true', { encoding: 'utf8' });
|
|
264
|
+
check('Daily report', crontab.includes('compass report --type daily'));
|
|
265
|
+
check('Weekly report', crontab.includes('compass report --type weekly'));
|
|
266
|
+
} catch {
|
|
267
|
+
check('Cron', false, 'could not read crontab');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Backups
|
|
272
|
+
console.log('\n Backups');
|
|
273
|
+
const latest = latestBackup();
|
|
274
|
+
if (latest) {
|
|
275
|
+
const name = path.basename(latest);
|
|
276
|
+
check('Latest backup', true, name);
|
|
277
|
+
} else {
|
|
278
|
+
console.log(' — No backups yet (created automatically before each migration)');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log('');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Utilities ─────────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
function copyDir(src, dest, exclude = []) {
|
|
287
|
+
if (!fs.existsSync(src)) return;
|
|
288
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
289
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
290
|
+
if (exclude.includes(entry.name)) continue;
|
|
291
|
+
const srcPath = path.join(src, entry.name);
|
|
292
|
+
const destPath = path.join(dest, entry.name);
|
|
293
|
+
if (entry.isDirectory()) copyDir(srcPath, destPath, exclude);
|
|
294
|
+
else fs.copyFileSync(srcPath, destPath);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function rmDir(dir) {
|
|
299
|
+
if (!fs.existsSync(dir)) return;
|
|
300
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
301
|
+
const p = path.join(dir, entry.name);
|
|
302
|
+
if (entry.isDirectory()) rmDir(p);
|
|
303
|
+
else fs.unlinkSync(p);
|
|
304
|
+
}
|
|
305
|
+
fs.rmdirSync(dir);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
migrate,
|
|
310
|
+
rollback,
|
|
311
|
+
checkVersion,
|
|
312
|
+
initVersion,
|
|
313
|
+
doctor,
|
|
314
|
+
CURRENT_SCHEMA,
|
|
315
|
+
readVersion,
|
|
316
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compass-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AI chief of staff for startups — built on OpenClaw",
|
|
5
5
|
"bin": {
|
|
6
6
|
"compass": "./bin/compass.js"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/",
|
|
10
10
|
"skills/",
|
|
11
|
-
"
|
|
11
|
+
"migrations/"
|
|
12
12
|
],
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"openclaw": "latest"
|