reflectt-node 0.1.4 → 0.1.6
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 +63 -146
- package/defaults/TEAM-ROLES.yaml +221 -31
- package/dist/activationEvents.d.ts +13 -2
- package/dist/activationEvents.d.ts.map +1 -1
- package/dist/activationEvents.js +172 -38
- package/dist/activationEvents.js.map +1 -1
- package/dist/activity.d.ts +72 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +510 -0
- package/dist/activity.js.map +1 -0
- package/dist/alert-preflight.d.ts +33 -0
- package/dist/alert-preflight.d.ts.map +1 -1
- package/dist/alert-preflight.js +218 -2
- package/dist/alert-preflight.js.map +1 -1
- package/dist/assignment.d.ts.map +1 -1
- package/dist/assignment.js +11 -6
- package/dist/assignment.js.map +1 -1
- package/dist/boardHealthWorker.d.ts.map +1 -1
- package/dist/boardHealthWorker.js +25 -12
- package/dist/boardHealthWorker.js.map +1 -1
- package/dist/canvas-slots.d.ts +1 -1
- package/dist/channels.d.ts +1 -1
- package/dist/chat-approval-detector.d.ts.map +1 -1
- package/dist/chat-approval-detector.js +29 -11
- package/dist/chat-approval-detector.js.map +1 -1
- package/dist/chat.d.ts +14 -0
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +68 -4
- package/dist/chat.js.map +1 -1
- package/dist/cli.js +349 -28
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts +28 -1
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +62 -25
- package/dist/cloud.js.map +1 -1
- package/dist/compliance-detector.d.ts +42 -0
- package/dist/compliance-detector.d.ts.map +1 -0
- package/dist/compliance-detector.js +286 -0
- package/dist/compliance-detector.js.map +1 -0
- package/dist/continuity-loop.d.ts.map +1 -1
- package/dist/continuity-loop.js +7 -3
- package/dist/continuity-loop.js.map +1 -1
- package/dist/dashboard.d.ts +6 -2
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +84 -28
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +24 -1
- package/dist/db.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +17 -6
- package/dist/doctor.js.map +1 -1
- package/dist/executionSweeper.d.ts +2 -0
- package/dist/executionSweeper.d.ts.map +1 -1
- package/dist/executionSweeper.js +60 -4
- package/dist/executionSweeper.js.map +1 -1
- package/dist/focus.d.ts +20 -0
- package/dist/focus.d.ts.map +1 -0
- package/dist/focus.js +57 -0
- package/dist/focus.js.map +1 -0
- package/dist/health.d.ts +1 -0
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +47 -15
- package/dist/health.js.map +1 -1
- package/dist/hostConnectGuard.d.ts +25 -0
- package/dist/hostConnectGuard.d.ts.map +1 -0
- package/dist/hostConnectGuard.js +27 -0
- package/dist/hostConnectGuard.js.map +1 -0
- package/dist/index.js +257 -39
- package/dist/index.js.map +1 -1
- package/dist/insight-mutation.d.ts +26 -0
- package/dist/insight-mutation.d.ts.map +1 -1
- package/dist/insight-mutation.js +103 -12
- package/dist/insight-mutation.js.map +1 -1
- package/dist/insight-task-bridge.d.ts +1 -1
- package/dist/insight-task-bridge.d.ts.map +1 -1
- package/dist/insight-task-bridge.js +6 -3
- package/dist/insight-task-bridge.js.map +1 -1
- package/dist/insights.d.ts +20 -0
- package/dist/insights.d.ts.map +1 -1
- package/dist/insights.js +129 -4
- package/dist/insights.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +9 -8
- package/dist/mcp.js.map +1 -1
- package/dist/notificationDedupeGuard.d.ts +33 -0
- package/dist/notificationDedupeGuard.d.ts.map +1 -0
- package/dist/notificationDedupeGuard.js +88 -0
- package/dist/notificationDedupeGuard.js.map +1 -0
- package/dist/openclaw.d.ts.map +1 -1
- package/dist/openclaw.js +3 -2
- package/dist/openclaw.js.map +1 -1
- package/dist/policy.d.ts +1 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +3 -1
- package/dist/policy.js.map +1 -1
- package/dist/prAutoMerge.d.ts.map +1 -1
- package/dist/prAutoMerge.js +23 -0
- package/dist/prAutoMerge.js.map +1 -1
- package/dist/presence.d.ts +16 -1
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +97 -9
- package/dist/presence.js.map +1 -1
- package/dist/pulse.d.ts +60 -0
- package/dist/pulse.d.ts.map +1 -0
- package/dist/pulse.js +139 -0
- package/dist/pulse.js.map +1 -0
- package/dist/reflection-automation.d.ts.map +1 -1
- package/dist/reflection-automation.js +38 -0
- package/dist/reflection-automation.js.map +1 -1
- package/dist/release.d.ts +2 -0
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +14 -1
- package/dist/release.js.map +1 -1
- package/dist/request-tracker.d.ts +6 -0
- package/dist/request-tracker.d.ts.map +1 -1
- package/dist/request-tracker.js +31 -12
- package/dist/request-tracker.js.map +1 -1
- package/dist/scopeOverlap.d.ts +32 -0
- package/dist/scopeOverlap.d.ts.map +1 -0
- package/dist/scopeOverlap.js +219 -0
- package/dist/scopeOverlap.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +736 -117
- package/dist/server.js.map +1 -1
- package/dist/service-probe.d.ts.map +1 -1
- package/dist/service-probe.js +39 -2
- package/dist/service-probe.js.map +1 -1
- package/dist/shipped-heartbeat.d.ts +1 -1
- package/dist/shipped-heartbeat.js +1 -1
- package/dist/taskPrecheck.js +6 -6
- package/dist/taskPrecheck.js.map +1 -1
- package/dist/tasks-next-diagnostics.d.ts +15 -0
- package/dist/tasks-next-diagnostics.d.ts.map +1 -0
- package/dist/tasks-next-diagnostics.js +33 -0
- package/dist/tasks-next-diagnostics.js.map +1 -0
- package/dist/tasks.d.ts +3 -2
- package/dist/tasks.d.ts.map +1 -1
- package/dist/tasks.js +41 -16
- package/dist/tasks.js.map +1 -1
- package/dist/team-config.d.ts.map +1 -1
- package/dist/team-config.js +20 -0
- package/dist/team-config.js.map +1 -1
- package/dist/todoHoardingGuard.d.ts +35 -0
- package/dist/todoHoardingGuard.d.ts.map +1 -0
- package/dist/todoHoardingGuard.js +150 -0
- package/dist/todoHoardingGuard.js.map +1 -0
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +16 -0
- package/dist/version.js.map +1 -0
- package/dist/working-contract.d.ts.map +1 -1
- package/dist/working-contract.js +59 -3
- package/dist/working-contract.js.map +1 -1
- package/package.json +5 -1
- package/public/dashboard.js +161 -20
- package/public/docs.md +68 -8
- package/public/polls-mock.html +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* reflectt CLI - Command line interface for reflectt-node
|
|
6
6
|
*/
|
|
7
7
|
import { Command } from 'commander';
|
|
8
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from 'fs';
|
|
8
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, statSync, chmodSync } from 'fs';
|
|
9
9
|
import { collectDoctorReport, formatDoctorHuman } from './doctor.js';
|
|
10
|
+
import { hostConnectGuard } from './hostConnectGuard.js';
|
|
10
11
|
import { homedir, hostname } from 'os';
|
|
11
12
|
import { join, dirname } from 'path';
|
|
12
13
|
import { spawn, execSync } from 'child_process';
|
|
@@ -18,6 +19,7 @@ const PID_FILE = join(REFLECTT_HOME, 'server.pid');
|
|
|
18
19
|
function loadConfig() {
|
|
19
20
|
if (existsSync(CONFIG_PATH)) {
|
|
20
21
|
try {
|
|
22
|
+
migrateConfigPermissions();
|
|
21
23
|
return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
22
24
|
}
|
|
23
25
|
catch (err) {
|
|
@@ -27,7 +29,21 @@ function loadConfig() {
|
|
|
27
29
|
return { port: 4445, host: '127.0.0.1' };
|
|
28
30
|
}
|
|
29
31
|
function saveConfig(config) {
|
|
30
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
32
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
33
|
+
}
|
|
34
|
+
/** Tighten config.json permissions if too open (contains cloud credentials). */
|
|
35
|
+
function migrateConfigPermissions() {
|
|
36
|
+
if (!existsSync(CONFIG_PATH))
|
|
37
|
+
return;
|
|
38
|
+
try {
|
|
39
|
+
const stat = statSync(CONFIG_PATH);
|
|
40
|
+
const mode = stat.mode & 0o777;
|
|
41
|
+
if (mode !== 0o600) {
|
|
42
|
+
chmodSync(CONFIG_PATH, 0o600);
|
|
43
|
+
console.log(`🔒 Tightened ${CONFIG_PATH} permissions: ${mode.toString(8)} → 600 (contains credentials)`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch { /* best-effort */ }
|
|
31
47
|
}
|
|
32
48
|
function ensureReflecttHome() {
|
|
33
49
|
mkdirSync(REFLECTT_HOME, { recursive: true });
|
|
@@ -149,6 +165,44 @@ async function enrollHostWithApiKey(input) {
|
|
|
149
165
|
}
|
|
150
166
|
throw new Error(`API key enrollment failed: ${payload?.error || `${response.status} ${response.statusText}`}`);
|
|
151
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Try to reconnect using existing persisted credentials.
|
|
170
|
+
* Returns the existing registration if the host is still valid, null otherwise.
|
|
171
|
+
*/
|
|
172
|
+
async function tryReconnectExistingHost(cloudUrl) {
|
|
173
|
+
try {
|
|
174
|
+
const config = loadConfig();
|
|
175
|
+
const cloud = config.cloud;
|
|
176
|
+
if (!cloud?.hostId || !cloud?.credential)
|
|
177
|
+
return null;
|
|
178
|
+
// Verify the host still exists by hitting the heartbeat endpoint
|
|
179
|
+
const cloudBase = cloudUrl.replace(/\/+$/, '');
|
|
180
|
+
const response = await fetch(`${cloudBase}/api/hosts/${cloud.hostId}/heartbeat`, {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
headers: {
|
|
183
|
+
'content-type': 'application/json',
|
|
184
|
+
authorization: `Bearer ${cloud.credential}`,
|
|
185
|
+
},
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
contractVersion: 1,
|
|
188
|
+
status: 'online',
|
|
189
|
+
agents: [],
|
|
190
|
+
activeTasks: [],
|
|
191
|
+
timestamp: Date.now(),
|
|
192
|
+
source: {},
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
if (response.ok) {
|
|
196
|
+
console.log(` ♻️ Reusing existing host (${cloud.hostId})`);
|
|
197
|
+
return { hostId: cloud.hostId, credential: cloud.credential };
|
|
198
|
+
}
|
|
199
|
+
console.log(` ⚠️ Existing host ${cloud.hostId} no longer valid (${response.status}), will register new`);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
152
206
|
async function registerHostWithCloud(input) {
|
|
153
207
|
const cloudBase = input.cloudUrl.replace(/\/+$/, '');
|
|
154
208
|
// New cloud API path (reflectt-cloud): /api/hosts/claim
|
|
@@ -267,10 +321,20 @@ function printStep(name, pass, detail) {
|
|
|
267
321
|
console.log(`${icon} ${name} — ${detail}`);
|
|
268
322
|
}
|
|
269
323
|
const program = new Command();
|
|
324
|
+
// Read version from package.json at runtime
|
|
325
|
+
const PKG_VERSION = (() => {
|
|
326
|
+
try {
|
|
327
|
+
const pkgPath = join(import.meta.dirname ?? __dirname, '..', 'package.json');
|
|
328
|
+
return JSON.parse(readFileSync(pkgPath, 'utf-8')).version ?? '0.0.0';
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return '0.0.0';
|
|
332
|
+
}
|
|
333
|
+
})();
|
|
270
334
|
program
|
|
271
335
|
.name('reflectt')
|
|
272
336
|
.description('CLI for reflectt-node - local agent communication server')
|
|
273
|
-
.version(
|
|
337
|
+
.version(PKG_VERSION);
|
|
274
338
|
// ============ INIT COMMAND ============
|
|
275
339
|
program
|
|
276
340
|
.command('init')
|
|
@@ -403,8 +467,17 @@ program
|
|
|
403
467
|
.option('-d, --detach', 'Run in background')
|
|
404
468
|
.action(async (options) => {
|
|
405
469
|
if (!existsSync(REFLECTT_HOME)) {
|
|
406
|
-
console.
|
|
407
|
-
|
|
470
|
+
console.log('📦 First run — initializing reflectt...');
|
|
471
|
+
// Auto-init: create directories and default config
|
|
472
|
+
mkdirSync(REFLECTT_HOME, { recursive: true });
|
|
473
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
474
|
+
mkdirSync(join(DATA_DIR, 'inbox'), { recursive: true });
|
|
475
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
476
|
+
saveConfig({ port: 4445, host: '127.0.0.1' });
|
|
477
|
+
console.log(' ✅ config.json');
|
|
478
|
+
}
|
|
479
|
+
console.log(' ✅ ~/.reflectt/ created');
|
|
480
|
+
console.log('');
|
|
408
481
|
}
|
|
409
482
|
// Check if already running
|
|
410
483
|
if (existsSync(PID_FILE)) {
|
|
@@ -423,6 +496,27 @@ program
|
|
|
423
496
|
}
|
|
424
497
|
}
|
|
425
498
|
const config = loadConfig();
|
|
499
|
+
// Port-level guard: refuse to start if something is already listening
|
|
500
|
+
// (catches LaunchAgent-managed instances that don't leave a PID file)
|
|
501
|
+
const clientHost = (config.host === '0.0.0.0' || config.host === '::') ? '127.0.0.1' : config.host;
|
|
502
|
+
try {
|
|
503
|
+
const controller = new AbortController();
|
|
504
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
505
|
+
const res = await fetch(`http://${clientHost}:${config.port}/health/deploy`, { signal: controller.signal });
|
|
506
|
+
clearTimeout(timeout);
|
|
507
|
+
if (res.ok) {
|
|
508
|
+
const deploy = await res.json().catch(() => ({}));
|
|
509
|
+
const pid = deploy.pid ?? 'unknown';
|
|
510
|
+
const sha = deploy.gitSha ?? 'unknown';
|
|
511
|
+
console.error(`❌ Server already running on port ${config.port} (PID: ${pid}, sha: ${sha})`);
|
|
512
|
+
console.error(' If managed by LaunchAgent: launchctl kickstart -k gui/$(id -u)/com.reflectt.node');
|
|
513
|
+
console.error(' Otherwise: reflectt stop && reflectt start');
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// Port not responding — safe to start
|
|
519
|
+
}
|
|
426
520
|
const { projectRoot, serverPath, useNode } = getRuntimePaths();
|
|
427
521
|
if (!existsSync(serverPath)) {
|
|
428
522
|
console.error(`❌ Server file not found: ${serverPath}`);
|
|
@@ -432,11 +526,57 @@ program
|
|
|
432
526
|
const cmd = useNode ? 'node' : 'npx';
|
|
433
527
|
const args = useNode ? [serverPath] : ['tsx', serverPath];
|
|
434
528
|
if (options.detach) {
|
|
529
|
+
// Ephemeral container warning: detach is a trap in docker run --rm
|
|
530
|
+
const isContainer = existsSync('/.dockerenv') || existsSync('/run/.containerenv');
|
|
531
|
+
if (isContainer) {
|
|
532
|
+
console.warn('⚠️ Detected container environment. Using --detach here is risky:');
|
|
533
|
+
console.warn(' The server will stop when the container exits.');
|
|
534
|
+
console.warn(' Consider: reflectt start (foreground) or use Docker CMD directly.');
|
|
535
|
+
console.warn('');
|
|
536
|
+
}
|
|
435
537
|
const pid = startServerDetached(config);
|
|
436
|
-
|
|
538
|
+
const clientHost = (config.host === '0.0.0.0' || config.host === '::') ? '127.0.0.1' : config.host;
|
|
539
|
+
console.log(`⏳ Starting reflectt server (PID: ${pid})...`);
|
|
540
|
+
// Health check: wait up to 10s for server to respond
|
|
541
|
+
let healthy = false;
|
|
542
|
+
for (let i = 0; i < 20; i++) {
|
|
543
|
+
await new Promise(r => setTimeout(r, 500));
|
|
544
|
+
try {
|
|
545
|
+
const controller = new AbortController();
|
|
546
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
547
|
+
const res = await fetch(`http://${clientHost}:${config.port}/health`, { signal: controller.signal });
|
|
548
|
+
clearTimeout(timeout);
|
|
549
|
+
if (res.ok) {
|
|
550
|
+
healthy = true;
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch { /* not ready yet */ }
|
|
555
|
+
}
|
|
556
|
+
if (healthy) {
|
|
557
|
+
console.log('✅ Server is running!');
|
|
558
|
+
// Show deploy info for verification
|
|
559
|
+
try {
|
|
560
|
+
const controller = new AbortController();
|
|
561
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
562
|
+
const dRes = await fetch(`http://${clientHost}:${config.port}/health/deploy`, { signal: controller.signal });
|
|
563
|
+
clearTimeout(timeout);
|
|
564
|
+
if (dRes.ok) {
|
|
565
|
+
const deploy = await dRes.json();
|
|
566
|
+
if (deploy.gitSha)
|
|
567
|
+
console.log(` Commit: ${String(deploy.gitSha).slice(0, 12)}`);
|
|
568
|
+
if (deploy.startedAt)
|
|
569
|
+
console.log(` Started: ${deploy.startedAt}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch { /* deploy endpoint not available */ }
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
console.log('⚠️ Server started but not responding yet (may still be booting)');
|
|
576
|
+
}
|
|
437
577
|
console.log(` PID: ${pid}`);
|
|
438
|
-
console.log(` URL: http://${
|
|
439
|
-
console.log(` Dashboard: http://${
|
|
578
|
+
console.log(` URL: http://${clientHost}:${config.port}`);
|
|
579
|
+
console.log(` Dashboard: http://${clientHost}:${config.port}/dashboard`);
|
|
440
580
|
if (config.cloud) {
|
|
441
581
|
console.log(` Cloud: ${config.cloud.cloudUrl} (host: ${config.cloud.hostName})`);
|
|
442
582
|
}
|
|
@@ -491,41 +631,211 @@ program
|
|
|
491
631
|
}
|
|
492
632
|
}
|
|
493
633
|
});
|
|
634
|
+
// ============ UPGRADE COMMAND ============
|
|
635
|
+
program
|
|
636
|
+
.command('upgrade')
|
|
637
|
+
.description('Upgrade reflectt-node to the latest version and restart')
|
|
638
|
+
.action(async () => {
|
|
639
|
+
const { execSync } = await import('child_process');
|
|
640
|
+
// 1. Get current version
|
|
641
|
+
console.log('📦 Checking for updates...');
|
|
642
|
+
const currentVersion = PKG_VERSION;
|
|
643
|
+
// 2. Update via npm
|
|
644
|
+
try {
|
|
645
|
+
console.log('⬆️ Updating reflectt-node...');
|
|
646
|
+
execSync('npm update -g reflectt-node', { stdio: 'inherit' });
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
console.error('❌ npm update failed. Try: npm install -g reflectt-node@latest');
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
// 3. Check new version
|
|
653
|
+
try {
|
|
654
|
+
const newVersion = execSync('node -e "console.log(require(\'reflectt-node/package.json\').version)"', { encoding: 'utf-8' }).trim();
|
|
655
|
+
if (newVersion === currentVersion) {
|
|
656
|
+
console.log(`✅ Already on latest version (${currentVersion})`);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
console.log(`✅ Updated: ${currentVersion} → ${newVersion}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
console.log('✅ Update complete');
|
|
664
|
+
}
|
|
665
|
+
// 4. Restart if running
|
|
666
|
+
if (existsSync(PID_FILE)) {
|
|
667
|
+
const pid = readFileSync(PID_FILE, 'utf-8').trim();
|
|
668
|
+
try {
|
|
669
|
+
process.kill(Number(pid), 'SIGTERM');
|
|
670
|
+
console.log('⏹️ Server stopped');
|
|
671
|
+
unlinkSync(PID_FILE);
|
|
672
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
if (err.code === 'ESRCH')
|
|
676
|
+
unlinkSync(PID_FILE);
|
|
677
|
+
}
|
|
678
|
+
const { spawn } = await import('child_process');
|
|
679
|
+
console.log('🚀 Starting server...');
|
|
680
|
+
const child = spawn(process.execPath, [process.argv[1], 'start'], {
|
|
681
|
+
stdio: 'inherit',
|
|
682
|
+
detached: false,
|
|
683
|
+
cwd: process.cwd(),
|
|
684
|
+
});
|
|
685
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
console.log('ℹ️ Server not running. Start with: reflectt start');
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
// ============ RESTART COMMAND ============
|
|
692
|
+
program
|
|
693
|
+
.command('restart')
|
|
694
|
+
.description('Restart the reflectt server (stop + start)')
|
|
695
|
+
.action(async () => {
|
|
696
|
+
// Stop if running
|
|
697
|
+
if (existsSync(PID_FILE)) {
|
|
698
|
+
const pid = readFileSync(PID_FILE, 'utf-8').trim();
|
|
699
|
+
try {
|
|
700
|
+
process.kill(Number(pid), 'SIGTERM');
|
|
701
|
+
console.log('⏹️ Server stopped (PID ' + pid + ')');
|
|
702
|
+
unlinkSync(PID_FILE);
|
|
703
|
+
// Wait for port to free up
|
|
704
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
705
|
+
}
|
|
706
|
+
catch (err) {
|
|
707
|
+
if (err.code === 'ESRCH') {
|
|
708
|
+
console.log('⚠️ Previous process not found, cleaning up');
|
|
709
|
+
unlinkSync(PID_FILE);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
console.log('ℹ️ No running server found, starting fresh');
|
|
715
|
+
}
|
|
716
|
+
// Start
|
|
717
|
+
const { spawn } = await import('child_process');
|
|
718
|
+
const child = spawn(process.execPath, [process.argv[1], 'start'], {
|
|
719
|
+
stdio: 'inherit',
|
|
720
|
+
detached: false,
|
|
721
|
+
cwd: process.cwd(),
|
|
722
|
+
});
|
|
723
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
724
|
+
});
|
|
494
725
|
// ============ STATUS COMMAND ============
|
|
495
726
|
program
|
|
496
727
|
.command('status')
|
|
497
728
|
.description('Check server health and status')
|
|
498
729
|
.action(async () => {
|
|
499
730
|
const config = loadConfig();
|
|
731
|
+
const DEFAULT_PORT = 4445;
|
|
732
|
+
const configHost = (config.host === '0.0.0.0' || config.host === '::') ? '127.0.0.1' : config.host;
|
|
500
733
|
// Check PID file
|
|
501
734
|
const isRunning = existsSync(PID_FILE);
|
|
502
735
|
const pid = isRunning ? readFileSync(PID_FILE, 'utf-8').trim() : null;
|
|
503
736
|
console.log('📊 reflectt Status');
|
|
504
737
|
console.log(` Config: ${CONFIG_PATH}`);
|
|
505
|
-
|
|
738
|
+
// Try config port first, then default port as fallback
|
|
739
|
+
let health = null;
|
|
740
|
+
let activePort = config.port;
|
|
741
|
+
let activeUrl = `http://${configHost}:${config.port}`;
|
|
742
|
+
async function tryPort(port) {
|
|
743
|
+
try {
|
|
744
|
+
const url = `http://${configHost}:${port}/health`;
|
|
745
|
+
const controller = new AbortController();
|
|
746
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
747
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
748
|
+
clearTimeout(timeout);
|
|
749
|
+
if (res.ok)
|
|
750
|
+
return await res.json();
|
|
751
|
+
}
|
|
752
|
+
catch { /* not responding on this port */ }
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
health = await tryPort(config.port);
|
|
756
|
+
if (!health && config.port !== DEFAULT_PORT) {
|
|
757
|
+
// Config port failed — try default port (common drift: config says 4446, server runs on 4445)
|
|
758
|
+
health = await tryPort(DEFAULT_PORT);
|
|
759
|
+
if (health) {
|
|
760
|
+
activePort = DEFAULT_PORT;
|
|
761
|
+
activeUrl = `http://${configHost}:${DEFAULT_PORT}`;
|
|
762
|
+
console.log(` ⚠️ Config port ${config.port} not responding, found server on default port ${DEFAULT_PORT}`);
|
|
763
|
+
// Auto-fix config port to match reality
|
|
764
|
+
try {
|
|
765
|
+
config.port = DEFAULT_PORT;
|
|
766
|
+
saveConfig(config);
|
|
767
|
+
console.log(` 🔧 Auto-fixed config.json → port ${DEFAULT_PORT}`);
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
console.log(` 💡 Fix: update ${CONFIG_PATH} → "port": ${DEFAULT_PORT}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
console.log(` URL: ${activeUrl}`);
|
|
506
775
|
if (pid) {
|
|
507
776
|
try {
|
|
508
777
|
process.kill(Number(pid), 0); // Check if process exists
|
|
509
|
-
|
|
778
|
+
if (health) {
|
|
779
|
+
console.log(` Process: Running (PID: ${pid})`);
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
console.log(` Process: PID ${pid} exists but /health not responding — server may be unhealthy`);
|
|
783
|
+
}
|
|
510
784
|
}
|
|
511
785
|
catch (err) {
|
|
512
|
-
|
|
513
|
-
|
|
786
|
+
if (health) {
|
|
787
|
+
console.log(` Process: PID file stale, but server is responding on port ${activePort}`);
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
console.log(` Process: Not found (stale PID file)`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
514
793
|
}
|
|
515
794
|
}
|
|
516
795
|
else {
|
|
517
|
-
|
|
518
|
-
|
|
796
|
+
if (health) {
|
|
797
|
+
console.log(` Process: No PID file, but server is responding on port ${activePort}`);
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
console.log(` Process: Not running`);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
519
803
|
}
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
const health = await apiRequest('/health');
|
|
804
|
+
// Show health status
|
|
805
|
+
if (health) {
|
|
523
806
|
console.log('\n✅ Server Health');
|
|
524
807
|
console.log(` Status: ${health.status}`);
|
|
808
|
+
console.log(` Version: ${health.version || 'unknown'}`);
|
|
809
|
+
// Fetch deploy info (commit SHA + startedAt) for done_criteria #3
|
|
810
|
+
try {
|
|
811
|
+
const deployUrl = `http://${configHost}:${activePort}/health/deploy`;
|
|
812
|
+
const controller = new AbortController();
|
|
813
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
814
|
+
const dRes = await fetch(deployUrl, { signal: controller.signal });
|
|
815
|
+
clearTimeout(timeout);
|
|
816
|
+
if (dRes.ok) {
|
|
817
|
+
const deploy = await dRes.json();
|
|
818
|
+
if (deploy.gitSha)
|
|
819
|
+
console.log(` Commit: ${String(deploy.gitSha).slice(0, 12)}`);
|
|
820
|
+
if (deploy.startedAt)
|
|
821
|
+
console.log(` Started: ${deploy.startedAt}`);
|
|
822
|
+
if (deploy.pid)
|
|
823
|
+
console.log(` Server PID: ${deploy.pid}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
catch { /* deploy endpoint not available */ }
|
|
525
827
|
console.log(` Chat messages: ${health.chat?.messageCount || 0}`);
|
|
526
|
-
|
|
828
|
+
const tasks = health.tasks;
|
|
829
|
+
console.log(` Tasks: ${tasks?.total || 0}`);
|
|
830
|
+
if (health.openclaw) {
|
|
831
|
+
const oc = health.openclaw;
|
|
832
|
+
console.log(` OpenClaw: ${oc.status}${oc.gateway ? ` (${oc.gateway})` : ''}`);
|
|
833
|
+
}
|
|
834
|
+
if (health.cloud) {
|
|
835
|
+
console.log(` Cloud: connected`);
|
|
836
|
+
}
|
|
527
837
|
}
|
|
528
|
-
|
|
838
|
+
else {
|
|
529
839
|
console.log('\n⚠️ Server process exists but not responding');
|
|
530
840
|
}
|
|
531
841
|
});
|
|
@@ -720,7 +1030,7 @@ dogfood
|
|
|
720
1030
|
.description('Run end-to-end cloud enrollment chain verification')
|
|
721
1031
|
.requiredOption('--team-id <id>', 'Cloud team id to target')
|
|
722
1032
|
.requiredOption('--token <jwt>', 'Bearer token for cloud API auth (team admin/owner)')
|
|
723
|
-
.option('--cloud-url <url>', 'Cloud API base URL', process.env.REFLECTT_CLOUD_URL || 'https://
|
|
1033
|
+
.option('--cloud-url <url>', 'Cloud API base URL', process.env.REFLECTT_CLOUD_URL || 'https://app.reflectt.ai')
|
|
724
1034
|
.option('--dashboard-url <url>', 'Dashboard base URL', process.env.REFLECTT_APP_URL || 'https://app.reflectt.ai')
|
|
725
1035
|
.option('--host-name <name>', 'Host name to register', `dogfood-${process.pid}`)
|
|
726
1036
|
.option('--capability <value...>', 'Host capabilities', ['openclaw', 'dogfood-smoke'])
|
|
@@ -815,7 +1125,7 @@ program
|
|
|
815
1125
|
.description('One-shot setup: init + connect to cloud + start server. Fastest way to get running.')
|
|
816
1126
|
.option('--join-token <token>', 'Cloud host join token (get one at app.reflectt.ai)')
|
|
817
1127
|
.option('--api-key <key>', 'Team API key')
|
|
818
|
-
.option('--cloud-url <url>', 'Cloud API base URL', 'https://
|
|
1128
|
+
.option('--cloud-url <url>', 'Cloud API base URL', 'https://app.reflectt.ai')
|
|
819
1129
|
.option('--name <hostName>', 'Host display name', hostname())
|
|
820
1130
|
.option('--type <hostType>', 'Host type', 'openclaw')
|
|
821
1131
|
.action(async (options) => {
|
|
@@ -860,8 +1170,10 @@ program
|
|
|
860
1170
|
console.log(` ✅ Home: ${REFLECTT_HOME}`);
|
|
861
1171
|
// Step 2: Connect
|
|
862
1172
|
console.log('☁️ Step 2/3: Connecting to Reflectt Cloud...');
|
|
863
|
-
const cloudUrl = String(options.cloudUrl || 'https://
|
|
864
|
-
|
|
1173
|
+
const cloudUrl = String(options.cloudUrl || 'https://app.reflectt.ai').replace(/\/+$/, '');
|
|
1174
|
+
// Try to reconnect existing host first (preserves hostId across re-enrollments)
|
|
1175
|
+
const existingHost = await tryReconnectExistingHost(cloudUrl);
|
|
1176
|
+
const registered = existingHost || (options.apiKey
|
|
865
1177
|
? await enrollHostWithApiKey({
|
|
866
1178
|
cloudUrl,
|
|
867
1179
|
apiKey: options.apiKey,
|
|
@@ -873,7 +1185,7 @@ program
|
|
|
873
1185
|
joinToken: options.joinToken,
|
|
874
1186
|
hostName: options.name,
|
|
875
1187
|
hostType: options.type,
|
|
876
|
-
});
|
|
1188
|
+
}));
|
|
877
1189
|
const config = loadConfig();
|
|
878
1190
|
const nextConfig = {
|
|
879
1191
|
...config,
|
|
@@ -941,10 +1253,11 @@ host
|
|
|
941
1253
|
.description('Enroll this reflectt-node host with Reflectt Cloud')
|
|
942
1254
|
.option('--join-token <token>', 'Cloud host join token (from dashboard)')
|
|
943
1255
|
.option('--api-key <key>', 'Team API key for agent-friendly enrollment (no browser needed)')
|
|
944
|
-
.option('--cloud-url <url>', 'Cloud API base URL', 'https://
|
|
1256
|
+
.option('--cloud-url <url>', 'Cloud API base URL', 'https://app.reflectt.ai')
|
|
945
1257
|
.option('--name <hostName>', 'Host display name', hostname())
|
|
946
1258
|
.option('--type <hostType>', 'Host type', 'openclaw')
|
|
947
1259
|
.option('--auth-token <jwt>', 'Temporary user JWT for environments where claim endpoint is JWT-gated')
|
|
1260
|
+
.option('--force', 'Overwrite existing cloud enrollment (destructive)')
|
|
948
1261
|
.option('--no-restart', 'Do not restart/start local reflectt server after enrollment')
|
|
949
1262
|
.action(async (options) => {
|
|
950
1263
|
try {
|
|
@@ -960,12 +1273,20 @@ host
|
|
|
960
1273
|
}
|
|
961
1274
|
ensureReflecttHome();
|
|
962
1275
|
const config = loadConfig();
|
|
963
|
-
const cloudUrl = String(options.cloudUrl || 'https://
|
|
1276
|
+
const cloudUrl = String(options.cloudUrl || 'https://app.reflectt.ai').replace(/\/+$/, '');
|
|
1277
|
+
// Guard against destructive overwrite.
|
|
1278
|
+
const decision = hostConnectGuard({ existingCloud: config.cloud, force: Boolean(options.force) });
|
|
1279
|
+
if (!decision.allow) {
|
|
1280
|
+
console.error(decision.warning);
|
|
1281
|
+
process.exit(1);
|
|
1282
|
+
}
|
|
964
1283
|
console.log('☁️ Enrolling host with Reflectt Cloud...');
|
|
965
1284
|
console.log(` Cloud: ${cloudUrl}`);
|
|
966
1285
|
console.log(` Host: ${options.name} (${options.type})`);
|
|
967
1286
|
console.log(` Method: ${options.apiKey ? 'API key' : 'join token'}`);
|
|
968
|
-
|
|
1287
|
+
// Try to reconnect existing host first (preserves hostId across re-enrollments)
|
|
1288
|
+
const existingHost = await tryReconnectExistingHost(cloudUrl);
|
|
1289
|
+
const registered = existingHost || (options.apiKey
|
|
969
1290
|
? await enrollHostWithApiKey({
|
|
970
1291
|
cloudUrl,
|
|
971
1292
|
apiKey: options.apiKey,
|
|
@@ -978,7 +1299,7 @@ host
|
|
|
978
1299
|
hostName: options.name,
|
|
979
1300
|
hostType: options.type,
|
|
980
1301
|
authToken: options.authToken,
|
|
981
|
-
});
|
|
1302
|
+
}));
|
|
982
1303
|
const nextConfig = {
|
|
983
1304
|
...config,
|
|
984
1305
|
cloud: {
|