electric-ax 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +177 -0
- package/bin/electric-dev.mjs +42 -0
- package/dist/chunk-BCwAaXi7.cjs +31 -0
- package/dist/completions-BHILvHgZ.js +137 -0
- package/dist/completions-CO5UBiNh.cjs +162 -0
- package/dist/completions.cjs +6 -0
- package/dist/completions.d.cts +10 -0
- package/dist/completions.d.ts +10 -0
- package/dist/completions.js +3 -0
- package/dist/entity-stream-db-BzuIvhSy.d.ts +13 -0
- package/dist/entity-stream-db-C2C3t_hD.d.cts +13 -0
- package/dist/entity-stream-db-CGP2xVeJ.js +37 -0
- package/dist/entity-stream-db-Djo-7a11.cjs +44 -0
- package/dist/entity-stream-db.cjs +3 -0
- package/dist/entity-stream-db.d.cts +2 -0
- package/dist/entity-stream-db.d.ts +2 -0
- package/dist/entity-stream-db.js +3 -0
- package/dist/index-B6gIlecl.d.cts +83 -0
- package/dist/index-BJvlcIKL.d.ts +83 -0
- package/dist/index.cjs +629 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +622 -0
- package/dist/observe-ui.cjs +380 -0
- package/dist/observe-ui.d.cts +59 -0
- package/dist/observe-ui.d.ts +59 -0
- package/dist/observe-ui.js +371 -0
- package/docker-compose.full.yml +68 -0
- package/package.json +94 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const require_chunk = require('./chunk-BCwAaXi7.cjs');
|
|
4
|
+
const require_completions = require('./completions-CO5UBiNh.cjs');
|
|
5
|
+
const node_fs = require_chunk.__toESM(require("node:fs"));
|
|
6
|
+
const node_os = require_chunk.__toESM(require("node:os"));
|
|
7
|
+
const node_path = require_chunk.__toESM(require("node:path"));
|
|
8
|
+
const node_url = require_chunk.__toESM(require("node:url"));
|
|
9
|
+
const commander = require_chunk.__toESM(require("commander"));
|
|
10
|
+
const node_child_process = require_chunk.__toESM(require("node:child_process"));
|
|
11
|
+
const __electric_ax_agents = require_chunk.__toESM(require("@electric-ax/agents"));
|
|
12
|
+
|
|
13
|
+
//#region src/start.ts
|
|
14
|
+
const DEFAULT_ELECTRIC_AGENTS_PORT = 4437;
|
|
15
|
+
const DEFAULT_BUILTIN_AGENTS_PORT = 4448;
|
|
16
|
+
const DOCKER_COMPOSE_FILE = (0, node_url.fileURLToPath)(new URL(`../docker-compose.full.yml`, require("url").pathToFileURL(__filename).href));
|
|
17
|
+
function parseDotEnvValue(raw) {
|
|
18
|
+
const trimmed = raw.trim();
|
|
19
|
+
if (trimmed.startsWith(`"`) && trimmed.endsWith(`"`) || trimmed.startsWith(`'`) && trimmed.endsWith(`'`)) return trimmed.slice(1, -1);
|
|
20
|
+
const hashIndex = trimmed.indexOf(`#`);
|
|
21
|
+
return hashIndex === -1 ? trimmed : trimmed.slice(0, hashIndex).trim();
|
|
22
|
+
}
|
|
23
|
+
function readDotEnvFile(cwd = process.cwd()) {
|
|
24
|
+
const envPath = (0, node_path.resolve)(cwd, `.env`);
|
|
25
|
+
try {
|
|
26
|
+
const content = (0, node_fs.readFileSync)(envPath, `utf8`);
|
|
27
|
+
const values = {};
|
|
28
|
+
for (const line of content.split(/\r?\n/)) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed || trimmed.startsWith(`#`)) continue;
|
|
31
|
+
const equalsIndex = trimmed.indexOf(`=`);
|
|
32
|
+
if (equalsIndex <= 0) continue;
|
|
33
|
+
const key = trimmed.slice(0, equalsIndex).trim();
|
|
34
|
+
const value = parseDotEnvValue(trimmed.slice(equalsIndex + 1));
|
|
35
|
+
values[key] = value;
|
|
36
|
+
}
|
|
37
|
+
return values;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code === `ENOENT`) return {};
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function resolveAnthropicApiKey(options, env = process.env, fileEnv = readDotEnvFile()) {
|
|
44
|
+
const candidate = options.anthropicApiKey?.trim() || env.ANTHROPIC_API_KEY?.trim() || fileEnv.ANTHROPIC_API_KEY?.trim();
|
|
45
|
+
if (!candidate) throw new Error(`ANTHROPIC_API_KEY is required. Pass --anthropic-api-key, export it in your shell, or set it in .env.`);
|
|
46
|
+
return candidate;
|
|
47
|
+
}
|
|
48
|
+
function resolveBuiltinAgentsPort(env = process.env, fileEnv = readDotEnvFile()) {
|
|
49
|
+
const raw = env.ELECTRIC_AGENTS_BUILTIN_PORT?.trim() || fileEnv.ELECTRIC_AGENTS_BUILTIN_PORT?.trim();
|
|
50
|
+
const parsed = raw ? Number(raw) : DEFAULT_BUILTIN_AGENTS_PORT;
|
|
51
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`ELECTRIC_AGENTS_BUILTIN_PORT must be a positive integer`);
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
function resolveElectricAgentsPort(env = process.env, fileEnv = readDotEnvFile()) {
|
|
55
|
+
const raw = env.ELECTRIC_AGENTS_PORT?.trim() || fileEnv.ELECTRIC_AGENTS_PORT?.trim();
|
|
56
|
+
const parsed = raw ? Number(raw) : DEFAULT_ELECTRIC_AGENTS_PORT;
|
|
57
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`ELECTRIC_AGENTS_PORT must be a positive integer`);
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
function getStartedEnvironmentMessage(started) {
|
|
61
|
+
return [
|
|
62
|
+
`Electric Agents dev environment is up.`,
|
|
63
|
+
`Server + UI: ${started.uiUrl}`,
|
|
64
|
+
`Docker project: ${started.composeProjectName}`
|
|
65
|
+
].join(`\n`);
|
|
66
|
+
}
|
|
67
|
+
function getStoppedEnvironmentMessage(stopped) {
|
|
68
|
+
return [
|
|
69
|
+
`Electric Agents dev environment is down.`,
|
|
70
|
+
`Docker project: ${stopped.composeProjectName}`,
|
|
71
|
+
stopped.removedVolumes ? `Volumes removed: yes` : `Volumes removed: no`
|
|
72
|
+
].join(`\n`);
|
|
73
|
+
}
|
|
74
|
+
function getStartedBuiltinAgentsMessage(started) {
|
|
75
|
+
return [
|
|
76
|
+
`Builtin Horton server is up.`,
|
|
77
|
+
`Webhook server: ${started.url}`,
|
|
78
|
+
`Registers with: ${started.agentServerUrl}`,
|
|
79
|
+
`Press Ctrl-C to stop.`
|
|
80
|
+
].join(`\n`);
|
|
81
|
+
}
|
|
82
|
+
function slugify(input) {
|
|
83
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, `-`).replace(/^-+|-+$/g, ``);
|
|
84
|
+
}
|
|
85
|
+
function resolveComposeProjectName(cwd = process.cwd(), env = process.env) {
|
|
86
|
+
const explicit = env.ELECTRIC_AGENTS_COMPOSE_PROJECT?.trim();
|
|
87
|
+
if (explicit) return explicit;
|
|
88
|
+
const base = slugify((0, node_path.basename)(cwd)) || `workspace`;
|
|
89
|
+
return `electric-agents-${base}`;
|
|
90
|
+
}
|
|
91
|
+
async function runDockerCompose(args, env) {
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
const child = (0, node_child_process.spawn)(`docker`, args, {
|
|
94
|
+
cwd: process.cwd(),
|
|
95
|
+
env,
|
|
96
|
+
stdio: `inherit`
|
|
97
|
+
});
|
|
98
|
+
child.on(`error`, (error) => {
|
|
99
|
+
reject(new Error(`Failed to run docker compose: ${error instanceof Error ? error.message : String(error)}`));
|
|
100
|
+
});
|
|
101
|
+
child.on(`exit`, (code) => {
|
|
102
|
+
if (code === 0) {
|
|
103
|
+
resolve();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
reject(new Error(`docker compose exited with code ${code ?? `unknown`}`));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function delay(ms) {
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
setTimeout(resolve, ms);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async function waitForElectricAgentsServer(baseUrl, options = {}) {
|
|
116
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
117
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
118
|
+
const intervalMs = options.intervalMs ?? 1e3;
|
|
119
|
+
const deadline = Date.now() + timeoutMs;
|
|
120
|
+
const healthUrl = `${baseUrl.replace(/\/$/, ``)}/_electric/health`;
|
|
121
|
+
let lastError = null;
|
|
122
|
+
while (Date.now() < deadline) {
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetchImpl(healthUrl, { signal: AbortSignal.timeout(5e3) });
|
|
125
|
+
if (response.ok) return;
|
|
126
|
+
lastError = `healthcheck returned ${response.status}`;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
129
|
+
}
|
|
130
|
+
await delay(intervalMs);
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`Timed out waiting for Electric Agents server at ${healthUrl}${lastError ? `: ${lastError}` : ``}`);
|
|
133
|
+
}
|
|
134
|
+
async function startElectricAgentsDevEnvironment(_options = {}, env = process.env, cwd = process.cwd()) {
|
|
135
|
+
const fileEnv = readDotEnvFile(cwd);
|
|
136
|
+
const port = resolveElectricAgentsPort(env, fileEnv);
|
|
137
|
+
const composeProjectName = resolveComposeProjectName(cwd, env);
|
|
138
|
+
await runDockerCompose([
|
|
139
|
+
`compose`,
|
|
140
|
+
`-f`,
|
|
141
|
+
DOCKER_COMPOSE_FILE,
|
|
142
|
+
`up`,
|
|
143
|
+
`-d`
|
|
144
|
+
], {
|
|
145
|
+
...env,
|
|
146
|
+
COMPOSE_PROJECT_NAME: composeProjectName,
|
|
147
|
+
ELECTRIC_AGENTS_PORT: String(port)
|
|
148
|
+
});
|
|
149
|
+
const uiUrl = `http://localhost:${port}`;
|
|
150
|
+
await waitForElectricAgentsServer(uiUrl);
|
|
151
|
+
return {
|
|
152
|
+
port,
|
|
153
|
+
uiUrl,
|
|
154
|
+
composeProjectName
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function stopElectricAgentsDevEnvironment(options, env = process.env, cwd = process.cwd()) {
|
|
158
|
+
const composeProjectName = resolveComposeProjectName(cwd, env);
|
|
159
|
+
const args = [
|
|
160
|
+
`compose`,
|
|
161
|
+
`-f`,
|
|
162
|
+
DOCKER_COMPOSE_FILE,
|
|
163
|
+
`down`
|
|
164
|
+
];
|
|
165
|
+
if (options.removeVolumes) args.push(`--volumes`);
|
|
166
|
+
await runDockerCompose(args, {
|
|
167
|
+
...env,
|
|
168
|
+
COMPOSE_PROJECT_NAME: composeProjectName
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
composeProjectName,
|
|
172
|
+
removedVolumes: options.removeVolumes ?? false
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function waitForShutdown(stop, signalSource = process) {
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
let stopping = false;
|
|
178
|
+
const cleanup = () => {
|
|
179
|
+
signalSource.off(`SIGINT`, onSigint);
|
|
180
|
+
signalSource.off(`SIGTERM`, onSigterm);
|
|
181
|
+
};
|
|
182
|
+
const shutdown = (signal) => {
|
|
183
|
+
if (stopping) return;
|
|
184
|
+
stopping = true;
|
|
185
|
+
cleanup();
|
|
186
|
+
stop().then(resolve).catch((error) => {
|
|
187
|
+
reject(new Error(`Failed to stop builtin agents server after ${signal}: ${error instanceof Error ? error.message : String(error)}`));
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
const onSigint = () => {
|
|
191
|
+
shutdown(`SIGINT`);
|
|
192
|
+
};
|
|
193
|
+
const onSigterm = () => {
|
|
194
|
+
shutdown(`SIGTERM`);
|
|
195
|
+
};
|
|
196
|
+
signalSource.on(`SIGINT`, onSigint);
|
|
197
|
+
signalSource.on(`SIGTERM`, onSigterm);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async function startBuiltinAgentsServer(options, params = {}) {
|
|
201
|
+
const env = params.env ?? process.env;
|
|
202
|
+
const cwd = params.cwd ?? process.cwd();
|
|
203
|
+
const fileEnv = readDotEnvFile(cwd);
|
|
204
|
+
const anthropicApiKey = resolveAnthropicApiKey(options, env, fileEnv);
|
|
205
|
+
const port = resolveBuiltinAgentsPort(env, fileEnv);
|
|
206
|
+
const agentServerUrl = params.agentServerUrl ?? env.ELECTRIC_AGENTS_URL?.trim() ?? `http://localhost:${resolveElectricAgentsPort(env, fileEnv)}`;
|
|
207
|
+
process.env.ANTHROPIC_API_KEY = anthropicApiKey;
|
|
208
|
+
await waitForElectricAgentsServer(agentServerUrl);
|
|
209
|
+
const server = new __electric_ax_agents.BuiltinAgentsServer({
|
|
210
|
+
agentServerUrl,
|
|
211
|
+
port,
|
|
212
|
+
workingDirectory: cwd
|
|
213
|
+
});
|
|
214
|
+
await server.start();
|
|
215
|
+
const started = {
|
|
216
|
+
port,
|
|
217
|
+
url: server.url,
|
|
218
|
+
registeredBaseUrl: server.registeredBaseUrl,
|
|
219
|
+
agentServerUrl
|
|
220
|
+
};
|
|
221
|
+
console.log(getStartedBuiltinAgentsMessage(started));
|
|
222
|
+
await waitForShutdown(() => server.stop());
|
|
223
|
+
return started;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/index.ts
|
|
228
|
+
const DEFAULT_ELECTRIC_AGENTS_URL = `http://localhost:4437`;
|
|
229
|
+
var CliError = class extends Error {};
|
|
230
|
+
function getDefaultElectricAgentsIdentity() {
|
|
231
|
+
return `${(0, node_os.userInfo)().username}@${(0, node_os.hostname)()}`;
|
|
232
|
+
}
|
|
233
|
+
function getElectricCliEnv(env = process.env) {
|
|
234
|
+
return {
|
|
235
|
+
electricAgentsUrl: env.ELECTRIC_AGENTS_URL || DEFAULT_ELECTRIC_AGENTS_URL,
|
|
236
|
+
electricAgentsIdentity: env.ELECTRIC_AGENTS_IDENTITY || getDefaultElectricAgentsIdentity()
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function getErrorMessage(error) {
|
|
240
|
+
return error instanceof Error ? error.message : String(error);
|
|
241
|
+
}
|
|
242
|
+
function fail(message) {
|
|
243
|
+
throw new CliError(message);
|
|
244
|
+
}
|
|
245
|
+
function relativeTime(epochMs) {
|
|
246
|
+
const seconds = Math.floor((Date.now() - epochMs) / 1e3);
|
|
247
|
+
if (seconds < 5) return `just now`;
|
|
248
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
249
|
+
const minutes = Math.floor(seconds / 60);
|
|
250
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
251
|
+
const hours = Math.floor(minutes / 60);
|
|
252
|
+
if (hours < 24) return `${hours}h ago`;
|
|
253
|
+
const days = Math.floor(hours / 24);
|
|
254
|
+
return `${days}d ago`;
|
|
255
|
+
}
|
|
256
|
+
function parsePayload(input, json) {
|
|
257
|
+
if (json) try {
|
|
258
|
+
return JSON.parse(input);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
fail(`Invalid JSON: ${getErrorMessage(error)}`);
|
|
261
|
+
}
|
|
262
|
+
return { text: input };
|
|
263
|
+
}
|
|
264
|
+
function normalizeVariadicArg(value) {
|
|
265
|
+
if (value === void 0) return [];
|
|
266
|
+
return Array.isArray(value) ? value : [value];
|
|
267
|
+
}
|
|
268
|
+
function getCommandActionArg(args) {
|
|
269
|
+
return args[args.length - 1];
|
|
270
|
+
}
|
|
271
|
+
function resolveCommandName(argv) {
|
|
272
|
+
const invoked = (0, node_path.basename)(argv[1] ?? ``);
|
|
273
|
+
if (!invoked) return `electric`;
|
|
274
|
+
if (invoked === `index.js` || invoked === `index.ts` || invoked === `node`) return `electric`;
|
|
275
|
+
return invoked.replace(/\.(c|m)?js$/, ``);
|
|
276
|
+
}
|
|
277
|
+
function commandExample(commandName) {
|
|
278
|
+
return `${commandName} agent`;
|
|
279
|
+
}
|
|
280
|
+
function resolveCommandPrefix(argv, env = process.env) {
|
|
281
|
+
if (env.npm_command === `exec`) {
|
|
282
|
+
const userAgent = env.npm_config_user_agent ?? ``;
|
|
283
|
+
if (userAgent.startsWith(`pnpm/`)) return `pnpx electric-ax agent`;
|
|
284
|
+
if (userAgent.startsWith(`npm/`)) return `npx electric-ax agent`;
|
|
285
|
+
}
|
|
286
|
+
return commandExample(resolveCommandName(argv));
|
|
287
|
+
}
|
|
288
|
+
async function electricAgentsFetch(env, path, opts = {}) {
|
|
289
|
+
try {
|
|
290
|
+
return await fetch(`${env.electricAgentsUrl}${path}`, {
|
|
291
|
+
...opts,
|
|
292
|
+
headers: {
|
|
293
|
+
"content-type": `application/json`,
|
|
294
|
+
...opts.headers
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
} catch (error) {
|
|
298
|
+
fail(`Could not connect to ${env.electricAgentsUrl} — is the Electric Agents server running?\n Set ELECTRIC_AGENTS_URL to point to a different server.\n Original error: ${getErrorMessage(error)}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function parseJsonResponse(res) {
|
|
302
|
+
const text = await res.text();
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(text);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
307
|
+
return { error: { message: text || res.statusText } };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function failFromResponse(data, res) {
|
|
311
|
+
const err = data.error;
|
|
312
|
+
fail(String(err?.message ?? res.statusText));
|
|
313
|
+
}
|
|
314
|
+
async function fetchEntityTypes(env) {
|
|
315
|
+
const res = await electricAgentsFetch(env, `/_electric/entity-types`);
|
|
316
|
+
const data = await res.json();
|
|
317
|
+
if (!res.ok) fail(typeof data === `object` && data !== null ? String(data.error?.message ?? res.statusText) : res.statusText);
|
|
318
|
+
if (!Array.isArray(data)) fail(`Unexpected response from server when listing entity types`);
|
|
319
|
+
return data;
|
|
320
|
+
}
|
|
321
|
+
async function listTypes(env) {
|
|
322
|
+
const rows = await fetchEntityTypes(env);
|
|
323
|
+
if (rows.length === 0) {
|
|
324
|
+
console.log(`No entity types found`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const types = rows.map((entityType) => ({
|
|
328
|
+
name: entityType.name,
|
|
329
|
+
description: entityType.description,
|
|
330
|
+
serve_endpoint: entityType.serve_endpoint
|
|
331
|
+
}));
|
|
332
|
+
const { renderTypesTable } = await import(`./types-table.js`);
|
|
333
|
+
renderTypesTable(types);
|
|
334
|
+
}
|
|
335
|
+
async function fetchEntities(env, options = {}) {
|
|
336
|
+
const searchParams = new URLSearchParams();
|
|
337
|
+
if (options.type) searchParams.set(`type`, options.type);
|
|
338
|
+
if (options.status) searchParams.set(`status`, options.status);
|
|
339
|
+
if (options.parent) searchParams.set(`parent`, options.parent);
|
|
340
|
+
const suffix = searchParams.size > 0 ? `?${searchParams.toString()}` : ``;
|
|
341
|
+
const res = await electricAgentsFetch(env, `/_electric/entities${suffix}`);
|
|
342
|
+
const data = await res.json();
|
|
343
|
+
if (!res.ok) fail(typeof data === `object` && data !== null ? String(data.error?.message ?? res.statusText) : res.statusText);
|
|
344
|
+
if (!Array.isArray(data)) fail(`Unexpected response from server when listing entities`);
|
|
345
|
+
return data;
|
|
346
|
+
}
|
|
347
|
+
async function inspectType(env, name) {
|
|
348
|
+
const res = await electricAgentsFetch(env, `/_electric/entity-types/${encodeURIComponent(name)}`);
|
|
349
|
+
const data = await parseJsonResponse(res);
|
|
350
|
+
if (!res.ok) failFromResponse(data, res);
|
|
351
|
+
console.log(JSON.stringify(data, null, 2));
|
|
352
|
+
}
|
|
353
|
+
async function deleteType(env, name) {
|
|
354
|
+
const res = await electricAgentsFetch(env, `/_electric/entity-types/${encodeURIComponent(name)}`, { method: `DELETE` });
|
|
355
|
+
if (!res.ok) {
|
|
356
|
+
const data = await parseJsonResponse(res);
|
|
357
|
+
failFromResponse(data, res);
|
|
358
|
+
}
|
|
359
|
+
console.log(`Deleted entity type ${name}`);
|
|
360
|
+
}
|
|
361
|
+
async function spawnEntity(env, urlPath, options) {
|
|
362
|
+
let spawnArgs = {};
|
|
363
|
+
if (options.args) try {
|
|
364
|
+
spawnArgs = JSON.parse(options.args);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
fail(`--args must be valid JSON: ${getErrorMessage(error)}`);
|
|
367
|
+
}
|
|
368
|
+
const res = await electricAgentsFetch(env, urlPath, {
|
|
369
|
+
method: `PUT`,
|
|
370
|
+
body: JSON.stringify({ args: spawnArgs })
|
|
371
|
+
});
|
|
372
|
+
const data = await parseJsonResponse(res);
|
|
373
|
+
if (!res.ok) failFromResponse(data, res);
|
|
374
|
+
const entity = data;
|
|
375
|
+
if (!entity.url || !entity.status) fail(`Unexpected response from server: ${JSON.stringify(data)}`);
|
|
376
|
+
console.log(`Spawned ${entity.url} (type: ${entity.type ?? `unknown`}, status: ${entity.status})`);
|
|
377
|
+
}
|
|
378
|
+
async function sendMessage(env, url, message, options) {
|
|
379
|
+
const payload = parsePayload(message, options.json ?? false);
|
|
380
|
+
const body = {
|
|
381
|
+
from: env.electricAgentsIdentity,
|
|
382
|
+
payload
|
|
383
|
+
};
|
|
384
|
+
if (options.type) body.type = options.type;
|
|
385
|
+
const res = await electricAgentsFetch(env, `${url}/send`, {
|
|
386
|
+
method: `POST`,
|
|
387
|
+
body: JSON.stringify(body)
|
|
388
|
+
});
|
|
389
|
+
if (!res.ok) {
|
|
390
|
+
const data = await parseJsonResponse(res);
|
|
391
|
+
failFromResponse(data, res);
|
|
392
|
+
}
|
|
393
|
+
console.log(`Message sent`);
|
|
394
|
+
}
|
|
395
|
+
async function observeEntity(env, url, options) {
|
|
396
|
+
if (!process.stdout.isTTY) fail(`observe requires an interactive terminal`);
|
|
397
|
+
const { renderObserve } = await import(`./observe-ui.js`);
|
|
398
|
+
renderObserve({
|
|
399
|
+
entityUrl: url,
|
|
400
|
+
baseUrl: env.electricAgentsUrl,
|
|
401
|
+
identity: env.electricAgentsIdentity,
|
|
402
|
+
initialOffset: options.from
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
async function inspectEntity(env, url) {
|
|
406
|
+
const res = await electricAgentsFetch(env, url);
|
|
407
|
+
const data = await parseJsonResponse(res);
|
|
408
|
+
if (!res.ok) failFromResponse(data, res);
|
|
409
|
+
console.log(JSON.stringify(data, null, 2));
|
|
410
|
+
}
|
|
411
|
+
async function listEntities(env, options) {
|
|
412
|
+
const entities = await fetchEntities(env, options);
|
|
413
|
+
if (entities.length === 0) {
|
|
414
|
+
console.log(`No entities found`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
entities.sort((a, b) => {
|
|
418
|
+
const aTime = Number(a.updated_at) || 0;
|
|
419
|
+
const bTime = Number(b.updated_at) || 0;
|
|
420
|
+
return bTime - aTime;
|
|
421
|
+
});
|
|
422
|
+
console.log(`${`URL`.padEnd(30)} ${`STATUS`.padEnd(10)} ${`CREATED`.padEnd(16)} LAST ACTIVE`);
|
|
423
|
+
console.log(`${`─`.repeat(30)} ${`─`.repeat(10)} ${`─`.repeat(16)} ${`─`.repeat(16)}`);
|
|
424
|
+
for (const entity of entities) {
|
|
425
|
+
const created = entity.created_at ? relativeTime(Number(entity.created_at)) : `-`;
|
|
426
|
+
const lastActive = entity.updated_at ? relativeTime(Number(entity.updated_at)) : `-`;
|
|
427
|
+
console.log(`${entity.url.padEnd(30)} ${entity.status.padEnd(10)} ${created.padEnd(16)} ${lastActive}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function killEntity(env, url) {
|
|
431
|
+
const res = await electricAgentsFetch(env, url, { method: `DELETE` });
|
|
432
|
+
if (!res.ok) {
|
|
433
|
+
const data = await parseJsonResponse(res);
|
|
434
|
+
failFromResponse(data, res);
|
|
435
|
+
}
|
|
436
|
+
console.log(`Killed ${url}`);
|
|
437
|
+
}
|
|
438
|
+
function printStartedEnvironment(env) {
|
|
439
|
+
console.log(getStartedEnvironmentMessage(env));
|
|
440
|
+
}
|
|
441
|
+
function printStoppedEnvironment(env) {
|
|
442
|
+
console.log(getStoppedEnvironmentMessage(env));
|
|
443
|
+
}
|
|
444
|
+
function createElectricCliHandlers(env, commandPrefix = commandExample(`electric`)) {
|
|
445
|
+
return {
|
|
446
|
+
listTypes: () => listTypes(env),
|
|
447
|
+
inspectType: (name) => inspectType(env, name),
|
|
448
|
+
deleteType: (name) => deleteType(env, name),
|
|
449
|
+
spawn: (urlPath, options) => spawnEntity(env, urlPath, options),
|
|
450
|
+
send: (url, message, options) => sendMessage(env, url, message, options),
|
|
451
|
+
observe: (url, options) => observeEntity(env, url, options),
|
|
452
|
+
inspect: (url) => inspectEntity(env, url),
|
|
453
|
+
ps: (options) => listEntities(env, options),
|
|
454
|
+
kill: (url) => killEntity(env, url),
|
|
455
|
+
start: async (options) => {
|
|
456
|
+
const started = await startElectricAgentsDevEnvironment(options);
|
|
457
|
+
printStartedEnvironment(started);
|
|
458
|
+
return started;
|
|
459
|
+
},
|
|
460
|
+
startBuiltin: async (options) => {
|
|
461
|
+
return startBuiltinAgentsServer(options, { agentServerUrl: env.electricAgentsUrl });
|
|
462
|
+
},
|
|
463
|
+
stop: async (options) => {
|
|
464
|
+
const stopped = await stopElectricAgentsDevEnvironment(options);
|
|
465
|
+
printStoppedEnvironment(stopped);
|
|
466
|
+
return stopped;
|
|
467
|
+
},
|
|
468
|
+
quickstart: async (options) => {
|
|
469
|
+
const started = await startElectricAgentsDevEnvironment();
|
|
470
|
+
printStartedEnvironment(started);
|
|
471
|
+
console.log(``);
|
|
472
|
+
console.log([
|
|
473
|
+
`electric agents server is up`,
|
|
474
|
+
``,
|
|
475
|
+
`Open a separate terminal and run:`,
|
|
476
|
+
` ${commandPrefix} spawn /horton/onboarding`,
|
|
477
|
+
` ${commandPrefix} send /horton/onboarding "Please walk me through onboarding for the Electric agents"`,
|
|
478
|
+
` ${commandPrefix} observe /horton/onboarding`,
|
|
479
|
+
``,
|
|
480
|
+
`UI: ${started.uiUrl}`,
|
|
481
|
+
`This terminal will now run the built-in Horton server in the foreground.`
|
|
482
|
+
].join(`\n`));
|
|
483
|
+
console.log(``);
|
|
484
|
+
await startBuiltinAgentsServer(options, { agentServerUrl: started.uiUrl });
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function getHelpText(commandName) {
|
|
489
|
+
const agentsCommand = commandExample(commandName);
|
|
490
|
+
return `
|
|
491
|
+
Environment:
|
|
492
|
+
ELECTRIC_AGENTS_URL Base URL of the server (default: ${DEFAULT_ELECTRIC_AGENTS_URL})
|
|
493
|
+
ELECTRIC_AGENTS_IDENTITY Sender identity for messages (default: ${getDefaultElectricAgentsIdentity()})
|
|
494
|
+
ANTHROPIC_API_KEY Required for '${agentsCommand} start-builtin' and '${agentsCommand} quickstart'
|
|
495
|
+
|
|
496
|
+
Examples:
|
|
497
|
+
$ ${agentsCommand} types
|
|
498
|
+
$ ${agentsCommand} spawn /horton/onboarding
|
|
499
|
+
$ ${agentsCommand} send /horton/onboarding "Please walk me through onboarding for the Electric agents"
|
|
500
|
+
$ ${agentsCommand} observe /horton/onboarding --from 0
|
|
501
|
+
$ ${agentsCommand} start
|
|
502
|
+
$ ${agentsCommand} start-builtin --anthropic-api-key sk-ant-...
|
|
503
|
+
$ ${agentsCommand} stop --remove-volumes
|
|
504
|
+
`;
|
|
505
|
+
}
|
|
506
|
+
function createElectricProgram({ env = getElectricCliEnv(), commandName = `electric`, commandPrefix = commandExample(commandName), handlers = createElectricCliHandlers(env, commandPrefix) } = {}) {
|
|
507
|
+
const program = new commander.Command();
|
|
508
|
+
program.name(commandName).description(`Manage Electric tooling`).showHelpAfterError().showSuggestionAfterError().addHelpText(`after`, getHelpText(commandName));
|
|
509
|
+
const agentsCommand = program.command(`agent`).alias(`agents`).description(`Manage Electric Agents`);
|
|
510
|
+
const typesCommand = agentsCommand.command(`types`).description(`List entity types`).action(async () => {
|
|
511
|
+
await handlers.listTypes();
|
|
512
|
+
});
|
|
513
|
+
typesCommand.command(`inspect <name>`).description(`Show entity type details`).action(async (name) => {
|
|
514
|
+
await handlers.inspectType(name);
|
|
515
|
+
});
|
|
516
|
+
typesCommand.command(`delete <name>`).description(`Delete an entity type`).action(async (name) => {
|
|
517
|
+
await handlers.deleteType(name);
|
|
518
|
+
});
|
|
519
|
+
agentsCommand.command(`spawn <url-path>`).description(`Spawn an entity from a typed URL path`).option(`--args <json>`, `Spawn arguments as JSON`).action(async (...actionArgs) => {
|
|
520
|
+
const urlPath = actionArgs[0];
|
|
521
|
+
const command = getCommandActionArg(actionArgs);
|
|
522
|
+
await handlers.spawn(urlPath, command.opts());
|
|
523
|
+
});
|
|
524
|
+
agentsCommand.command(`send <url> <message...>`).description(`Send a message to an entity`).option(`--type <msg-type>`, `Message type`).option(`--json`, `Parse message as JSON`).action(async (...actionArgs) => {
|
|
525
|
+
const url = actionArgs[0];
|
|
526
|
+
const message = actionArgs[1];
|
|
527
|
+
const command = getCommandActionArg(actionArgs);
|
|
528
|
+
const messageText = normalizeVariadicArg(message).join(` `);
|
|
529
|
+
await handlers.send(url, messageText, command.opts());
|
|
530
|
+
});
|
|
531
|
+
agentsCommand.command(`observe <url>`).description(`Observe an entity conversation`).option(`--from <offset>`, `Initial offset`).action(async (...actionArgs) => {
|
|
532
|
+
const url = actionArgs[0];
|
|
533
|
+
const command = getCommandActionArg(actionArgs);
|
|
534
|
+
await handlers.observe(url, command.opts());
|
|
535
|
+
});
|
|
536
|
+
agentsCommand.command(`inspect <url>`).description(`Show entity details`).action(async (url) => {
|
|
537
|
+
await handlers.inspect(url);
|
|
538
|
+
});
|
|
539
|
+
agentsCommand.command(`ps`).description(`List entities`).option(`--type <type>`, `Filter by entity type`).option(`--status <status>`, `Filter by status`).option(`--parent <url>`, `Filter by parent URL`).action(async (...actionArgs) => {
|
|
540
|
+
const command = getCommandActionArg(actionArgs);
|
|
541
|
+
await handlers.ps(command.opts());
|
|
542
|
+
});
|
|
543
|
+
agentsCommand.command(`kill <url>`).description(`Delete an entity`).action(async (url) => {
|
|
544
|
+
await handlers.kill(url);
|
|
545
|
+
});
|
|
546
|
+
agentsCommand.command(`start`).description(`Start the Electric Agents coordinator server`).action(async (...actionArgs) => {
|
|
547
|
+
const command = getCommandActionArg(actionArgs);
|
|
548
|
+
await handlers.start(command.opts());
|
|
549
|
+
});
|
|
550
|
+
agentsCommand.command(`start-builtin`).description(`Start runtime for Horton & other builtin agents`).option(`--anthropic-api-key <key>`, `Anthropic API key for the builtin Horton server`).action(async (...actionArgs) => {
|
|
551
|
+
const command = getCommandActionArg(actionArgs);
|
|
552
|
+
await handlers.startBuiltin(command.opts());
|
|
553
|
+
});
|
|
554
|
+
agentsCommand.command(`stop`).description(`Stop the local Electric Agents dev environment`).option(`--remove-volumes`, `Remove Docker volumes as well`).action(async (...actionArgs) => {
|
|
555
|
+
const command = getCommandActionArg(actionArgs);
|
|
556
|
+
await handlers.stop(command.opts());
|
|
557
|
+
});
|
|
558
|
+
agentsCommand.command(`quickstart`).description(`Start the coordinator server, print onboarding steps, and run builtin agents locally`).option(`--anthropic-api-key <key>`, `Anthropic API key for the builtin Horton server`).action(async (...actionArgs) => {
|
|
559
|
+
const command = getCommandActionArg(actionArgs);
|
|
560
|
+
await handlers.quickstart(command.opts());
|
|
561
|
+
});
|
|
562
|
+
const completionCommand = agentsCommand.command(`completion [action]`).description(`Set up shell completion`).addHelpText(`after`, `
|
|
563
|
+
Setup (add to your shell init file):
|
|
564
|
+
|
|
565
|
+
Bash: eval "$(${commandName} --completion)" # add to .bashrc
|
|
566
|
+
Zsh: eval "$(${commandName} --completion)" # add to .zshrc
|
|
567
|
+
Fish: ${commandName} --completion-fish | source # add to config.fish
|
|
568
|
+
|
|
569
|
+
Auto-install (detects your shell and updates init file):
|
|
570
|
+
${commandName} agent completion install
|
|
571
|
+
`).action((action) => {
|
|
572
|
+
if (action === `install`) {
|
|
573
|
+
try {
|
|
574
|
+
console.log(`Installing shell completions...`);
|
|
575
|
+
require_completions.installCompletions(commandName);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
fail(`Could not install completions: ${getErrorMessage(error)}\n Try manual setup instead: eval "$(${commandName} --completion)"`);
|
|
578
|
+
}
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
console.log(`Set up shell completions for ${commandName}.\n`);
|
|
582
|
+
console.log(`Add to your shell init file:`);
|
|
583
|
+
console.log(` Bash/Zsh: eval "$(${commandName} --completion)"`);
|
|
584
|
+
console.log(` Fish: ${commandName} --completion-fish | source\n`);
|
|
585
|
+
console.log(`Or auto-install: ${commandName} agent completion install`);
|
|
586
|
+
});
|
|
587
|
+
completionCommand.alias(`completions`);
|
|
588
|
+
return program;
|
|
589
|
+
}
|
|
590
|
+
async function run(argv = process.argv) {
|
|
591
|
+
const env = getElectricCliEnv();
|
|
592
|
+
const commandName = resolveCommandName(argv);
|
|
593
|
+
const commandPrefix = resolveCommandPrefix(argv);
|
|
594
|
+
require_completions.setupCompletions(env, commandName);
|
|
595
|
+
if (argv.includes(`--compgen`)) return;
|
|
596
|
+
const program = createElectricProgram({
|
|
597
|
+
env,
|
|
598
|
+
commandName,
|
|
599
|
+
commandPrefix
|
|
600
|
+
});
|
|
601
|
+
if (argv.length <= 2) program.help({ error: true });
|
|
602
|
+
await program.parseAsync(argv);
|
|
603
|
+
}
|
|
604
|
+
function isMainModule() {
|
|
605
|
+
if (!process.argv[1]) return false;
|
|
606
|
+
try {
|
|
607
|
+
const scriptPath = (0, node_fs.realpathSync)((0, node_path.resolve)(process.argv[1]));
|
|
608
|
+
const modulePath = (0, node_fs.realpathSync)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
609
|
+
return scriptPath === modulePath;
|
|
610
|
+
} catch (error) {
|
|
611
|
+
if (!(error instanceof Error) || !(`code` in error) || error.code !== `ENOENT`) throw error;
|
|
612
|
+
const scriptPath = (0, node_path.resolve)(process.argv[1]);
|
|
613
|
+
const modulePath = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
614
|
+
return scriptPath === modulePath;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (isMainModule()) run().catch((error) => {
|
|
618
|
+
if (error instanceof CliError) console.error(`Error: ${error.message}`);
|
|
619
|
+
else console.error(`Fatal: ${getErrorMessage(error)}`);
|
|
620
|
+
process.exit(1);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
//#endregion
|
|
624
|
+
exports.DEFAULT_ELECTRIC_AGENTS_URL = DEFAULT_ELECTRIC_AGENTS_URL
|
|
625
|
+
exports.createElectricCliHandlers = createElectricCliHandlers
|
|
626
|
+
exports.createElectricProgram = createElectricProgram
|
|
627
|
+
exports.getElectricCliEnv = getElectricCliEnv
|
|
628
|
+
exports.resolveCommandPrefix = resolveCommandPrefix
|
|
629
|
+
exports.run = run
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { DEFAULT_ELECTRIC_AGENTS_URL, ElectricCliEnv, ElectricCliHandlers, ObserveCommandOptions, PsCommandOptions, SendCommandOptions, SpawnCommandOptions, StartBuiltinCommandOptions, StartCommandOptions, StartedBuiltinAgentsEnvironment, StartedDevEnvironment, StopCommandOptions, StoppedDevEnvironment, createElectricCliHandlers, createElectricProgram, getElectricCliEnv, resolveCommandPrefix, run } from "./index-B6gIlecl.cjs";
|
|
3
|
+
export { DEFAULT_ELECTRIC_AGENTS_URL, ElectricCliEnv, ElectricCliHandlers, ObserveCommandOptions, PsCommandOptions, SendCommandOptions, SpawnCommandOptions, StartBuiltinCommandOptions, StartCommandOptions, StartedBuiltinAgentsEnvironment, StartedDevEnvironment, StopCommandOptions, StoppedDevEnvironment, createElectricCliHandlers, createElectricProgram, getElectricCliEnv, resolveCommandPrefix, run };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { DEFAULT_ELECTRIC_AGENTS_URL, ElectricCliEnv, ElectricCliHandlers, ObserveCommandOptions, PsCommandOptions, SendCommandOptions, SpawnCommandOptions, StartBuiltinCommandOptions, StartCommandOptions, StartedBuiltinAgentsEnvironment, StartedDevEnvironment, StopCommandOptions, StoppedDevEnvironment, createElectricCliHandlers, createElectricProgram, getElectricCliEnv, resolveCommandPrefix, run } from "./index-BJvlcIKL.js";
|
|
3
|
+
export { DEFAULT_ELECTRIC_AGENTS_URL, ElectricCliEnv, ElectricCliHandlers, ObserveCommandOptions, PsCommandOptions, SendCommandOptions, SpawnCommandOptions, StartBuiltinCommandOptions, StartCommandOptions, StartedBuiltinAgentsEnvironment, StartedDevEnvironment, StopCommandOptions, StoppedDevEnvironment, createElectricCliHandlers, createElectricProgram, getElectricCliEnv, resolveCommandPrefix, run };
|