@esotech/contextuate 2.0.0 → 2.1.1
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 +169 -1
- package/dist/commands/claude.d.ts +21 -0
- package/dist/commands/claude.js +213 -0
- package/dist/commands/context.d.ts +1 -0
- package/dist/commands/create.d.ts +3 -0
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +67 -6
- package/dist/commands/install.d.ts +28 -0
- package/dist/commands/install.js +100 -11
- package/dist/commands/monitor.d.ts +55 -0
- package/dist/commands/monitor.js +1007 -0
- package/dist/commands/remove.d.ts +3 -0
- package/dist/commands/run.d.ts +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +113 -1
- package/dist/monitor/daemon/circuit-breaker.d.ts +121 -0
- package/dist/monitor/daemon/circuit-breaker.js +552 -0
- package/dist/monitor/daemon/cli.d.ts +8 -0
- package/dist/monitor/daemon/cli.js +82 -0
- package/dist/monitor/daemon/index.d.ts +137 -0
- package/dist/monitor/daemon/index.js +695 -0
- package/dist/monitor/daemon/notifier.d.ts +25 -0
- package/dist/monitor/daemon/notifier.js +98 -0
- package/dist/monitor/daemon/processor.d.ts +89 -0
- package/dist/monitor/daemon/processor.js +455 -0
- package/dist/monitor/daemon/state.d.ts +80 -0
- package/dist/monitor/daemon/state.js +162 -0
- package/dist/monitor/daemon/watcher.d.ts +47 -0
- package/dist/monitor/daemon/watcher.js +171 -0
- package/dist/monitor/daemon/wrapper-manager.d.ts +106 -0
- package/dist/monitor/daemon/wrapper-manager.js +374 -0
- package/dist/monitor/hooks/emit-event.js +652 -0
- package/dist/monitor/persistence/file-store.d.ts +88 -0
- package/dist/monitor/persistence/file-store.js +335 -0
- package/dist/monitor/persistence/index.d.ts +7 -0
- package/dist/monitor/persistence/index.js +10 -0
- package/dist/monitor/server/adapters/redis.d.ts +38 -0
- package/dist/monitor/server/adapters/redis.js +213 -0
- package/dist/monitor/server/adapters/unix-socket.d.ts +33 -0
- package/dist/monitor/server/adapters/unix-socket.js +182 -0
- package/dist/monitor/server/broker.d.ts +135 -0
- package/dist/monitor/server/broker.js +475 -0
- package/dist/monitor/server/cli.d.ts +8 -0
- package/dist/monitor/server/cli.js +98 -0
- package/dist/monitor/server/fastify.d.ts +16 -0
- package/dist/monitor/server/fastify.js +184 -0
- package/dist/monitor/server/index.d.ts +36 -0
- package/dist/monitor/server/index.js +153 -0
- package/dist/monitor/server/websocket.d.ts +80 -0
- package/dist/monitor/server/websocket.js +453 -0
- package/dist/monitor/ui/assets/index-4IssW9On.js +59 -0
- package/dist/monitor/ui/assets/index-vo9hLe5R.css +32 -0
- package/dist/monitor/ui/favicon.png +0 -0
- package/dist/monitor/ui/index.html +14 -0
- package/dist/monitor/ui/logo.png +0 -0
- package/dist/monitor/ui/logo.svg +1 -0
- package/dist/runtime/driver.d.ts +16 -0
- package/dist/runtime/tools.d.ts +10 -0
- package/dist/templates/README.md +33 -7
- package/dist/templates/agents/aegis.md +4 -0
- package/dist/templates/agents/archon.md +13 -22
- package/dist/templates/agents/atlas.md +4 -0
- package/dist/templates/agents/canvas.md +4 -0
- package/dist/templates/agents/chronicle.md +4 -0
- package/dist/templates/agents/chronos.md +4 -0
- package/dist/templates/agents/cipher.md +4 -0
- package/dist/templates/agents/crucible.md +4 -0
- package/dist/templates/agents/echo.md +4 -0
- package/dist/templates/agents/forge.md +4 -0
- package/dist/templates/agents/ledger.md +4 -0
- package/dist/templates/agents/meridian.md +4 -0
- package/dist/templates/agents/nexus.md +4 -0
- package/dist/templates/agents/pythia.md +217 -0
- package/dist/templates/agents/scribe.md +4 -0
- package/dist/templates/agents/sentinel.md +4 -0
- package/dist/templates/agents/{oracle.md → thoth.md} +11 -7
- package/dist/templates/agents/unity.md +4 -0
- package/dist/templates/agents/vox.md +4 -0
- package/dist/templates/agents/weaver.md +4 -0
- package/dist/templates/commands/consult.md +138 -0
- package/dist/templates/commands/orchestrate.md +173 -0
- package/dist/templates/framework-agents/documentation-expert.md +3 -3
- package/dist/templates/framework-agents/tools-expert.md +8 -8
- package/dist/templates/standards/agent-roles.md +68 -21
- package/dist/templates/standards/coding-standards.md +9 -26
- package/dist/templates/templates/context.md +17 -2
- package/dist/templates/templates/contextuate.md +21 -28
- package/dist/templates/tools/{agent-creator.tool.md → agent-creator.md} +3 -3
- package/dist/types/monitor.d.ts +660 -0
- package/dist/types/monitor.js +75 -0
- package/dist/utils/git.d.ts +9 -0
- package/dist/utils/tokens.d.ts +10 -0
- package/package.json +18 -5
- package/dist/templates/version.json +0 -8
- /package/dist/templates/templates/standards/{go.standards.md → go.md} +0 -0
- /package/dist/templates/templates/standards/{java.standards.md → java.md} +0 -0
- /package/dist/templates/templates/standards/{javascript.standards.md → javascript.md} +0 -0
- /package/dist/templates/templates/standards/{php.standards.md → php.md} +0 -0
- /package/dist/templates/templates/standards/{python.standards.md → python.md} +0 -0
- /package/dist/templates/tools/{quickref.tool.md → quickref.md} +0 -0
- /package/dist/templates/tools/{spawn.tool.md → spawn.md} +0 -0
- /package/dist/templates/tools/{standards-detector.tool.md → standards-detector.md} +0 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Monitor Command
|
|
4
|
+
*
|
|
5
|
+
* CLI commands for the Contextuate Monitor feature.
|
|
6
|
+
* - contextuate monitor init: Interactive setup
|
|
7
|
+
* - contextuate monitor [start]: Start the monitor server
|
|
8
|
+
* - contextuate monitor status: Show server status
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
44
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
45
|
+
};
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.monitorInitCommand = monitorInitCommand;
|
|
48
|
+
exports.monitorStartCommand = monitorStartCommand;
|
|
49
|
+
exports.monitorStopCommand = monitorStopCommand;
|
|
50
|
+
exports.monitorStatusCommand = monitorStatusCommand;
|
|
51
|
+
exports.monitorDaemonStartCommand = monitorDaemonStartCommand;
|
|
52
|
+
exports.monitorDaemonStopCommand = monitorDaemonStopCommand;
|
|
53
|
+
exports.monitorDaemonStatusCommand = monitorDaemonStatusCommand;
|
|
54
|
+
exports.monitorDaemonLogsCommand = monitorDaemonLogsCommand;
|
|
55
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
56
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
57
|
+
const path_1 = __importDefault(require("path"));
|
|
58
|
+
const os_1 = __importDefault(require("os"));
|
|
59
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
60
|
+
const child_process_1 = require("child_process");
|
|
61
|
+
const monitor_1 = require("../types/monitor");
|
|
62
|
+
// Use the centralized paths
|
|
63
|
+
const PATHS = (0, monitor_1.getDefaultMonitorPaths)();
|
|
64
|
+
// For backward compatibility, also define legacy paths
|
|
65
|
+
const LEGACY_CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.contextuate');
|
|
66
|
+
const LEGACY_CONFIG_FILE = path_1.default.join(LEGACY_CONFIG_DIR, 'monitor.config.json');
|
|
67
|
+
const LEGACY_SESSIONS_DIR = path_1.default.join(LEGACY_CONFIG_DIR, 'sessions');
|
|
68
|
+
// Claude settings file (unchanged)
|
|
69
|
+
const CLAUDE_SETTINGS_FILE = path_1.default.join(os_1.default.homedir(), '.claude', 'settings.json');
|
|
70
|
+
/**
|
|
71
|
+
* Migrate from old directory structure to new structure
|
|
72
|
+
*
|
|
73
|
+
* Old structure: ~/.contextuate/{monitor.config.json,sessions/,hooks/}
|
|
74
|
+
* New structure: ~/.contextuate/monitor/{config.json,sessions/,hooks/,raw/,processed/}
|
|
75
|
+
*
|
|
76
|
+
* @returns true if migration was performed, false if nothing to migrate or already migrated
|
|
77
|
+
*/
|
|
78
|
+
async function migrateToNewStructure() {
|
|
79
|
+
// Check if old structure exists
|
|
80
|
+
const legacyConfigExists = await fs_extra_1.default.pathExists(LEGACY_CONFIG_FILE);
|
|
81
|
+
const legacySessionsExists = await fs_extra_1.default.pathExists(LEGACY_SESSIONS_DIR);
|
|
82
|
+
const newConfigExists = await fs_extra_1.default.pathExists(PATHS.configFile);
|
|
83
|
+
if (!legacyConfigExists && !legacySessionsExists) {
|
|
84
|
+
return false; // Nothing to migrate
|
|
85
|
+
}
|
|
86
|
+
if (newConfigExists) {
|
|
87
|
+
return false; // Already migrated
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk_1.default.blue('[Migration] Migrating to new directory structure...'));
|
|
90
|
+
try {
|
|
91
|
+
// Create new directories
|
|
92
|
+
await fs_extra_1.default.mkdir(PATHS.baseDir, { recursive: true });
|
|
93
|
+
await fs_extra_1.default.mkdir(PATHS.rawDir, { recursive: true });
|
|
94
|
+
await fs_extra_1.default.mkdir(PATHS.processedDir, { recursive: true });
|
|
95
|
+
// Move config file
|
|
96
|
+
if (legacyConfigExists) {
|
|
97
|
+
await fs_extra_1.default.rename(LEGACY_CONFIG_FILE, PATHS.configFile);
|
|
98
|
+
console.log(chalk_1.default.green('[Migration] Moved config file'));
|
|
99
|
+
}
|
|
100
|
+
// Move sessions directory
|
|
101
|
+
if (legacySessionsExists) {
|
|
102
|
+
await fs_extra_1.default.rename(LEGACY_SESSIONS_DIR, PATHS.sessionsDir);
|
|
103
|
+
console.log(chalk_1.default.green('[Migration] Moved sessions directory'));
|
|
104
|
+
}
|
|
105
|
+
// Move hooks directory if it exists
|
|
106
|
+
const legacyHooksDir = path_1.default.join(LEGACY_CONFIG_DIR, 'hooks');
|
|
107
|
+
const legacyHooksExists = await fs_extra_1.default.pathExists(legacyHooksDir);
|
|
108
|
+
if (legacyHooksExists) {
|
|
109
|
+
await fs_extra_1.default.rename(legacyHooksDir, PATHS.hooksDir);
|
|
110
|
+
console.log(chalk_1.default.green('[Migration] Moved hooks directory'));
|
|
111
|
+
}
|
|
112
|
+
console.log(chalk_1.default.green('[Migration] Complete! New structure at ~/.contextuate/monitor/'));
|
|
113
|
+
console.log('');
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.log(chalk_1.default.yellow(`[Migration] Warning: Migration partially failed: ${error.message}`));
|
|
118
|
+
console.log(chalk_1.default.yellow('[Migration] You may need to manually move files to ~/.contextuate/monitor/'));
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Load monitor configuration
|
|
124
|
+
*/
|
|
125
|
+
async function loadConfig() {
|
|
126
|
+
try {
|
|
127
|
+
if (await fs_extra_1.default.pathExists(PATHS.configFile)) {
|
|
128
|
+
const content = await fs_extra_1.default.readFile(PATHS.configFile, 'utf-8');
|
|
129
|
+
return { ...monitor_1.DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
// Ignore errors
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Save monitor configuration
|
|
139
|
+
*/
|
|
140
|
+
async function saveConfig(config) {
|
|
141
|
+
await fs_extra_1.default.ensureDir(PATHS.baseDir);
|
|
142
|
+
await fs_extra_1.default.writeFile(PATHS.configFile, JSON.stringify(config, null, 2));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Initialize monitor command
|
|
146
|
+
*/
|
|
147
|
+
async function monitorInitCommand(options) {
|
|
148
|
+
console.log(chalk_1.default.blue(''));
|
|
149
|
+
console.log(chalk_1.default.blue('=========================================='));
|
|
150
|
+
console.log(chalk_1.default.blue(' Contextuate Monitor Setup'));
|
|
151
|
+
console.log(chalk_1.default.blue('=========================================='));
|
|
152
|
+
console.log('');
|
|
153
|
+
try {
|
|
154
|
+
// Attempt migration from old structure
|
|
155
|
+
await migrateToNewStructure();
|
|
156
|
+
// Check for existing configuration
|
|
157
|
+
const existingConfig = await loadConfig();
|
|
158
|
+
if (existingConfig) {
|
|
159
|
+
const { overwrite } = await inquirer_1.default.prompt([
|
|
160
|
+
{
|
|
161
|
+
type: 'confirm',
|
|
162
|
+
name: 'overwrite',
|
|
163
|
+
message: 'Configuration already exists. Overwrite?',
|
|
164
|
+
default: false,
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
if (!overwrite) {
|
|
168
|
+
console.log(chalk_1.default.yellow('[INFO] Setup cancelled.'));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Step 1: Choose mode
|
|
173
|
+
const { mode } = await inquirer_1.default.prompt([
|
|
174
|
+
{
|
|
175
|
+
type: 'list',
|
|
176
|
+
name: 'mode',
|
|
177
|
+
message: 'Select monitor mode:',
|
|
178
|
+
choices: [
|
|
179
|
+
{ name: 'Local (Unix socket) - Single machine', value: 'local' },
|
|
180
|
+
{ name: 'Redis (pub/sub) - Distributed/multi-machine', value: 'redis' },
|
|
181
|
+
],
|
|
182
|
+
default: 'local',
|
|
183
|
+
},
|
|
184
|
+
]);
|
|
185
|
+
const config = { ...monitor_1.DEFAULT_CONFIG, mode: mode };
|
|
186
|
+
// Step 2: Redis configuration (if selected)
|
|
187
|
+
if (mode === 'redis') {
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log(chalk_1.default.blue('[INFO] Configure Redis connection:'));
|
|
190
|
+
const redisAnswers = await inquirer_1.default.prompt([
|
|
191
|
+
{
|
|
192
|
+
type: 'input',
|
|
193
|
+
name: 'host',
|
|
194
|
+
message: 'Redis host:',
|
|
195
|
+
default: 'localhost',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: 'number',
|
|
199
|
+
name: 'port',
|
|
200
|
+
message: 'Redis port:',
|
|
201
|
+
default: 6379,
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
type: 'password',
|
|
205
|
+
name: 'password',
|
|
206
|
+
message: 'Redis password (leave empty for none):',
|
|
207
|
+
default: '',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
type: 'input',
|
|
211
|
+
name: 'channel',
|
|
212
|
+
message: 'Redis channel:',
|
|
213
|
+
default: 'contextuate:events',
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
config.redis = {
|
|
217
|
+
host: redisAnswers.host,
|
|
218
|
+
port: redisAnswers.port,
|
|
219
|
+
password: redisAnswers.password || null,
|
|
220
|
+
channel: redisAnswers.channel,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// Step 3: Server configuration
|
|
224
|
+
console.log('');
|
|
225
|
+
console.log(chalk_1.default.blue('[INFO] Configure server ports:'));
|
|
226
|
+
const serverAnswers = await inquirer_1.default.prompt([
|
|
227
|
+
{
|
|
228
|
+
type: 'input',
|
|
229
|
+
name: 'host',
|
|
230
|
+
message: 'Server host:',
|
|
231
|
+
default: '0.0.0.0',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
type: 'number',
|
|
235
|
+
name: 'port',
|
|
236
|
+
message: 'HTTP port:',
|
|
237
|
+
default: 3847,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
type: 'number',
|
|
241
|
+
name: 'wsPort',
|
|
242
|
+
message: 'WebSocket port:',
|
|
243
|
+
default: 3848,
|
|
244
|
+
},
|
|
245
|
+
]);
|
|
246
|
+
config.server = {
|
|
247
|
+
host: serverAnswers.host,
|
|
248
|
+
port: serverAnswers.port,
|
|
249
|
+
wsPort: serverAnswers.wsPort,
|
|
250
|
+
};
|
|
251
|
+
// Step 4: Persistence configuration
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log(chalk_1.default.blue('[INFO] Configure data persistence:'));
|
|
254
|
+
const { persistenceType } = await inquirer_1.default.prompt([
|
|
255
|
+
{
|
|
256
|
+
type: 'list',
|
|
257
|
+
name: 'persistenceType',
|
|
258
|
+
message: 'Persistence type:',
|
|
259
|
+
choices: [
|
|
260
|
+
{ name: 'File-based (recommended)', value: 'file' },
|
|
261
|
+
{ name: 'MySQL (coming soon)', value: 'mysql', disabled: true },
|
|
262
|
+
{ name: 'PostgreSQL (coming soon)', value: 'postgresql', disabled: true },
|
|
263
|
+
],
|
|
264
|
+
default: 'file',
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
config.persistence = {
|
|
268
|
+
enabled: true,
|
|
269
|
+
type: persistenceType,
|
|
270
|
+
};
|
|
271
|
+
// Step 5: Socket path (local mode only)
|
|
272
|
+
if (mode === 'local') {
|
|
273
|
+
const { socketPath } = await inquirer_1.default.prompt([
|
|
274
|
+
{
|
|
275
|
+
type: 'input',
|
|
276
|
+
name: 'socketPath',
|
|
277
|
+
message: 'Unix socket path:',
|
|
278
|
+
default: '/tmp/contextuate-monitor.sock',
|
|
279
|
+
},
|
|
280
|
+
]);
|
|
281
|
+
config.socketPath = socketPath;
|
|
282
|
+
}
|
|
283
|
+
// Create new directory structure
|
|
284
|
+
await fs_extra_1.default.ensureDir(PATHS.baseDir);
|
|
285
|
+
await fs_extra_1.default.ensureDir(PATHS.rawDir);
|
|
286
|
+
await fs_extra_1.default.ensureDir(PATHS.processedDir);
|
|
287
|
+
await fs_extra_1.default.ensureDir(PATHS.sessionsDir);
|
|
288
|
+
await fs_extra_1.default.ensureDir(PATHS.hooksDir);
|
|
289
|
+
// Save configuration
|
|
290
|
+
await saveConfig(config);
|
|
291
|
+
console.log('');
|
|
292
|
+
console.log(chalk_1.default.green(`[OK] Configuration saved to ${PATHS.configFile}`));
|
|
293
|
+
// Step 6: Install hook script
|
|
294
|
+
console.log('');
|
|
295
|
+
console.log(chalk_1.default.blue('[INFO] Installing hook script...'));
|
|
296
|
+
// Find the hook script source
|
|
297
|
+
let hookSource = path_1.default.join(__dirname, '../monitor/hooks/emit-event.js');
|
|
298
|
+
if (!await fs_extra_1.default.pathExists(hookSource)) {
|
|
299
|
+
hookSource = path_1.default.join(__dirname, '../../src/monitor/hooks/emit-event.js');
|
|
300
|
+
}
|
|
301
|
+
const hookDest = path_1.default.join(PATHS.hooksDir, 'emit-event.js');
|
|
302
|
+
if (await fs_extra_1.default.pathExists(hookSource)) {
|
|
303
|
+
await fs_extra_1.default.copy(hookSource, hookDest);
|
|
304
|
+
await fs_extra_1.default.chmod(hookDest, 0o755);
|
|
305
|
+
console.log(chalk_1.default.green(`[OK] Hook script installed to ${hookDest}`));
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
console.log(chalk_1.default.yellow(`[WARN] Hook script source not found. You may need to copy it manually.`));
|
|
309
|
+
}
|
|
310
|
+
// Step 7: Determine hook location (from flags or interactive)
|
|
311
|
+
console.log('');
|
|
312
|
+
let hookLocation;
|
|
313
|
+
// Check if flags were provided
|
|
314
|
+
if (options?.global && options?.project) {
|
|
315
|
+
console.log(chalk_1.default.yellow('[WARN] Both --global and --project flags specified. Using --global (default).'));
|
|
316
|
+
hookLocation = 'home';
|
|
317
|
+
}
|
|
318
|
+
else if (options?.global) {
|
|
319
|
+
hookLocation = 'home';
|
|
320
|
+
console.log(chalk_1.default.blue('[INFO] Installing hooks at user level (--global)'));
|
|
321
|
+
}
|
|
322
|
+
else if (options?.project) {
|
|
323
|
+
hookLocation = 'project';
|
|
324
|
+
console.log(chalk_1.default.blue('[INFO] Installing hooks at project level (--project)'));
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
// No flags provided - ask interactively (default to global/home)
|
|
328
|
+
const answer = await inquirer_1.default.prompt([
|
|
329
|
+
{
|
|
330
|
+
type: 'list',
|
|
331
|
+
name: 'hookLocation',
|
|
332
|
+
message: 'Where should hooks be registered?',
|
|
333
|
+
choices: [
|
|
334
|
+
{ name: 'User level (~/.claude/settings.json - applies to all projects) - DEFAULT', value: 'home' },
|
|
335
|
+
{ name: 'Project level (.claude/settings.json in current directory)', value: 'project' },
|
|
336
|
+
{ name: 'Skip - I will configure hooks manually', value: 'skip' },
|
|
337
|
+
],
|
|
338
|
+
default: 'home',
|
|
339
|
+
},
|
|
340
|
+
]);
|
|
341
|
+
hookLocation = answer.hookLocation;
|
|
342
|
+
}
|
|
343
|
+
if (hookLocation !== 'skip') {
|
|
344
|
+
const settingsFile = hookLocation === 'project'
|
|
345
|
+
? path_1.default.join(process.cwd(), '.claude', 'settings.json')
|
|
346
|
+
: CLAUDE_SETTINGS_FILE;
|
|
347
|
+
await updateClaudeHookSettings(hookDest, settingsFile);
|
|
348
|
+
}
|
|
349
|
+
// Done!
|
|
350
|
+
console.log('');
|
|
351
|
+
console.log(chalk_1.default.green('=========================================='));
|
|
352
|
+
console.log(chalk_1.default.green(' Setup Complete!'));
|
|
353
|
+
console.log(chalk_1.default.green('=========================================='));
|
|
354
|
+
console.log('');
|
|
355
|
+
console.log('Next steps:');
|
|
356
|
+
console.log(` 1. Start the monitor: ${chalk_1.default.cyan('contextuate monitor')}`);
|
|
357
|
+
console.log(` 2. Open your browser to: ${chalk_1.default.cyan(`http://localhost:${config.server.port}`)}`);
|
|
358
|
+
console.log(` 3. Start a Claude Code session - events will appear in the dashboard`);
|
|
359
|
+
console.log('');
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
if (error.name === 'ExitPromptError' || error.message?.includes('User force closed')) {
|
|
363
|
+
console.log('');
|
|
364
|
+
console.log(chalk_1.default.yellow('Setup cancelled.'));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Check if a hook command path refers to a contextuate emit-event hook
|
|
372
|
+
*/
|
|
373
|
+
function isContextuateHook(command) {
|
|
374
|
+
// Normalize the path and check for contextuate emit-event patterns
|
|
375
|
+
const normalized = command.replace(/^~/, os_1.default.homedir());
|
|
376
|
+
return normalized.includes('contextuate') && normalized.includes('emit-event');
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Update Claude settings with hook registrations
|
|
380
|
+
* - Detects existing contextuate hooks (any path format)
|
|
381
|
+
* - Updates them to use the canonical path
|
|
382
|
+
* - Removes duplicates
|
|
383
|
+
* - Adds hooks for any missing hook types
|
|
384
|
+
*/
|
|
385
|
+
async function updateClaudeHookSettings(hookPath, settingsFile) {
|
|
386
|
+
try {
|
|
387
|
+
const claudeDir = path_1.default.dirname(settingsFile);
|
|
388
|
+
await fs_extra_1.default.ensureDir(claudeDir);
|
|
389
|
+
let settings = {};
|
|
390
|
+
if (await fs_extra_1.default.pathExists(settingsFile)) {
|
|
391
|
+
try {
|
|
392
|
+
const content = await fs_extra_1.default.readFile(settingsFile, 'utf-8');
|
|
393
|
+
settings = JSON.parse(content);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// Start fresh if parsing fails
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// All hook types we want to register
|
|
400
|
+
const hookTypes = [
|
|
401
|
+
'SessionStart',
|
|
402
|
+
'SessionEnd',
|
|
403
|
+
'PreToolUse',
|
|
404
|
+
'PostToolUse',
|
|
405
|
+
'Notification',
|
|
406
|
+
'Stop',
|
|
407
|
+
'SubagentStart',
|
|
408
|
+
'SubagentStop',
|
|
409
|
+
'UserPromptSubmit'
|
|
410
|
+
];
|
|
411
|
+
// Initialize hooks object if needed
|
|
412
|
+
if (!settings.hooks || typeof settings.hooks !== 'object') {
|
|
413
|
+
settings.hooks = {};
|
|
414
|
+
}
|
|
415
|
+
const hooks = settings.hooks;
|
|
416
|
+
// Process each hook type
|
|
417
|
+
for (const hookType of hookTypes) {
|
|
418
|
+
if (!Array.isArray(hooks[hookType])) {
|
|
419
|
+
hooks[hookType] = [];
|
|
420
|
+
}
|
|
421
|
+
// Filter out any existing contextuate hooks and other duplicates
|
|
422
|
+
const filteredEntries = [];
|
|
423
|
+
let hasContextuateHook = false;
|
|
424
|
+
for (const entry of hooks[hookType]) {
|
|
425
|
+
if (!entry.hooks || !Array.isArray(entry.hooks)) {
|
|
426
|
+
filteredEntries.push(entry);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
// Filter hooks within this entry
|
|
430
|
+
const nonContextuateHooks = entry.hooks.filter((h) => {
|
|
431
|
+
if (h.type === 'command' && isContextuateHook(h.command)) {
|
|
432
|
+
hasContextuateHook = true;
|
|
433
|
+
return false; // Remove old contextuate hooks
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
});
|
|
437
|
+
// Keep entry if it has other hooks
|
|
438
|
+
if (nonContextuateHooks.length > 0) {
|
|
439
|
+
filteredEntries.push({ ...entry, hooks: nonContextuateHooks });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Add our canonical hook entry
|
|
443
|
+
filteredEntries.push({
|
|
444
|
+
matcher: '',
|
|
445
|
+
hooks: [{ type: 'command', command: hookPath }]
|
|
446
|
+
});
|
|
447
|
+
hooks[hookType] = filteredEntries;
|
|
448
|
+
if (hasContextuateHook) {
|
|
449
|
+
console.log(chalk_1.default.blue(`[INFO] Updated ${hookType} hook to canonical path`));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Save settings
|
|
453
|
+
await fs_extra_1.default.writeFile(settingsFile, JSON.stringify(settings, null, 2));
|
|
454
|
+
console.log(chalk_1.default.green(`[OK] Hooks registered in ${settingsFile}`));
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
console.log(chalk_1.default.yellow(`[WARN] Could not update Claude settings: ${err}`));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Start monitor server command
|
|
462
|
+
*/
|
|
463
|
+
async function monitorStartCommand(options) {
|
|
464
|
+
// Attempt migration from old structure
|
|
465
|
+
await migrateToNewStructure();
|
|
466
|
+
// Load configuration
|
|
467
|
+
let config = await loadConfig();
|
|
468
|
+
if (!config) {
|
|
469
|
+
console.log(chalk_1.default.yellow('[WARN] No configuration found. Running init...'));
|
|
470
|
+
console.log('');
|
|
471
|
+
await monitorInitCommand();
|
|
472
|
+
config = await loadConfig();
|
|
473
|
+
if (!config) {
|
|
474
|
+
console.log(chalk_1.default.red('[ERROR] Setup failed or cancelled.'));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Apply command-line overrides
|
|
479
|
+
if (options.port) {
|
|
480
|
+
config.server.port = options.port;
|
|
481
|
+
}
|
|
482
|
+
if (options.wsPort) {
|
|
483
|
+
config.server.wsPort = options.wsPort;
|
|
484
|
+
}
|
|
485
|
+
// Auto-start daemon if not running
|
|
486
|
+
await ensureDaemonRunning(config);
|
|
487
|
+
// Check if server is already running
|
|
488
|
+
const serverPid = await getServerPid();
|
|
489
|
+
if (serverPid && isProcessRunning(serverPid)) {
|
|
490
|
+
console.log(chalk_1.default.blue(`[Info] Server already running (PID: ${serverPid})`));
|
|
491
|
+
const url = `http://localhost:${config.server.port}`;
|
|
492
|
+
if (!options.noOpen) {
|
|
493
|
+
openBrowser(url);
|
|
494
|
+
}
|
|
495
|
+
console.log(chalk_1.default.green(`[OK] Monitor: ${url}`));
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}
|
|
498
|
+
// Clean up stale PID file if server not running
|
|
499
|
+
if (serverPid) {
|
|
500
|
+
await fs_extra_1.default.remove(PATHS.serverPidFile);
|
|
501
|
+
}
|
|
502
|
+
// Run in foreground mode if requested
|
|
503
|
+
if (options.foreground) {
|
|
504
|
+
await runServerForeground(config, options);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
// Start server in background
|
|
508
|
+
await startServerBackground(config, options);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Run the server in foreground mode (blocking)
|
|
512
|
+
*/
|
|
513
|
+
async function runServerForeground(config, options) {
|
|
514
|
+
console.log(chalk_1.default.blue('[INFO] Starting Contextuate Monitor in foreground...'));
|
|
515
|
+
console.log('');
|
|
516
|
+
try {
|
|
517
|
+
const { createMonitorServer } = await Promise.resolve().then(() => __importStar(require('../monitor/server')));
|
|
518
|
+
const server = await createMonitorServer({
|
|
519
|
+
config,
|
|
520
|
+
dataDir: PATHS.baseDir
|
|
521
|
+
});
|
|
522
|
+
await server.start();
|
|
523
|
+
const url = `http://localhost:${config.server.port}`;
|
|
524
|
+
if (!options.noOpen) {
|
|
525
|
+
openBrowser(url);
|
|
526
|
+
}
|
|
527
|
+
// Handle shutdown signals
|
|
528
|
+
const shutdown = async () => {
|
|
529
|
+
console.log('');
|
|
530
|
+
console.log(chalk_1.default.blue('[INFO] Shutting down...'));
|
|
531
|
+
await server.stop();
|
|
532
|
+
process.exit(0);
|
|
533
|
+
};
|
|
534
|
+
process.on('SIGINT', shutdown);
|
|
535
|
+
process.on('SIGTERM', shutdown);
|
|
536
|
+
console.log(chalk_1.default.green('[OK] Monitor is running. Press Ctrl+C to stop.'));
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
console.error(chalk_1.default.red(`[ERROR] Failed to start server: ${err.message}`));
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Start the server in background mode (non-blocking)
|
|
545
|
+
*/
|
|
546
|
+
async function startServerBackground(config, options) {
|
|
547
|
+
console.log(chalk_1.default.blue('[INFO] Starting Contextuate Monitor...'));
|
|
548
|
+
// Find the server CLI entry point
|
|
549
|
+
let serverPath = path_1.default.join(__dirname, '..', 'monitor', 'server', 'cli.js');
|
|
550
|
+
if (!await fs_extra_1.default.pathExists(serverPath)) {
|
|
551
|
+
const alternatives = [
|
|
552
|
+
path_1.default.join(__dirname, '..', '..', 'dist', 'monitor', 'server', 'cli.js'),
|
|
553
|
+
path_1.default.join(__dirname, 'monitor', 'server', 'cli.js'),
|
|
554
|
+
];
|
|
555
|
+
for (const altPath of alternatives) {
|
|
556
|
+
if (await fs_extra_1.default.pathExists(altPath)) {
|
|
557
|
+
serverPath = altPath;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (!await fs_extra_1.default.pathExists(serverPath)) {
|
|
563
|
+
console.log(chalk_1.default.red(`[Error] Server CLI not found at ${serverPath}`));
|
|
564
|
+
console.log(chalk_1.default.yellow('[Info] Try running: npm run build'));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// Build command arguments
|
|
568
|
+
const args = [
|
|
569
|
+
serverPath,
|
|
570
|
+
'--config', PATHS.configFile,
|
|
571
|
+
];
|
|
572
|
+
if (options.port) {
|
|
573
|
+
args.push('--port', options.port.toString());
|
|
574
|
+
}
|
|
575
|
+
if (options.wsPort) {
|
|
576
|
+
args.push('--ws-port', options.wsPort.toString());
|
|
577
|
+
}
|
|
578
|
+
// Ensure log directory exists and open log file
|
|
579
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(PATHS.serverLogFile));
|
|
580
|
+
const logFd = fs_extra_1.default.openSync(PATHS.serverLogFile, 'a');
|
|
581
|
+
const child = (0, child_process_1.spawn)(process.execPath, args, {
|
|
582
|
+
detached: true,
|
|
583
|
+
stdio: ['ignore', logFd, logFd],
|
|
584
|
+
});
|
|
585
|
+
// Close file descriptor in parent
|
|
586
|
+
fs_extra_1.default.closeSync(logFd);
|
|
587
|
+
// Write PID file
|
|
588
|
+
await fs_extra_1.default.writeFile(PATHS.serverPidFile, child.pid.toString());
|
|
589
|
+
child.unref();
|
|
590
|
+
// Wait briefly for server to start
|
|
591
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
592
|
+
// Verify server started successfully
|
|
593
|
+
const url = `http://localhost:${config.server.port}`;
|
|
594
|
+
try {
|
|
595
|
+
const response = await fetch(`${url}/api/status`);
|
|
596
|
+
if (response.ok) {
|
|
597
|
+
console.log(chalk_1.default.green(`[OK] Server started (PID: ${child.pid})`));
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
console.log(chalk_1.default.yellow(`[WARN] Server may not have started properly. Check logs: ${PATHS.serverLogFile}`));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
console.log(chalk_1.default.yellow(`[WARN] Could not verify server. Check logs: ${PATHS.serverLogFile}`));
|
|
605
|
+
}
|
|
606
|
+
// Open browser
|
|
607
|
+
if (!options.noOpen) {
|
|
608
|
+
openBrowser(url);
|
|
609
|
+
}
|
|
610
|
+
console.log('');
|
|
611
|
+
console.log(chalk_1.default.green(`Monitor: ${url}`));
|
|
612
|
+
console.log(chalk_1.default.blue(`Logs: ${PATHS.serverLogFile}`));
|
|
613
|
+
console.log('');
|
|
614
|
+
console.log(`To stop: ${chalk_1.default.cyan('contextuate monitor stop')}`);
|
|
615
|
+
// Exit cleanly - background processes are now running independently
|
|
616
|
+
process.exit(0);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Open browser to URL (fire-and-forget, doesn't block)
|
|
620
|
+
*/
|
|
621
|
+
function openBrowser(url) {
|
|
622
|
+
const platform = process.platform;
|
|
623
|
+
const command = platform === 'darwin' ? 'open' :
|
|
624
|
+
platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
625
|
+
const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
626
|
+
try {
|
|
627
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
628
|
+
detached: true,
|
|
629
|
+
stdio: 'ignore',
|
|
630
|
+
});
|
|
631
|
+
child.unref();
|
|
632
|
+
}
|
|
633
|
+
catch (err) {
|
|
634
|
+
console.log(chalk_1.default.yellow(`[WARN] Could not open browser. Please open ${url} manually.`));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Get server PID from PID file
|
|
639
|
+
*/
|
|
640
|
+
async function getServerPid() {
|
|
641
|
+
try {
|
|
642
|
+
const pidStr = await fs_extra_1.default.readFile(PATHS.serverPidFile, 'utf-8');
|
|
643
|
+
return parseInt(pidStr.trim(), 10);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Stop monitor server command
|
|
651
|
+
*/
|
|
652
|
+
async function monitorStopCommand(options) {
|
|
653
|
+
let stoppedServer = false;
|
|
654
|
+
let stoppedDaemon = false;
|
|
655
|
+
// Stop UI server
|
|
656
|
+
const serverPid = await getServerPid();
|
|
657
|
+
if (serverPid && isProcessRunning(serverPid)) {
|
|
658
|
+
console.log(chalk_1.default.blue(`[INFO] Stopping UI server (PID: ${serverPid})...`));
|
|
659
|
+
try {
|
|
660
|
+
process.kill(serverPid, 'SIGTERM');
|
|
661
|
+
// Wait for process to exit
|
|
662
|
+
let attempts = 0;
|
|
663
|
+
while (isProcessRunning(serverPid) && attempts < 10) {
|
|
664
|
+
await new Promise(r => setTimeout(r, 500));
|
|
665
|
+
attempts++;
|
|
666
|
+
}
|
|
667
|
+
if (isProcessRunning(serverPid)) {
|
|
668
|
+
console.log(chalk_1.default.yellow('[WARN] Server still running, sending SIGKILL'));
|
|
669
|
+
process.kill(serverPid, 'SIGKILL');
|
|
670
|
+
}
|
|
671
|
+
await fs_extra_1.default.remove(PATHS.serverPidFile);
|
|
672
|
+
console.log(chalk_1.default.green('[OK] UI server stopped'));
|
|
673
|
+
stoppedServer = true;
|
|
674
|
+
}
|
|
675
|
+
catch (err) {
|
|
676
|
+
console.error(chalk_1.default.red(`[ERROR] Failed to stop server: ${err.message}`));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
if (serverPid) {
|
|
681
|
+
// Clean up stale PID file
|
|
682
|
+
await fs_extra_1.default.remove(PATHS.serverPidFile);
|
|
683
|
+
}
|
|
684
|
+
console.log(chalk_1.default.blue('[INFO] UI server not running'));
|
|
685
|
+
}
|
|
686
|
+
// Stop daemon if --all flag is specified
|
|
687
|
+
if (options.all) {
|
|
688
|
+
const daemonPid = await getDaemonPid();
|
|
689
|
+
if (daemonPid && isProcessRunning(daemonPid)) {
|
|
690
|
+
console.log(chalk_1.default.blue(`[INFO] Stopping daemon (PID: ${daemonPid})...`));
|
|
691
|
+
await monitorDaemonStopCommand();
|
|
692
|
+
stoppedDaemon = true;
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
console.log(chalk_1.default.blue('[INFO] Daemon not running'));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (!stoppedServer && !stoppedDaemon) {
|
|
699
|
+
console.log('');
|
|
700
|
+
console.log(chalk_1.default.blue('[INFO] Nothing to stop'));
|
|
701
|
+
if (!options.all) {
|
|
702
|
+
console.log(chalk_1.default.blue('[INFO] Use --all to also stop the daemon'));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Show monitor status command
|
|
708
|
+
*/
|
|
709
|
+
async function monitorStatusCommand() {
|
|
710
|
+
const config = await loadConfig();
|
|
711
|
+
console.log(chalk_1.default.blue(''));
|
|
712
|
+
console.log(chalk_1.default.blue('=========================================='));
|
|
713
|
+
console.log(chalk_1.default.blue(' Contextuate Monitor Status'));
|
|
714
|
+
console.log(chalk_1.default.blue('=========================================='));
|
|
715
|
+
console.log('');
|
|
716
|
+
if (!config) {
|
|
717
|
+
console.log(chalk_1.default.yellow('Status: Not configured'));
|
|
718
|
+
console.log('');
|
|
719
|
+
console.log(`Run ${chalk_1.default.cyan('contextuate monitor init')} to set up the monitor.`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
// Configuration info
|
|
723
|
+
console.log(chalk_1.default.white('Configuration:'));
|
|
724
|
+
console.log(` Mode: ${chalk_1.default.cyan(config.mode)}`);
|
|
725
|
+
console.log(` HTTP Port: ${chalk_1.default.cyan(config.server.port)}`);
|
|
726
|
+
console.log(` WS Port: ${chalk_1.default.cyan(config.server.wsPort)}`);
|
|
727
|
+
if (config.mode === 'redis') {
|
|
728
|
+
console.log(` Redis: ${chalk_1.default.cyan(`${config.redis.host}:${config.redis.port}`)}`);
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
console.log(` Socket: ${chalk_1.default.cyan(config.socketPath)}`);
|
|
732
|
+
}
|
|
733
|
+
console.log(` Persistence: ${chalk_1.default.cyan(config.persistence.type)}`);
|
|
734
|
+
console.log('');
|
|
735
|
+
// Check daemon status
|
|
736
|
+
console.log(chalk_1.default.white('Daemon Status:'));
|
|
737
|
+
const pid = await getDaemonPid();
|
|
738
|
+
if (pid && isProcessRunning(pid)) {
|
|
739
|
+
console.log(` Status: ${chalk_1.default.green('Running')} (PID: ${pid})`);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
console.log(` Status: ${chalk_1.default.yellow('Not running')}`);
|
|
743
|
+
}
|
|
744
|
+
console.log('');
|
|
745
|
+
// Check if server is running
|
|
746
|
+
console.log(chalk_1.default.white('UI Server Status:'));
|
|
747
|
+
const serverPid = await getServerPid();
|
|
748
|
+
try {
|
|
749
|
+
const response = await fetch(`http://localhost:${config.server.port}/api/status`);
|
|
750
|
+
if (response.ok) {
|
|
751
|
+
const status = await response.json();
|
|
752
|
+
console.log(` Status: ${chalk_1.default.green('Running')}${serverPid ? ` (PID: ${serverPid})` : ''}`);
|
|
753
|
+
console.log(` Sessions: ${chalk_1.default.cyan(status.sessions)} total, ${chalk_1.default.cyan(status.activeSessions)} active`);
|
|
754
|
+
console.log(` Uptime: ${chalk_1.default.cyan(formatUptime(status.uptime))}`);
|
|
755
|
+
console.log('');
|
|
756
|
+
console.log(`Dashboard: ${chalk_1.default.cyan(`http://localhost:${config.server.port}`)}`);
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
console.log(` Status: ${chalk_1.default.red('Error (HTTP ' + response.status + ')')}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
if (serverPid && isProcessRunning(serverPid)) {
|
|
764
|
+
console.log(` Status: ${chalk_1.default.yellow('Starting...')} (PID: ${serverPid})`);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
if (serverPid) {
|
|
768
|
+
// Clean up stale PID file
|
|
769
|
+
await fs_extra_1.default.remove(PATHS.serverPidFile);
|
|
770
|
+
}
|
|
771
|
+
console.log(` Status: ${chalk_1.default.yellow('Not running')}`);
|
|
772
|
+
console.log('');
|
|
773
|
+
console.log(`Start with: ${chalk_1.default.cyan('contextuate monitor')}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
console.log('');
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Format uptime in human-readable form
|
|
780
|
+
*/
|
|
781
|
+
function formatUptime(seconds) {
|
|
782
|
+
const hours = Math.floor(seconds / 3600);
|
|
783
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
784
|
+
const secs = Math.floor(seconds % 60);
|
|
785
|
+
const parts = [];
|
|
786
|
+
if (hours > 0)
|
|
787
|
+
parts.push(`${hours}h`);
|
|
788
|
+
if (minutes > 0)
|
|
789
|
+
parts.push(`${minutes}m`);
|
|
790
|
+
parts.push(`${secs}s`);
|
|
791
|
+
return parts.join(' ');
|
|
792
|
+
}
|
|
793
|
+
// =============================================================================
|
|
794
|
+
// Daemon Management Helper Functions
|
|
795
|
+
// =============================================================================
|
|
796
|
+
/**
|
|
797
|
+
* Get daemon PID from PID file
|
|
798
|
+
*/
|
|
799
|
+
async function getDaemonPid() {
|
|
800
|
+
try {
|
|
801
|
+
const pidStr = await fs_extra_1.default.readFile(PATHS.daemonPidFile, 'utf-8');
|
|
802
|
+
return parseInt(pidStr.trim(), 10);
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Check if a process is running
|
|
810
|
+
*/
|
|
811
|
+
function isProcessRunning(pid) {
|
|
812
|
+
try {
|
|
813
|
+
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Ensure daemon is running
|
|
822
|
+
* - If not running, start it
|
|
823
|
+
* - Clean up stale PID files if process not running
|
|
824
|
+
*/
|
|
825
|
+
async function ensureDaemonRunning(config) {
|
|
826
|
+
const pid = await getDaemonPid();
|
|
827
|
+
const socketPath = '/tmp/contextuate-daemon.sock';
|
|
828
|
+
if (pid && isProcessRunning(pid)) {
|
|
829
|
+
// Daemon is already running
|
|
830
|
+
console.log(chalk_1.default.green(`[OK] Daemon is running (PID: ${pid})`));
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (pid) {
|
|
834
|
+
// PID file exists but process not running - clean up stale files
|
|
835
|
+
console.log(chalk_1.default.blue('[INFO] Cleaning up stale daemon files...'));
|
|
836
|
+
await fs_extra_1.default.remove(PATHS.daemonPidFile);
|
|
837
|
+
try {
|
|
838
|
+
await fs_extra_1.default.remove(socketPath);
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
// Ignore
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// Start the daemon
|
|
845
|
+
console.log(chalk_1.default.blue('[INFO] Starting daemon...'));
|
|
846
|
+
await monitorDaemonStartCommand({ detach: true });
|
|
847
|
+
// Brief wait for daemon to initialize socket
|
|
848
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
849
|
+
console.log('');
|
|
850
|
+
}
|
|
851
|
+
// =============================================================================
|
|
852
|
+
// Daemon Management Commands
|
|
853
|
+
// =============================================================================
|
|
854
|
+
/**
|
|
855
|
+
* Start daemon command
|
|
856
|
+
*/
|
|
857
|
+
async function monitorDaemonStartCommand(options) {
|
|
858
|
+
const config = await loadConfig();
|
|
859
|
+
if (!config) {
|
|
860
|
+
console.log(chalk_1.default.red('[Error] Monitor not initialized. Run: contextuate monitor init'));
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
// Check if already running
|
|
864
|
+
const pid = await getDaemonPid();
|
|
865
|
+
if (pid && isProcessRunning(pid)) {
|
|
866
|
+
console.log(chalk_1.default.blue(`[Info] Daemon already running (PID: ${pid})`));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (options.detach) {
|
|
870
|
+
// Start as background process
|
|
871
|
+
// Find the daemon CLI entry point
|
|
872
|
+
let daemonPath = path_1.default.join(__dirname, '..', 'monitor', 'daemon', 'cli.js');
|
|
873
|
+
if (!await fs_extra_1.default.pathExists(daemonPath)) {
|
|
874
|
+
// Try alternative paths
|
|
875
|
+
const alternatives = [
|
|
876
|
+
path_1.default.join(__dirname, '..', '..', 'dist', 'monitor', 'daemon', 'cli.js'),
|
|
877
|
+
path_1.default.join(__dirname, 'monitor', 'daemon', 'cli.js'),
|
|
878
|
+
];
|
|
879
|
+
for (const altPath of alternatives) {
|
|
880
|
+
if (await fs_extra_1.default.pathExists(altPath)) {
|
|
881
|
+
daemonPath = altPath;
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (!await fs_extra_1.default.pathExists(daemonPath)) {
|
|
887
|
+
console.log(chalk_1.default.red(`[Error] Daemon CLI not found at ${daemonPath}`));
|
|
888
|
+
console.log(chalk_1.default.yellow('[Info] Try running: npm run build'));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
// Ensure log directory exists and open log file for daemon output
|
|
892
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(PATHS.daemonLogFile));
|
|
893
|
+
const logFd = fs_extra_1.default.openSync(PATHS.daemonLogFile, 'a');
|
|
894
|
+
const child = (0, child_process_1.spawn)(process.execPath, [
|
|
895
|
+
daemonPath,
|
|
896
|
+
'--config', PATHS.configFile,
|
|
897
|
+
], {
|
|
898
|
+
detached: true,
|
|
899
|
+
// Use file descriptor directly so daemon doesn't depend on parent's pipes
|
|
900
|
+
stdio: ['ignore', logFd, logFd],
|
|
901
|
+
});
|
|
902
|
+
// Close the file descriptor in the parent - the child has its own copy
|
|
903
|
+
fs_extra_1.default.closeSync(logFd);
|
|
904
|
+
// Write PID file
|
|
905
|
+
await fs_extra_1.default.writeFile(PATHS.daemonPidFile, child.pid.toString());
|
|
906
|
+
child.unref();
|
|
907
|
+
console.log(chalk_1.default.green(`[OK] Daemon started in background (PID: ${child.pid})`));
|
|
908
|
+
console.log(chalk_1.default.blue(`[Info] Logs: ${PATHS.daemonLogFile}`));
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
// Run in foreground
|
|
912
|
+
console.log(chalk_1.default.blue('[Info] Starting daemon in foreground (Ctrl+C to stop)'));
|
|
913
|
+
const { startDaemon } = await Promise.resolve().then(() => __importStar(require('../monitor/daemon/index.js')));
|
|
914
|
+
const daemon = await startDaemon(config);
|
|
915
|
+
// Handle shutdown
|
|
916
|
+
const shutdown = async () => {
|
|
917
|
+
console.log('');
|
|
918
|
+
console.log(chalk_1.default.blue('[Info] Shutting down daemon...'));
|
|
919
|
+
await daemon.stop();
|
|
920
|
+
process.exit(0);
|
|
921
|
+
};
|
|
922
|
+
process.on('SIGINT', shutdown);
|
|
923
|
+
process.on('SIGTERM', shutdown);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Stop daemon command
|
|
928
|
+
*/
|
|
929
|
+
async function monitorDaemonStopCommand() {
|
|
930
|
+
const pid = await getDaemonPid();
|
|
931
|
+
if (!pid) {
|
|
932
|
+
console.log(chalk_1.default.blue('[Info] Daemon not running (no PID file)'));
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (!isProcessRunning(pid)) {
|
|
936
|
+
console.log(chalk_1.default.blue('[Info] Daemon not running (stale PID file)'));
|
|
937
|
+
await fs_extra_1.default.remove(PATHS.daemonPidFile);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
process.kill(pid, 'SIGTERM');
|
|
942
|
+
console.log(chalk_1.default.green(`[OK] Sent shutdown signal to daemon (PID: ${pid})`));
|
|
943
|
+
// Wait for process to exit
|
|
944
|
+
let attempts = 0;
|
|
945
|
+
while (isProcessRunning(pid) && attempts < 10) {
|
|
946
|
+
await new Promise(r => setTimeout(r, 500));
|
|
947
|
+
attempts++;
|
|
948
|
+
}
|
|
949
|
+
if (isProcessRunning(pid)) {
|
|
950
|
+
console.log(chalk_1.default.yellow('[Warning] Daemon still running, sending SIGKILL'));
|
|
951
|
+
process.kill(pid, 'SIGKILL');
|
|
952
|
+
}
|
|
953
|
+
await fs_extra_1.default.remove(PATHS.daemonPidFile);
|
|
954
|
+
console.log(chalk_1.default.green('[OK] Daemon stopped'));
|
|
955
|
+
}
|
|
956
|
+
catch (err) {
|
|
957
|
+
console.error(chalk_1.default.red(`[Error] Failed to stop daemon: ${err.message}`));
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Show daemon status command
|
|
962
|
+
*/
|
|
963
|
+
async function monitorDaemonStatusCommand() {
|
|
964
|
+
const pid = await getDaemonPid();
|
|
965
|
+
if (!pid) {
|
|
966
|
+
console.log('Daemon: not running (no PID file)');
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (isProcessRunning(pid)) {
|
|
970
|
+
console.log(`Daemon: ${chalk_1.default.green('running')} (PID: ${pid})`);
|
|
971
|
+
console.log(`Logs: ${PATHS.daemonLogFile}`);
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
console.log('Daemon: not running (stale PID file)');
|
|
975
|
+
await fs_extra_1.default.remove(PATHS.daemonPidFile);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* View daemon logs command
|
|
980
|
+
*/
|
|
981
|
+
async function monitorDaemonLogsCommand(options) {
|
|
982
|
+
const lines = options.lines || 50;
|
|
983
|
+
try {
|
|
984
|
+
await fs_extra_1.default.access(PATHS.daemonLogFile);
|
|
985
|
+
}
|
|
986
|
+
catch {
|
|
987
|
+
console.log(chalk_1.default.blue('[Info] No daemon log file found'));
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (options.follow) {
|
|
991
|
+
// Use tail -f
|
|
992
|
+
const tail = (0, child_process_1.spawn)('tail', ['-f', '-n', lines.toString(), PATHS.daemonLogFile], {
|
|
993
|
+
stdio: 'inherit',
|
|
994
|
+
});
|
|
995
|
+
process.on('SIGINT', () => {
|
|
996
|
+
tail.kill();
|
|
997
|
+
process.exit(0);
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
// Read last N lines
|
|
1002
|
+
const content = await fs_extra_1.default.readFile(PATHS.daemonLogFile, 'utf-8');
|
|
1003
|
+
const allLines = content.split('\n');
|
|
1004
|
+
const lastLines = allLines.slice(-lines).join('\n');
|
|
1005
|
+
console.log(lastLines);
|
|
1006
|
+
}
|
|
1007
|
+
}
|