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.
@@ -0,0 +1,371 @@
1
+ import { createEntityStreamDB } from "./entity-stream-db-CGP2xVeJ.js";
2
+ import { createOptimisticAction } from "@durable-streams/state";
3
+ import { buildSections, createEntityIncludesQuery, normalizeEntityTimelineData } from "@electric-ax/agents-runtime";
4
+ import React, { useEffect, useMemo, useRef, useState } from "react";
5
+ import { Box, Newline, Text, render, useInput } from "ink";
6
+ import { useLiveQuery } from "@tanstack/react-db";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+
9
+ //#region src/observe-ui.tsx
10
+ function formatTime(iso) {
11
+ if (!iso) return ``;
12
+ try {
13
+ const d = new Date(iso);
14
+ return d.toLocaleTimeString([], {
15
+ hour: `2-digit`,
16
+ minute: `2-digit`,
17
+ second: `2-digit`
18
+ });
19
+ } catch {
20
+ return ``;
21
+ }
22
+ }
23
+ function truncate(s, max) {
24
+ return s.length > max ? s.slice(0, max - 3) + `...` : s;
25
+ }
26
+ function UserMessageView({ msg }) {
27
+ const time = formatTime(msg.timestamp);
28
+ const payload = msg.payload;
29
+ let text = ``;
30
+ if (typeof payload === `string`) text = payload;
31
+ else if (typeof payload === `object` && payload !== null) {
32
+ const p = payload;
33
+ text = typeof p.text === `string` ? p.text : JSON.stringify(payload);
34
+ }
35
+ return /* @__PURE__ */ jsxs(Box, {
36
+ flexDirection: "column",
37
+ marginTop: 1,
38
+ children: [/* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
39
+ bold: true,
40
+ color: "cyan",
41
+ children: `┌ ${msg.from}`
42
+ }), time ? /* @__PURE__ */ jsx(Text, {
43
+ dimColor: true,
44
+ children: ` ${time}`
45
+ }) : null] }), text.split(`\n`).map((line, i) => /* @__PURE__ */ jsx(Text, {
46
+ color: "white",
47
+ children: `│ ${line}`
48
+ }, i))]
49
+ });
50
+ }
51
+ function AgentTextView({ text, accumulatedText, label }) {
52
+ const lines = accumulatedText.split(`\n`);
53
+ const cursor = text.status !== `completed` ? ` ▌` : ``;
54
+ return /* @__PURE__ */ jsxs(Box, {
55
+ flexDirection: "column",
56
+ marginTop: 1,
57
+ children: [/* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsx(Text, {
58
+ bold: true,
59
+ color: "green",
60
+ children: `┌ ${label ?? `assistant`}`
61
+ }) }), lines.map((line, i) => /* @__PURE__ */ jsx(Text, {
62
+ color: "white",
63
+ children: `│ ${line}${i === lines.length - 1 ? cursor : ``}`
64
+ }, i))]
65
+ });
66
+ }
67
+ function ToolCallView({ tc }) {
68
+ let statusIcon;
69
+ let statusColor;
70
+ if (tc.status === `started`) {
71
+ statusIcon = `○`;
72
+ statusColor = `yellow`;
73
+ } else if (tc.isError) {
74
+ statusIcon = `✗`;
75
+ statusColor = `red`;
76
+ } else if (tc.result !== void 0) {
77
+ statusIcon = `✓`;
78
+ statusColor = `green`;
79
+ } else {
80
+ statusIcon = `⟳`;
81
+ statusColor = `yellow`;
82
+ }
83
+ const resultStr = tc.result;
84
+ return /* @__PURE__ */ jsxs(Box, {
85
+ flexDirection: "column",
86
+ children: [
87
+ /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
88
+ color: statusColor,
89
+ children: ` ${statusIcon} `
90
+ }), /* @__PURE__ */ jsx(Text, {
91
+ bold: true,
92
+ dimColor: true,
93
+ children: tc.toolName
94
+ })] }),
95
+ resultStr !== void 0 && !tc.isError ? /* @__PURE__ */ jsx(ToolResultView, { result: resultStr }) : null,
96
+ resultStr !== void 0 && tc.isError ? /* @__PURE__ */ jsx(Text, {
97
+ color: "red",
98
+ children: ` ↳ ${truncate(resultStr, 120)}`
99
+ }) : null
100
+ ]
101
+ });
102
+ }
103
+ function ToolResultView({ result }) {
104
+ const lines = result.split(`\n`);
105
+ const maxLines = 5;
106
+ const shown = lines.slice(0, maxLines);
107
+ const remaining = lines.length - maxLines;
108
+ return /* @__PURE__ */ jsxs(Box, {
109
+ flexDirection: "column",
110
+ children: [shown.map((line, i) => /* @__PURE__ */ jsx(Text, {
111
+ dimColor: true,
112
+ children: ` │ ${truncate(line, 100)}`
113
+ }, i)), remaining > 0 ? /* @__PURE__ */ jsx(Text, {
114
+ dimColor: true,
115
+ children: ` │ ... ${remaining} more lines`
116
+ }) : null]
117
+ });
118
+ }
119
+ function MessageInput({ db, baseUrl, entityUrl, identity, disabled }) {
120
+ const [value, setValue] = useState(``);
121
+ const [error, setError] = useState(null);
122
+ const sendAction = useMemo(() => createOptimisticAction({
123
+ onMutate: ({ text }) => {
124
+ db.collections.inbox.insert({
125
+ key: `optimistic-${Date.now()}`,
126
+ from: identity,
127
+ payload: { text },
128
+ timestamp: new Date().toISOString()
129
+ });
130
+ },
131
+ mutationFn: async ({ text }) => {
132
+ const res = await fetch(`${baseUrl}${entityUrl}/send`, {
133
+ method: `POST`,
134
+ headers: { "content-type": `application/json` },
135
+ body: JSON.stringify({
136
+ from: identity,
137
+ payload: { text }
138
+ })
139
+ });
140
+ if (!res.ok) {
141
+ const body = await res.text().catch(() => ``);
142
+ let message = `Send failed (${res.status})`;
143
+ if (body) try {
144
+ const data = JSON.parse(body);
145
+ if (data.message) message = String(data.message);
146
+ else message = body;
147
+ } catch (err) {
148
+ if (err instanceof SyntaxError) message = body;
149
+ else throw err;
150
+ }
151
+ throw new Error(message);
152
+ }
153
+ }
154
+ }), [
155
+ db,
156
+ baseUrl,
157
+ entityUrl,
158
+ identity
159
+ ]);
160
+ useInput((input, key) => {
161
+ if (disabled) return;
162
+ if (key.return) {
163
+ if (value.trim()) {
164
+ setError(null);
165
+ const tx = sendAction({ text: value.trim() });
166
+ setValue(``);
167
+ tx.isPersisted.promise.catch((err) => {
168
+ setError(err.message);
169
+ });
170
+ }
171
+ return;
172
+ }
173
+ if (key.backspace || key.delete) {
174
+ setValue((prev) => prev.slice(0, -1));
175
+ return;
176
+ }
177
+ if (input && !key.ctrl && !key.meta) setValue((prev) => prev + input);
178
+ }, { isActive: !disabled });
179
+ if (disabled) return /* @__PURE__ */ jsx(Fragment, {});
180
+ return /* @__PURE__ */ jsxs(Box, {
181
+ flexDirection: "column",
182
+ marginTop: 1,
183
+ children: [error ? /* @__PURE__ */ jsx(Text, {
184
+ color: "red",
185
+ children: ` ${error}`
186
+ }) : null, /* @__PURE__ */ jsxs(Box, { children: [
187
+ /* @__PURE__ */ jsx(Text, {
188
+ color: "cyan",
189
+ bold: true,
190
+ children: `> `
191
+ }),
192
+ /* @__PURE__ */ jsx(Text, { children: value }),
193
+ /* @__PURE__ */ jsx(Text, {
194
+ dimColor: true,
195
+ children: `▌`
196
+ })
197
+ ] })]
198
+ });
199
+ }
200
+ function AgentResponseView({ section, label, isStreaming }) {
201
+ return /* @__PURE__ */ jsxs(Box, {
202
+ flexDirection: "column",
203
+ children: [
204
+ section.items.map((item, i) => {
205
+ if (item.kind === `text`) return /* @__PURE__ */ jsx(AgentTextView, {
206
+ text: {
207
+ key: `${label}-text-${i}`,
208
+ status: isStreaming ? `streaming` : `completed`
209
+ },
210
+ accumulatedText: item.text,
211
+ label
212
+ }, `${label}-text-${i}`);
213
+ return /* @__PURE__ */ jsx(ToolCallView, { tc: item }, item.toolCallId);
214
+ }),
215
+ section.done ? /* @__PURE__ */ jsx(Box, {
216
+ marginTop: 1,
217
+ children: /* @__PURE__ */ jsx(Text, {
218
+ color: "green",
219
+ children: `✓ complete`
220
+ })
221
+ }) : null,
222
+ section.error ? /* @__PURE__ */ jsx(Box, {
223
+ marginTop: 1,
224
+ children: /* @__PURE__ */ jsx(Text, {
225
+ color: "red",
226
+ children: `✗ ${section.error}`
227
+ })
228
+ }) : null
229
+ ]
230
+ });
231
+ }
232
+ function ObserveView({ db, entityUrl, baseUrl, identity }) {
233
+ const timelineQuery = useMemo(() => createEntityIncludesQuery(db), [db]);
234
+ const { data: timelineRows = [] } = useLiveQuery(timelineQuery, [timelineQuery]);
235
+ const timelineData = normalizeEntityTimelineData(timelineRows[0] ?? {
236
+ runs: [],
237
+ inbox: [],
238
+ wakes: [],
239
+ entities: []
240
+ });
241
+ const typedRuns = timelineData.runs;
242
+ const typedInbox = timelineData.inbox;
243
+ const timeline = useMemo(() => buildSections(typedRuns, typedInbox), [typedRuns, typedInbox]);
244
+ const { data: stopped = [] } = useLiveQuery((q) => q.from({ entityStopped: db.collections.entityStopped }), [db]);
245
+ const closed = stopped.length > 0;
246
+ const lastAgentIndex = useMemo(() => {
247
+ for (let i = timeline.length - 1; i >= 0; i--) if (timeline[i].kind === `agent_response`) return i;
248
+ return -1;
249
+ }, [timeline]);
250
+ return /* @__PURE__ */ jsxs(Box, {
251
+ flexDirection: "column",
252
+ children: [
253
+ /* @__PURE__ */ jsx(Box, {
254
+ marginBottom: 1,
255
+ children: /* @__PURE__ */ jsx(Text, {
256
+ dimColor: true,
257
+ children: `Observing ${entityUrl}${closed ? `` : ` (Ctrl+C to stop)`}`
258
+ })
259
+ }),
260
+ /* @__PURE__ */ jsxs(Box, {
261
+ borderStyle: "single",
262
+ borderColor: "gray",
263
+ flexDirection: "column",
264
+ paddingX: 1,
265
+ children: [
266
+ timeline.length === 0 ? /* @__PURE__ */ jsx(Text, {
267
+ dimColor: true,
268
+ children: "Waiting for events..."
269
+ }) : null,
270
+ timeline.map((section, i) => {
271
+ if (section.kind === `user_message`) return /* @__PURE__ */ jsx(UserMessageView, { msg: {
272
+ key: `timeline-${i}`,
273
+ from: section.from ?? `user`,
274
+ payload: { text: section.text },
275
+ timestamp: new Date(section.timestamp).toISOString()
276
+ } }, `msg-${i}`);
277
+ return /* @__PURE__ */ jsx(AgentResponseView, {
278
+ section,
279
+ label: entityUrl,
280
+ isStreaming: !closed && i === lastAgentIndex && !section.done
281
+ }, `agent-${i}`);
282
+ }),
283
+ closed ? /* @__PURE__ */ jsx(Box, {
284
+ marginTop: 1,
285
+ children: /* @__PURE__ */ jsx(Text, {
286
+ color: "yellow",
287
+ children: `⚠ Entity stopped`
288
+ })
289
+ }) : null
290
+ ]
291
+ }),
292
+ closed ? /* @__PURE__ */ jsxs(Box, {
293
+ marginTop: 1,
294
+ children: [/* @__PURE__ */ jsx(Text, {
295
+ color: "yellow",
296
+ children: `Stream closed`
297
+ }), /* @__PURE__ */ jsx(Newline, {})]
298
+ }) : null,
299
+ /* @__PURE__ */ jsx(MessageInput, {
300
+ db,
301
+ baseUrl,
302
+ entityUrl,
303
+ identity,
304
+ disabled: closed
305
+ })
306
+ ]
307
+ });
308
+ }
309
+ function ObserveApp({ entityUrl, baseUrl, identity, initialOffset }) {
310
+ const [db, setDb] = useState(null);
311
+ const [error, setError] = useState(null);
312
+ const closeRef = useRef(null);
313
+ useEffect(() => {
314
+ let cancelled = false;
315
+ createEntityStreamDB({
316
+ baseUrl,
317
+ entityUrl,
318
+ initialOffset
319
+ }).then((result) => {
320
+ if (cancelled) {
321
+ result.close();
322
+ return;
323
+ }
324
+ closeRef.current = result.close;
325
+ setDb(result.db);
326
+ }).catch((err) => {
327
+ if (!cancelled) setError(err instanceof Error ? err.message : String(err));
328
+ });
329
+ return () => {
330
+ cancelled = true;
331
+ closeRef.current?.();
332
+ };
333
+ }, [
334
+ baseUrl,
335
+ entityUrl,
336
+ initialOffset
337
+ ]);
338
+ if (error) return /* @__PURE__ */ jsx(Box, {
339
+ flexDirection: "column",
340
+ children: /* @__PURE__ */ jsx(Text, {
341
+ color: "red",
342
+ children: `Error: ${error}`
343
+ })
344
+ });
345
+ if (!db) return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, {
346
+ dimColor: true,
347
+ children: `Connecting to ${entityUrl}...`
348
+ }) });
349
+ return /* @__PURE__ */ jsx(ObserveView, {
350
+ db,
351
+ entityUrl,
352
+ baseUrl,
353
+ identity
354
+ });
355
+ }
356
+ function renderObserve(opts) {
357
+ const { entityUrl, baseUrl, identity, initialOffset } = opts;
358
+ const app = render(/* @__PURE__ */ jsx(ObserveApp, {
359
+ entityUrl,
360
+ baseUrl,
361
+ identity,
362
+ initialOffset
363
+ }));
364
+ process.on(`SIGINT`, () => {
365
+ app.unmount();
366
+ process.exit(0);
367
+ });
368
+ }
369
+
370
+ //#endregion
371
+ export { AgentTextView, MessageInput, ToolCallView, ToolResultView, UserMessageView, formatTime, renderObserve, truncate };
@@ -0,0 +1,68 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:18-alpine
4
+ restart: unless-stopped
5
+ command:
6
+ - postgres
7
+ - -c
8
+ - wal_level=logical
9
+ - -c
10
+ - max_connections=300
11
+ - -c
12
+ - shared_buffers=256MB
13
+ environment:
14
+ POSTGRES_DB: ${ELECTRIC_AGENTS_POSTGRES_DB:-electric_agents}
15
+ POSTGRES_USER: ${ELECTRIC_AGENTS_POSTGRES_USER:-electric_agents}
16
+ POSTGRES_PASSWORD: ${ELECTRIC_AGENTS_POSTGRES_PASSWORD:-electric_agents}
17
+ healthcheck:
18
+ test:
19
+ [
20
+ 'CMD-SHELL',
21
+ 'pg_isready -U ${ELECTRIC_AGENTS_POSTGRES_USER:-electric_agents} -d ${ELECTRIC_AGENTS_POSTGRES_DB:-electric_agents}',
22
+ ]
23
+ interval: 2s
24
+ timeout: 5s
25
+ retries: 30
26
+ volumes:
27
+ - electric-agents-postgres-data:/var/lib/postgresql/data
28
+
29
+ electric:
30
+ image: electricsql/electric:latest
31
+ restart: unless-stopped
32
+ depends_on:
33
+ postgres:
34
+ condition: service_healthy
35
+ environment:
36
+ DATABASE_URL: postgres://${ELECTRIC_AGENTS_POSTGRES_USER:-electric_agents}:${ELECTRIC_AGENTS_POSTGRES_PASSWORD:-electric_agents}@postgres:5432/${ELECTRIC_AGENTS_POSTGRES_DB:-electric_agents}
37
+ ELECTRIC_INSECURE: 'true'
38
+
39
+ electric-agents:
40
+ image: electricax/agents-server:latest
41
+ restart: unless-stopped
42
+ extra_hosts:
43
+ - 'host.docker.internal:host-gateway'
44
+ depends_on:
45
+ postgres:
46
+ condition: service_healthy
47
+ electric:
48
+ condition: service_started
49
+ environment:
50
+ ELECTRIC_AGENTS_LOG_LEVEL: ${ELECTRIC_AGENTS_LOG_LEVEL:-debug}
51
+ ELECTRIC_AGENTS_BASE_URL: http://electric-agents:4437
52
+ DATABASE_URL: postgres://${ELECTRIC_AGENTS_POSTGRES_USER:-electric_agents}:${ELECTRIC_AGENTS_POSTGRES_PASSWORD:-electric_agents}@postgres:5432/${ELECTRIC_AGENTS_POSTGRES_DB:-electric_agents}
53
+ ELECTRIC_AGENTS_HOST: 0.0.0.0
54
+ ELECTRIC_AGENTS_PORT: 4437
55
+ ELECTRIC_AGENTS_STREAMS_DATA_DIR: /var/lib/electric-agents/streams
56
+ ELECTRIC_AGENTS_DURABLE_STREAMS_URL: ${ELECTRIC_AGENTS_DURABLE_STREAMS_URL:-}
57
+ ELECTRIC_AGENTS_STREAMS_HOST: ${ELECTRIC_AGENTS_STREAMS_HOST:-127.0.0.1}
58
+ ELECTRIC_AGENTS_STREAMS_PORT: ${ELECTRIC_AGENTS_STREAMS_PORT:-0}
59
+ ELECTRIC_AGENTS_ELECTRIC_URL: ${ELECTRIC_AGENTS_ELECTRIC_URL:-http://electric:3000}
60
+ ELECTRIC_AGENTS_REWRITE_LOOPBACK_WEBHOOKS_TO: ${ELECTRIC_AGENTS_REWRITE_LOOPBACK_WEBHOOKS_TO:-host.docker.internal}
61
+ ports:
62
+ - '${ELECTRIC_AGENTS_PORT:-4437}:4437'
63
+ volumes:
64
+ - electric-agents-streams-data:/var/lib/electric-agents/streams
65
+
66
+ volumes:
67
+ electric-agents-postgres-data:
68
+ electric-agents-streams-data:
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "electric-ax",
3
+ "version": "0.1.0",
4
+ "description": "CLI for Electric Agents",
5
+ "author": "ElectricSQL team and contributors",
6
+ "license": "Apache-2.0",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "bin": {
12
+ "electric": "./dist/index.js",
13
+ "electric-ax": "./dist/index.js",
14
+ "electric-dev": "./bin/electric-dev.mjs"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "import": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "require": {
23
+ "types": "./dist/index.d.cts",
24
+ "default": "./dist/index.cjs"
25
+ }
26
+ },
27
+ "./entity-stream-db": {
28
+ "import": {
29
+ "types": "./dist/entity-stream-db.d.ts",
30
+ "default": "./dist/entity-stream-db.js"
31
+ },
32
+ "require": {
33
+ "types": "./dist/entity-stream-db.d.cts",
34
+ "default": "./dist/entity-stream-db.cjs"
35
+ }
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "dependencies": {
40
+ "@durable-streams/client": "npm:@electric-ax/durable-streams-client-beta@^0.3.0",
41
+ "@durable-streams/state": "npm:@electric-ax/durable-streams-state-beta@^0.3.0",
42
+ "@electric-sql/client": "^1.5.14",
43
+ "@tanstack/db": "^0.6.4",
44
+ "@tanstack/react-db": "^0.1.82",
45
+ "commander": "^13.1.0",
46
+ "ink": "^6.8.0",
47
+ "omelette": "^0.4.17",
48
+ "react": "^19.2.0",
49
+ "@electric-ax/agents": "0.1.0",
50
+ "@electric-ax/agents-runtime": "0.0.1"
51
+ },
52
+ "devDependencies": {
53
+ "@vitest/coverage-v8": "^4.1.0",
54
+ "@types/node": "^22.19.15",
55
+ "@types/omelette": "^0.4.5",
56
+ "@types/react": "^19.2.14",
57
+ "ink-testing-library": "^4.0.0",
58
+ "tsdown": "^0.9.0",
59
+ "tsx": "^4.19.2",
60
+ "typescript": "^5.7.2",
61
+ "vitest": "^4.1.0"
62
+ },
63
+ "files": [
64
+ "dist",
65
+ "bin",
66
+ "docker-compose.full.yml"
67
+ ],
68
+ "keywords": [
69
+ "agents",
70
+ "cli",
71
+ "electric",
72
+ "electric-agents"
73
+ ],
74
+ "engines": {
75
+ "node": ">=18.0.0"
76
+ },
77
+ "repository": {
78
+ "type": "git",
79
+ "url": "git+https://github.com/electric-sql/electric.git",
80
+ "directory": "packages/electric-ax"
81
+ },
82
+ "bugs": {
83
+ "url": "https://github.com/electric-sql/electric/issues"
84
+ },
85
+ "sideEffects": false,
86
+ "scripts": {
87
+ "build": "tsdown",
88
+ "dev": "tsdown --watch",
89
+ "test": "vitest run",
90
+ "coverage": "pnpm exec vitest run --coverage",
91
+ "typecheck": "tsc --noEmit",
92
+ "stylecheck": "eslint . --quiet"
93
+ }
94
+ }