@cef-ai/cli 0.2.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 +1273 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +66 -0
- package/dist/commands/deploy/clone.d.ts +13 -0
- package/dist/commands/deploy/clone.js +143 -0
- package/dist/commands/deploy/delete.d.ts +14 -0
- package/dist/commands/deploy/delete.js +134 -0
- package/dist/commands/deploy/deploy.d.ts +24 -0
- package/dist/commands/deploy/deploy.js +654 -0
- package/dist/commands/deploy/index.d.ts +2 -0
- package/dist/commands/deploy/index.js +216 -0
- package/dist/commands/deploy/status.d.ts +14 -0
- package/dist/commands/deploy/status.js +61 -0
- package/dist/commands/dev/index.d.ts +2 -0
- package/dist/commands/dev/index.js +134 -0
- package/dist/commands/dev/server.d.ts +14 -0
- package/dist/commands/dev/server.js +167 -0
- package/dist/commands/dev/ui.d.ts +1 -0
- package/dist/commands/dev/ui.js +984 -0
- package/dist/commands/playground/index.d.ts +2 -0
- package/dist/commands/playground/index.js +75 -0
- package/dist/commands/playground/test.d.ts +15 -0
- package/dist/commands/playground/test.js +166 -0
- package/dist/commands/service/index.d.ts +2 -0
- package/dist/commands/service/index.js +75 -0
- package/dist/lib/api.d.ts +146 -0
- package/dist/lib/api.js +330 -0
- package/dist/lib/config.d.ts +12 -0
- package/dist/lib/config.js +120 -0
- package/dist/lib/runtime/agent-router.d.ts +12 -0
- package/dist/lib/runtime/agent-router.js +78 -0
- package/dist/lib/runtime/context-builder.d.ts +27 -0
- package/dist/lib/runtime/context-builder.js +166 -0
- package/dist/lib/runtime/cubby-manager.d.ts +20 -0
- package/dist/lib/runtime/cubby-manager.js +161 -0
- package/dist/lib/runtime/index.d.ts +51 -0
- package/dist/lib/runtime/index.js +183 -0
- package/dist/lib/runtime/log-collector.d.ts +17 -0
- package/dist/lib/runtime/log-collector.js +61 -0
- package/dist/lib/runtime/stream-manager.d.ts +10 -0
- package/dist/lib/runtime/stream-manager.js +77 -0
- package/dist/lib/runtime/types.d.ts +76 -0
- package/dist/lib/runtime/types.js +2 -0
- package/dist/lib/typecheck.d.ts +31 -0
- package/dist/lib/typecheck.js +175 -0
- package/dist/lib/types.d.ts +246 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/utils.d.ts +4 -0
- package/dist/lib/utils.js +33 -0
- package/example/.env.example +6 -0
- package/example/agents/object-detection/tasks/yolo.ts +28 -0
- package/example/agents/parking-violation-detector/tasks/detect.ts +326 -0
- package/example/cef.config.yaml +91 -0
- package/example/engagements/engagement.ts +902 -0
- package/package.json +69 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { loadConfig, loadEnv } from '../../lib/config.js';
|
|
6
|
+
import { runDeploy, writeIdsBack } from './deploy.js';
|
|
7
|
+
import { runStatus } from './status.js';
|
|
8
|
+
import { runClone } from './clone.js';
|
|
9
|
+
import { runDelete } from './delete.js';
|
|
10
|
+
import { errorMessage } from '../../lib/utils.js';
|
|
11
|
+
const DEPLOY_ENTITY_TYPES = ['engagement', 'agent', 'cubby', 'workspace', 'stream', 'deployment', 'raft'];
|
|
12
|
+
function loadEnvFromDir(dir) {
|
|
13
|
+
const resolved = path.resolve(dir);
|
|
14
|
+
dotenv.config({ path: path.join(resolved, '.env') });
|
|
15
|
+
return loadEnv();
|
|
16
|
+
}
|
|
17
|
+
function printDeploySummary(results) {
|
|
18
|
+
const created = results.filter(r => r.action === 'created').length;
|
|
19
|
+
const updated = results.filter(r => r.action === 'updated').length;
|
|
20
|
+
const dryRun = results.filter(r => r.action === 'dry-run').length;
|
|
21
|
+
if (dryRun > 0) {
|
|
22
|
+
console.log(`\nDry-run complete. ${dryRun} operation(s) planned.`);
|
|
23
|
+
for (const r of results) {
|
|
24
|
+
console.log(` [dry-run] ${r.entity}: ${r.name}`);
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(`\nDeploy complete. ${created} created, ${updated} updated.`);
|
|
29
|
+
for (const r of results) {
|
|
30
|
+
const label = r.action === 'created' ? 'created' : 'updated';
|
|
31
|
+
console.log(` [${label}] ${r.entity}: ${r.name}${r.id ? ` (${r.id})` : ''}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function statusIcon(status) {
|
|
35
|
+
if (status === 'up-to-date')
|
|
36
|
+
return 'ok';
|
|
37
|
+
return status.toUpperCase();
|
|
38
|
+
}
|
|
39
|
+
function printStatusSummary(results) {
|
|
40
|
+
console.log('');
|
|
41
|
+
for (const r of results) {
|
|
42
|
+
console.log(` [${statusIcon(r.status)}] ${r.entity}: ${r.name}${r.id ? ` (${r.id})` : ''}`);
|
|
43
|
+
if (r.tasks) {
|
|
44
|
+
for (const t of r.tasks) {
|
|
45
|
+
console.log(` task: ${t.name} [${statusIcon(t.status)}]`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const outdated = results.filter(r => r.status === 'outdated').length;
|
|
50
|
+
const notDeployed = results.filter(r => r.status === 'not-deployed').length;
|
|
51
|
+
console.log(`\n${outdated} outdated, ${notDeployed} not deployed.`);
|
|
52
|
+
}
|
|
53
|
+
function printCloneSummary(results) {
|
|
54
|
+
const written = results.filter(r => r.action === 'written').length;
|
|
55
|
+
const skipped = results.filter(r => r.action === 'skipped').length;
|
|
56
|
+
console.log(`\nClone complete. ${written} written, ${skipped} skipped.`);
|
|
57
|
+
for (const r of results) {
|
|
58
|
+
console.log(` [${r.action}] ${r.entity}: ${r.name}${r.file ? ` → ${r.file}` : ''}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function printDeleteSummary(results) {
|
|
62
|
+
const deleted = results.filter(r => r.action === 'deleted').length;
|
|
63
|
+
const failed = results.filter(r => r.action === 'failed').length;
|
|
64
|
+
const skipped = results.filter(r => r.action === 'skipped').length;
|
|
65
|
+
console.log(`\nDelete complete. ${deleted} deleted, ${failed} failed, ${skipped} skipped.`);
|
|
66
|
+
for (const r of results) {
|
|
67
|
+
const suffix = r.error ? ` (${r.error})` : '';
|
|
68
|
+
console.log(` [${r.action}] ${r.entity}: ${r.name} (${r.id})${suffix}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function handleDeploy(opts) {
|
|
72
|
+
let resolved;
|
|
73
|
+
try {
|
|
74
|
+
resolved = loadConfig(opts.config);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error(`Config error: ${errorMessage(err)}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
if (opts.dryRun) {
|
|
81
|
+
console.log(`Dry-run mode. Config: ${resolved.configPath}`);
|
|
82
|
+
console.log(`Agent service: ${resolved.config.agentServicePubKey}`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.log(`Deploying to: ${resolved.env.orchestratorUrl}`);
|
|
86
|
+
console.log(`Agent service: ${resolved.config.agentServicePubKey}`);
|
|
87
|
+
if (opts.only)
|
|
88
|
+
console.log(`Scope: --only ${opts.only}`);
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const results = await runDeploy(resolved, { dryRun: opts.dryRun, only: opts.only });
|
|
92
|
+
printDeploySummary(results);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
if (!opts.dryRun) {
|
|
96
|
+
try {
|
|
97
|
+
writeIdsBack(resolved.configPath, resolved.config);
|
|
98
|
+
}
|
|
99
|
+
catch (writeErr) {
|
|
100
|
+
console.error(`Warning: failed to save partial IDs: ${errorMessage(writeErr)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
console.error(`Deploy failed: ${errorMessage(err)}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function handleStatus(opts) {
|
|
108
|
+
let resolved;
|
|
109
|
+
try {
|
|
110
|
+
resolved = loadConfig(opts.config);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
console.error(`Config error: ${errorMessage(err)}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
console.log(`Checking status: ${resolved.env.orchestratorUrl}`);
|
|
117
|
+
console.log(`Agent service: ${resolved.config.agentServicePubKey}`);
|
|
118
|
+
try {
|
|
119
|
+
const results = await runStatus(resolved);
|
|
120
|
+
printStatusSummary(results);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.error(`Status failed: ${errorMessage(err)}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function handleClone(opts) {
|
|
128
|
+
const dir = opts.outputDir ?? path.dirname(path.resolve(opts.config));
|
|
129
|
+
const resolvedOutputDir = path.resolve(dir);
|
|
130
|
+
try {
|
|
131
|
+
const env = loadEnvFromDir(resolvedOutputDir);
|
|
132
|
+
let agentServicePubKey = opts.pubkey;
|
|
133
|
+
if (!agentServicePubKey) {
|
|
134
|
+
const absConfigPath = path.resolve(opts.config);
|
|
135
|
+
if (fs.existsSync(absConfigPath)) {
|
|
136
|
+
const raw = fs.readFileSync(absConfigPath, 'utf8');
|
|
137
|
+
const parsed = parse(raw);
|
|
138
|
+
agentServicePubKey = parsed?.agentServicePubKey;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!agentServicePubKey) {
|
|
142
|
+
console.error('No agentServicePubKey found. Provide --pubkey or ensure cef.config.yaml exists.');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
console.log(`Cloning agent service: ${agentServicePubKey}`);
|
|
146
|
+
console.log(`Output: ${resolvedOutputDir}`);
|
|
147
|
+
const results = await runClone(env, {
|
|
148
|
+
pubkey: agentServicePubKey,
|
|
149
|
+
outputDir: resolvedOutputDir,
|
|
150
|
+
force: opts.force ?? false,
|
|
151
|
+
});
|
|
152
|
+
printCloneSummary(results);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
console.error(`Clone failed: ${errorMessage(err)}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function handleDelete(opts) {
|
|
160
|
+
let resolved;
|
|
161
|
+
try {
|
|
162
|
+
resolved = loadConfig(opts.config);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
console.error(`Config error: ${errorMessage(err)}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
console.log(`Deleting from: ${resolved.env.orchestratorUrl}`);
|
|
169
|
+
console.log(`Agent service: ${resolved.config.agentServicePubKey}`);
|
|
170
|
+
if (opts.only)
|
|
171
|
+
console.log(`Scope: --only ${opts.only}`);
|
|
172
|
+
try {
|
|
173
|
+
const results = await runDelete(resolved, { only: opts.only, force: opts.force });
|
|
174
|
+
printDeleteSummary(results);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
console.error(`Delete failed: ${errorMessage(err)}`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export function registerDeployCommands(program) {
|
|
182
|
+
const types = DEPLOY_ENTITY_TYPES.join(', ');
|
|
183
|
+
const deploy = program
|
|
184
|
+
.command('deploy', { isDefault: true })
|
|
185
|
+
.description('Sync cef.config.yaml to the orchestrator')
|
|
186
|
+
.option('--config <path>', 'Config file path', './cef.config.yaml')
|
|
187
|
+
.option('--dry-run', 'Show planned operations without calling API')
|
|
188
|
+
.option('--only <type>', `Sync only one entity type (${types})`)
|
|
189
|
+
.action(async (opts) => {
|
|
190
|
+
if (opts.only && !DEPLOY_ENTITY_TYPES.includes(opts.only)) {
|
|
191
|
+
console.error(`Invalid --only value: "${opts.only}". Must be one of: ${types}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
await handleDeploy(opts);
|
|
195
|
+
});
|
|
196
|
+
deploy
|
|
197
|
+
.command('status')
|
|
198
|
+
.description('Compare local config with deployed state')
|
|
199
|
+
.option('--config <path>', 'Config file path', './cef.config.yaml')
|
|
200
|
+
.action(handleStatus);
|
|
201
|
+
deploy
|
|
202
|
+
.command('clone')
|
|
203
|
+
.description('Download an existing agent service')
|
|
204
|
+
.option('--config <path>', 'Config file path', './cef.config.yaml')
|
|
205
|
+
.option('--pubkey <key>', 'Agent service public key (0x...)')
|
|
206
|
+
.option('--output-dir <dir>', 'Directory to write files into')
|
|
207
|
+
.option('--force', 'Overwrite existing files')
|
|
208
|
+
.action(handleClone);
|
|
209
|
+
deploy
|
|
210
|
+
.command('delete')
|
|
211
|
+
.description('Delete deployed entities in reverse dependency order')
|
|
212
|
+
.option('--config <path>', 'Config file path', './cef.config.yaml')
|
|
213
|
+
.option('--only <type>', `Delete only one entity type (${types})`)
|
|
214
|
+
.option('--force', 'Skip confirmation prompt')
|
|
215
|
+
.action(handleDelete);
|
|
216
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ResolvedConfig } from '../../lib/types.js';
|
|
2
|
+
export type StatusValue = 'up-to-date' | 'outdated' | 'not-deployed' | 'not-found';
|
|
3
|
+
export interface TaskStatus {
|
|
4
|
+
name: string;
|
|
5
|
+
status: StatusValue;
|
|
6
|
+
}
|
|
7
|
+
export interface StatusResult {
|
|
8
|
+
entity: 'engagement' | 'agent';
|
|
9
|
+
name: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
status: StatusValue;
|
|
12
|
+
tasks?: TaskStatus[];
|
|
13
|
+
}
|
|
14
|
+
export declare function runStatus(resolved: ResolvedConfig): Promise<StatusResult[]>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createApiClient } from '../../lib/api.js';
|
|
2
|
+
export async function runStatus(resolved) {
|
|
3
|
+
const { config, env } = resolved;
|
|
4
|
+
const api = createApiClient(env, config.agentServicePubKey);
|
|
5
|
+
const results = [];
|
|
6
|
+
const [remoteEngagementsRes, remoteAgentsRes] = await Promise.all([
|
|
7
|
+
api.getEngagements(),
|
|
8
|
+
api.getAgents(),
|
|
9
|
+
]);
|
|
10
|
+
const remoteEngById = new Map(remoteEngagementsRes.data.map(e => [e.engagementId, e]));
|
|
11
|
+
// Includes concierge agents (isConcierge: true) which hold engagement tsCode
|
|
12
|
+
const remoteAgentById = new Map(remoteAgentsRes.data.map(a => [a.agentId, a]));
|
|
13
|
+
for (const eng of config.engagements ?? []) {
|
|
14
|
+
const localCode = eng.tsCode ?? '';
|
|
15
|
+
if (!eng.engagementId) {
|
|
16
|
+
results.push({ entity: 'engagement', name: eng.name, status: 'not-deployed' });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const remoteEng = remoteEngById.get(eng.engagementId);
|
|
20
|
+
if (!remoteEng) {
|
|
21
|
+
results.push({ entity: 'engagement', name: eng.name, id: eng.engagementId, status: 'not-found' });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// Resolve tsCode via concierge agent's first task
|
|
25
|
+
const conciergeAgent = remoteAgentById.get(remoteEng.conciergeAgentId);
|
|
26
|
+
const remoteCode = conciergeAgent?.tasks[0]?.tsCode ?? '';
|
|
27
|
+
results.push({
|
|
28
|
+
entity: 'engagement',
|
|
29
|
+
name: eng.name,
|
|
30
|
+
id: eng.engagementId,
|
|
31
|
+
status: localCode === remoteCode ? 'up-to-date' : 'outdated',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
for (const agent of config.agents ?? []) {
|
|
35
|
+
if (!agent.agentId) {
|
|
36
|
+
results.push({ entity: 'agent', name: agent.name, status: 'not-deployed' });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const remote = remoteAgentById.get(agent.agentId);
|
|
40
|
+
if (!remote) {
|
|
41
|
+
results.push({ entity: 'agent', name: agent.name, id: agent.agentId, status: 'not-found' });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const remoteTaskByName = new Map(remote.tasks.map(t => [t.name, t]));
|
|
45
|
+
const taskStatuses = agent.tasks.map(task => {
|
|
46
|
+
const localCode = task.tsCode ?? '';
|
|
47
|
+
const remoteTask = remoteTaskByName.get(task.name);
|
|
48
|
+
if (!remoteTask)
|
|
49
|
+
return { name: task.name, status: 'not-found' };
|
|
50
|
+
return {
|
|
51
|
+
name: task.name,
|
|
52
|
+
status: localCode === remoteTask.tsCode ? 'up-to-date' : 'outdated',
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
const agentStatus = taskStatuses.some(t => t.status === 'outdated' || t.status === 'not-found')
|
|
56
|
+
? 'outdated'
|
|
57
|
+
: 'up-to-date';
|
|
58
|
+
results.push({ entity: 'agent', name: agent.name, id: agent.agentId, status: agentStatus, tasks: taskStatuses });
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadConfigForDev } from '../../lib/config.js';
|
|
5
|
+
import { LocalRuntime } from '../../lib/runtime/index.js';
|
|
6
|
+
import { createDevServer } from './server.js';
|
|
7
|
+
export function registerDevCommands(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('dev')
|
|
10
|
+
.description('Start local development server for agent testing')
|
|
11
|
+
.option('--config <path>', 'Config file path', './cef.config.yaml')
|
|
12
|
+
.option('--port <port>', 'Server port', '8787')
|
|
13
|
+
.option('--host <host>', 'Server host', 'localhost')
|
|
14
|
+
.option('--persist', 'Keep .cef-dev/ data after server stops (default: cleared on shutdown)', false)
|
|
15
|
+
.action(handleDev);
|
|
16
|
+
}
|
|
17
|
+
async function handleDev(opts) {
|
|
18
|
+
const port = parseInt(opts.port, 10);
|
|
19
|
+
const host = opts.host;
|
|
20
|
+
// Load config (no auth required)
|
|
21
|
+
let resolved;
|
|
22
|
+
try {
|
|
23
|
+
resolved = loadConfigForDev(opts.config);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
console.error(chalk.red(`Failed to load config: ${err.message}`));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const { config, configDir } = resolved;
|
|
30
|
+
// Data directory for local state (cleared on shutdown unless --persist)
|
|
31
|
+
const cefDevDir = path.join(configDir, '.cef-dev');
|
|
32
|
+
const dataDir = path.join(cefDevDir, 'cubbies');
|
|
33
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
34
|
+
// Initialize runtime
|
|
35
|
+
const runtime = new LocalRuntime(dataDir);
|
|
36
|
+
await runtime.initialize(config);
|
|
37
|
+
// Start HTTP server
|
|
38
|
+
const server = createDevServer(runtime, { port, host });
|
|
39
|
+
try {
|
|
40
|
+
await server.start();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error(chalk.red(err.message));
|
|
44
|
+
runtime.shutdown();
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
// Summary
|
|
48
|
+
const topology = runtime.getTopology();
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(chalk.bold('CEF Dev Server'));
|
|
51
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
52
|
+
console.log(` Engagements: ${chalk.cyan(String(topology.engagements.length))}`);
|
|
53
|
+
console.log(` Agents: ${chalk.cyan(String(topology.agents.length))}`);
|
|
54
|
+
console.log(` Cubbies: ${chalk.cyan(String(topology.cubbies.length))}`);
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(` ${chalk.green(`http://${host}:${port}`)}`);
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(chalk.gray('Endpoints:'));
|
|
59
|
+
console.log(chalk.gray(' POST /api/trigger Trigger engagement'));
|
|
60
|
+
console.log(chalk.gray(' POST /api/agents/:alias/:task Call agent task'));
|
|
61
|
+
console.log(chalk.gray(' POST /api/streams/:id/push Push stream packet'));
|
|
62
|
+
console.log(chalk.gray(' POST /api/cubbies/:alias/query SQL query'));
|
|
63
|
+
console.log(chalk.gray(' GET /api/topology View topology'));
|
|
64
|
+
console.log(chalk.gray(' WS /ws Real-time logs'));
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(chalk.gray('Watching for file changes. Press Ctrl+C to stop.'));
|
|
67
|
+
console.log('');
|
|
68
|
+
// Stream runtime logs to console
|
|
69
|
+
runtime.logEmitter.on('log', (entry) => {
|
|
70
|
+
const prefix = entry.agentAlias
|
|
71
|
+
? chalk.blue(`[${entry.agentAlias}${entry.taskAlias ? '.' + entry.taskAlias : ''}]`)
|
|
72
|
+
: chalk.gray('[engagement]');
|
|
73
|
+
const level = entry.level === 'error' ? chalk.red(entry.level)
|
|
74
|
+
: entry.level === 'warn' ? chalk.yellow(entry.level)
|
|
75
|
+
: chalk.gray(entry.level);
|
|
76
|
+
const args = entry.args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
|
|
77
|
+
console.log(`${chalk.gray(new Date(entry.timestamp).toISOString().slice(11, 23))} ${level} ${prefix} ${args}`);
|
|
78
|
+
});
|
|
79
|
+
// File watcher
|
|
80
|
+
const watchedFiles = new Map();
|
|
81
|
+
for (const agent of config.agents ?? []) {
|
|
82
|
+
for (const task of agent.tasks) {
|
|
83
|
+
if (task._resolvedFilePath) {
|
|
84
|
+
watchedFiles.set(task._resolvedFilePath, `${agent.alias}/${task.alias}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const eng of config.engagements ?? []) {
|
|
89
|
+
if (eng._resolvedFilePath) {
|
|
90
|
+
watchedFiles.set(eng._resolvedFilePath, `engagement:${eng.name}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const [filePath, label] of watchedFiles) {
|
|
94
|
+
try {
|
|
95
|
+
fs.watch(filePath, () => {
|
|
96
|
+
try {
|
|
97
|
+
const tsCode = fs.readFileSync(filePath, 'utf8');
|
|
98
|
+
const parts = label.split('/');
|
|
99
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
100
|
+
runtime.reloadFile(parts[0], parts[1], tsCode);
|
|
101
|
+
}
|
|
102
|
+
console.log(chalk.yellow(`[reload] ${path.relative(configDir, filePath)}`));
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.error(chalk.red(`Failed to reload ${filePath}: ${err.message}`));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// File watch not supported on all platforms for all paths
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Graceful shutdown with hard timeout
|
|
114
|
+
let shuttingDown = false;
|
|
115
|
+
const shutdown = async () => {
|
|
116
|
+
if (shuttingDown) {
|
|
117
|
+
process.exit(0);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
shuttingDown = true;
|
|
121
|
+
console.log(chalk.gray('\nShutting down...'));
|
|
122
|
+
runtime.shutdown();
|
|
123
|
+
if (!opts.persist && fs.existsSync(cefDevDir)) {
|
|
124
|
+
fs.rmSync(cefDevDir, { recursive: true, force: true });
|
|
125
|
+
console.log(chalk.gray('Cleared .cef-dev/'));
|
|
126
|
+
}
|
|
127
|
+
const hardExit = setTimeout(() => process.exit(0), 1000);
|
|
128
|
+
hardExit.unref();
|
|
129
|
+
await server.stop();
|
|
130
|
+
process.exit(0);
|
|
131
|
+
};
|
|
132
|
+
process.on('SIGINT', shutdown);
|
|
133
|
+
process.on('SIGTERM', shutdown);
|
|
134
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { type WebSocket } from 'ws';
|
|
3
|
+
import type { LocalRuntime } from '../../lib/runtime/index.js';
|
|
4
|
+
interface ServerOptions {
|
|
5
|
+
port: number;
|
|
6
|
+
host: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createDevServer(runtime: LocalRuntime, options: ServerOptions): {
|
|
9
|
+
start: () => Promise<void>;
|
|
10
|
+
stop: () => Promise<void>;
|
|
11
|
+
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
12
|
+
wss: import("ws").Server<typeof WebSocket, typeof http.IncomingMessage>;
|
|
13
|
+
};
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { WebSocketServer } from 'ws';
|
|
3
|
+
import { getDevUiHtml } from './ui.js';
|
|
4
|
+
export function createDevServer(runtime, options) {
|
|
5
|
+
const wsClients = new Set();
|
|
6
|
+
// Stream logs and trace events to WebSocket clients
|
|
7
|
+
const broadcast = (msg) => {
|
|
8
|
+
for (const ws of wsClients) {
|
|
9
|
+
if (ws.readyState === ws.OPEN)
|
|
10
|
+
ws.send(msg);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
runtime.logEmitter.on('log', (entry) => {
|
|
14
|
+
broadcast(JSON.stringify({ type: 'log', data: entry }));
|
|
15
|
+
});
|
|
16
|
+
runtime.logEmitter.on('trace', (event) => {
|
|
17
|
+
broadcast(JSON.stringify({ type: 'trace', data: event }));
|
|
18
|
+
});
|
|
19
|
+
const server = http.createServer(async (req, res) => {
|
|
20
|
+
// CORS headers for dev
|
|
21
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
22
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
23
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
24
|
+
if (req.method === 'OPTIONS') {
|
|
25
|
+
res.writeHead(204);
|
|
26
|
+
res.end();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
30
|
+
const pathname = url.pathname;
|
|
31
|
+
try {
|
|
32
|
+
// ─── API Routes ───────────────────────────────────────────────
|
|
33
|
+
if (req.method === 'GET' && pathname === '/api/topology') {
|
|
34
|
+
json(res, runtime.getTopology());
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (req.method === 'GET' && pathname === '/api/executions') {
|
|
38
|
+
json(res, runtime.getLogs());
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (req.method === 'POST' && pathname === '/api/trigger') {
|
|
42
|
+
const body = await readBody(req);
|
|
43
|
+
const { engagement, payload } = body;
|
|
44
|
+
if (!engagement) {
|
|
45
|
+
json(res, { error: 'Missing "engagement" field' }, 400);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const handle = await runtime.startEngagement(engagement, payload ?? {});
|
|
49
|
+
json(res, { executionId: handle.executionId, started: true });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
{
|
|
53
|
+
let m;
|
|
54
|
+
if (req.method === 'POST' && (m = pathname.match(/^\/api\/streams\/([^/]+)\/push$/))) {
|
|
55
|
+
const streamId = m[1];
|
|
56
|
+
const body = await readBody(req);
|
|
57
|
+
const packet = body.data ?? body;
|
|
58
|
+
// Ensure payload is Uint8Array
|
|
59
|
+
if (typeof packet.payload === 'string') {
|
|
60
|
+
packet.payload = new TextEncoder().encode(packet.payload);
|
|
61
|
+
}
|
|
62
|
+
else if (Array.isArray(packet.payload)) {
|
|
63
|
+
packet.payload = new Uint8Array(packet.payload);
|
|
64
|
+
}
|
|
65
|
+
else if (packet.payload && !ArrayBuffer.isView(packet.payload)) {
|
|
66
|
+
packet.payload = new TextEncoder().encode(JSON.stringify(packet.payload));
|
|
67
|
+
}
|
|
68
|
+
if (!packet.sequenceNum)
|
|
69
|
+
packet.sequenceNum = 0;
|
|
70
|
+
runtime.pushToStream(streamId, packet);
|
|
71
|
+
json(res, { pushed: true });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (req.method === 'POST' && (m = pathname.match(/^\/api\/agents\/([^/]+)\/([^/]+)$/))) {
|
|
75
|
+
const body = await readBody(req);
|
|
76
|
+
const result = await runtime.callAgentTask(m[1], m[2], body.payload ?? body);
|
|
77
|
+
json(res, result);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (req.method === 'POST' && (m = pathname.match(/^\/api\/cubbies\/([^/]+)\/query$/))) {
|
|
81
|
+
const body = await readBody(req);
|
|
82
|
+
const result = await runtime.queryCubby(m[1], body.instanceId ?? 'default', body.sql, body.params ?? []);
|
|
83
|
+
json(res, result);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (req.method === 'POST' && (m = pathname.match(/^\/api\/cubbies\/([^/]+)\/exec$/))) {
|
|
87
|
+
const body = await readBody(req);
|
|
88
|
+
const result = await runtime.execCubby(m[1], body.instanceId ?? 'default', body.sql, body.params ?? []);
|
|
89
|
+
json(res, result);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (req.method === 'GET' && (m = pathname.match(/^\/api\/cubbies\/([^/]+)\/instances$/))) {
|
|
93
|
+
json(res, { instances: runtime.listCubbyInstances(m[1]) });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// ─── Dev UI ──────────────────────────────────────────────────
|
|
98
|
+
if (req.method === 'GET' && (pathname === '/' || pathname === '/index.html')) {
|
|
99
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
100
|
+
res.end(getDevUiHtml());
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// ─── 404 ────────────────────────────────────────────────────
|
|
104
|
+
json(res, { error: 'Not found' }, 404);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
json(res, { error: err.message }, 500);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
// Track open sockets so we can destroy them on shutdown
|
|
111
|
+
const sockets = new Set();
|
|
112
|
+
server.on('connection', (socket) => {
|
|
113
|
+
sockets.add(socket);
|
|
114
|
+
socket.on('close', () => sockets.delete(socket));
|
|
115
|
+
});
|
|
116
|
+
// WebSocket server
|
|
117
|
+
const wss = new WebSocketServer({ server });
|
|
118
|
+
wss.on('connection', (ws) => {
|
|
119
|
+
wsClients.add(ws);
|
|
120
|
+
ws.on('close', () => wsClients.delete(ws));
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
start: () => new Promise((resolve, reject) => {
|
|
124
|
+
server.on('error', (err) => {
|
|
125
|
+
if (err.code === 'EADDRINUSE') {
|
|
126
|
+
reject(new Error(`Port ${options.port} is already in use. Stop the other process or use --port <number>.`));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
reject(err);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
server.listen(options.port, options.host, () => resolve());
|
|
133
|
+
}),
|
|
134
|
+
stop: () => new Promise((resolve) => {
|
|
135
|
+
for (const ws of wsClients)
|
|
136
|
+
ws.terminate();
|
|
137
|
+
wsClients.clear();
|
|
138
|
+
for (const socket of sockets)
|
|
139
|
+
socket.destroy();
|
|
140
|
+
sockets.clear();
|
|
141
|
+
wss.close();
|
|
142
|
+
server.close(() => resolve());
|
|
143
|
+
}),
|
|
144
|
+
server,
|
|
145
|
+
wss,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function json(res, data, status = 200) {
|
|
149
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
150
|
+
res.end(JSON.stringify(data));
|
|
151
|
+
}
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
+
function readBody(req) {
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
let body = '';
|
|
156
|
+
req.on('data', chunk => { body += chunk; });
|
|
157
|
+
req.on('end', () => {
|
|
158
|
+
try {
|
|
159
|
+
resolve(body ? JSON.parse(body) : {});
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
reject(new Error('Invalid JSON body'));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
req.on('error', reject);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getDevUiHtml(): string;
|