@eidentic/cli 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/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@eidentic/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "description": "The eidentic command-line tool — dev server, project init, component scaffolding, and health diagnostics.",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/eidentic/eidentic.git",
13
+ "directory": "packages/cli"
14
+ },
15
+ "main": "./dist/index.js",
16
+ "exports": {
17
+ ".": "./dist/index.js"
18
+ },
19
+ "bin": {
20
+ "eidentic": "./dist/index.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "templates",
25
+ "LICENSE",
26
+ "README.md"
27
+ ],
28
+ "sideEffects": false,
29
+ "dependencies": {
30
+ "@clack/prompts": "^1.5.1",
31
+ "@hono/node-server": "^2.0.0",
32
+ "citty": "^0.2.2",
33
+ "consola": "^3.4.2",
34
+ "hono": "^4.12.0",
35
+ "jiti": "^2.7.0",
36
+ "picocolors": "^1.1.1",
37
+ "@eidentic/core": "0.1.0",
38
+ "@eidentic/eval": "0.1.0",
39
+ "@eidentic/server": "0.1.0",
40
+ "@eidentic/studio": "0.1.0",
41
+ "@eidentic/types": "0.1.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/react": "^19.0.0",
45
+ "react": "^19.0.0",
46
+ "@eidentic/react": "0.1.0",
47
+ "@eidentic/skills": "^0.1.0"
48
+ },
49
+ "keywords": [
50
+ "ai",
51
+ "agents",
52
+ "typescript",
53
+ "eidentic",
54
+ "cli",
55
+ "dev-server",
56
+ "scaffold"
57
+ ],
58
+ "homepage": "https://github.com/eidentic/eidentic#readme",
59
+ "bugs": {
60
+ "url": "https://github.com/eidentic/eidentic/issues"
61
+ },
62
+ "engines": {
63
+ "node": ">=22"
64
+ },
65
+ "scripts": {
66
+ "build": "tsup src/index.ts --format esm --clean",
67
+ "typecheck": "tsc --noEmit",
68
+ "typecheck:templates": "tsc --noEmit -p tsconfig.templates.json"
69
+ }
70
+ }
@@ -0,0 +1,226 @@
1
+ // Copied by `eidentic add component` — yours to edit.
2
+ "use client";
3
+
4
+ import React, { useRef, useEffect, type KeyboardEvent, type FormEvent } from "react";
5
+ import { useAgent } from "@eidentic/react";
6
+ import type { TextMessage, ToolCall } from "@eidentic/react";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Props
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface EidenticChatProps {
13
+ /** Agent ID registered in the Eidentic server. */
14
+ agentId: string;
15
+ /** Base URL of the Eidentic server. Defaults to same-origin (""). */
16
+ baseUrl?: string;
17
+ /** Placeholder text for the input field. */
18
+ placeholder?: string;
19
+ /** Class name applied to the outermost container. */
20
+ className?: string;
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Sub-components
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function MessageBubble({ message }: { message: TextMessage }) {
28
+ return (
29
+ <div className="flex justify-end">
30
+ <div className="max-w-[80%] rounded-2xl rounded-br-sm bg-indigo-500 px-4 py-2 text-sm text-white">
31
+ {message.content}
32
+ </div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ function AssistantBubble({ message }: { message: TextMessage }) {
38
+ return (
39
+ <div className="flex justify-start">
40
+ <div className="max-w-[80%] rounded-2xl rounded-bl-sm bg-white px-4 py-2 text-sm text-zinc-800 shadow-sm">
41
+ {message.content}
42
+ {message.streaming && (
43
+ <span className="ml-1 inline-block h-3 w-1 animate-pulse rounded-sm bg-indigo-400" aria-hidden="true" />
44
+ )}
45
+ </div>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ function ToolChip({ call }: { call: ToolCall }) {
51
+ return (
52
+ <div
53
+ role="status"
54
+ aria-label={`Tool call: ${call.name}`}
55
+ className="flex items-center gap-1.5 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs text-indigo-700"
56
+ >
57
+ <span className="h-1.5 w-1.5 rounded-full bg-indigo-400" aria-hidden="true" />
58
+ <span className="font-mono">{call.name}</span>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // EidenticChat
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Drop-in chat UI for a Eidentic agent.
69
+ *
70
+ * Usage:
71
+ * <EidenticChat agentId="my-agent" baseUrl="http://localhost:3000" />
72
+ */
73
+ export function EidenticChat({
74
+ agentId,
75
+ baseUrl = "",
76
+ placeholder = "Type a message…",
77
+ className = "",
78
+ }: EidenticChatProps) {
79
+ const { messages, toolCalls, result, status, error, send, stop } = useAgent(agentId, baseUrl);
80
+
81
+ const inputRef = useRef<HTMLTextAreaElement>(null);
82
+ const scrollRef = useRef<HTMLDivElement>(null);
83
+
84
+ // Autoscroll to bottom on new messages.
85
+ useEffect(() => {
86
+ const el = scrollRef.current;
87
+ if (el) {
88
+ el.scrollTop = el.scrollHeight;
89
+ }
90
+ }, [messages, toolCalls]);
91
+
92
+ const isStreaming = status === "streaming";
93
+
94
+ function handleSubmit(e: FormEvent) {
95
+ e.preventDefault();
96
+ const val = inputRef.current?.value.trim();
97
+ if (!val || isStreaming) return;
98
+ if (inputRef.current) inputRef.current.value = "";
99
+ send(val);
100
+ }
101
+
102
+ function handleKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
103
+ if (e.key === "Enter" && !e.shiftKey) {
104
+ e.preventDefault();
105
+ const val = inputRef.current?.value.trim();
106
+ if (!val || isStreaming) return;
107
+ if (inputRef.current) inputRef.current.value = "";
108
+ send(val);
109
+ }
110
+ }
111
+
112
+ // Format USD cost when available.
113
+ const costDisplay =
114
+ result?.cost?.usd != null ? `$${result.cost.usd.toFixed(6)}` : null;
115
+ const usageDisplay =
116
+ result?.usage != null
117
+ ? `${result.usage.inputTokens + result.usage.outputTokens} tokens`
118
+ : null;
119
+
120
+ return (
121
+ <div
122
+ className={`flex h-full flex-col rounded-2xl border border-zinc-200 bg-zinc-50 shadow-sm ${className}`}
123
+ role="region"
124
+ aria-label="Eidentic chat"
125
+ >
126
+ {/* Message list */}
127
+ <div
128
+ ref={scrollRef}
129
+ className="flex-1 overflow-y-auto space-y-3 px-4 py-4"
130
+ role="log"
131
+ aria-live="polite"
132
+ aria-label="Conversation"
133
+ >
134
+ {messages.length === 0 && !isStreaming && (
135
+ <p className="text-center text-sm text-zinc-400">
136
+ Send a message to start the conversation.
137
+ </p>
138
+ )}
139
+
140
+ {messages.map((msg, i) =>
141
+ msg.role === "assistant" ? (
142
+ <AssistantBubble key={i} message={msg} />
143
+ ) : (
144
+ <MessageBubble key={i} message={msg} />
145
+ ),
146
+ )}
147
+
148
+ {/* Tool-call chips */}
149
+ {toolCalls.length > 0 && (
150
+ <div className="flex flex-wrap gap-2 pt-1" aria-label="Active tool calls">
151
+ {toolCalls.map((tc) => (
152
+ <ToolChip key={tc.callId} call={tc} />
153
+ ))}
154
+ </div>
155
+ )}
156
+
157
+ {/* Error */}
158
+ {error && (
159
+ <div
160
+ role="alert"
161
+ className="rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700"
162
+ >
163
+ {error.message}
164
+ </div>
165
+ )}
166
+ </div>
167
+
168
+ {/* Footer: usage + cost */}
169
+ {(usageDisplay || costDisplay) && (
170
+ <div
171
+ className="flex items-center gap-3 border-t border-zinc-100 px-4 py-1.5 text-xs text-zinc-400"
172
+ aria-label="Usage summary"
173
+ >
174
+ {usageDisplay && <span>{usageDisplay}</span>}
175
+ {costDisplay && <span>{costDisplay}</span>}
176
+ </div>
177
+ )}
178
+
179
+ {/* Input */}
180
+ <form
181
+ onSubmit={handleSubmit}
182
+ className="flex items-end gap-2 border-t border-zinc-200 px-3 py-3"
183
+ aria-label="Message input"
184
+ >
185
+ <label htmlFor="eidentic-chat-input" className="sr-only">
186
+ Message
187
+ </label>
188
+ <textarea
189
+ id="eidentic-chat-input"
190
+ ref={inputRef}
191
+ rows={1}
192
+ placeholder={placeholder}
193
+ disabled={isStreaming}
194
+ onKeyDown={handleKeyDown}
195
+ className="flex-1 resize-none rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-800 placeholder-zinc-400 outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 disabled:opacity-60"
196
+ aria-multiline="true"
197
+ />
198
+ {isStreaming ? (
199
+ <button
200
+ type="button"
201
+ onClick={stop}
202
+ aria-label="Stop generation"
203
+ className="flex h-9 w-9 items-center justify-center rounded-xl bg-zinc-200 text-zinc-600 hover:bg-zinc-300 focus-visible:outline-2 focus-visible:outline-indigo-500"
204
+ >
205
+ <span className="block h-3.5 w-3.5 rounded-sm bg-current" aria-hidden="true" />
206
+ </button>
207
+ ) : (
208
+ <button
209
+ type="submit"
210
+ aria-label="Send message"
211
+ className="flex h-9 w-9 items-center justify-center rounded-xl bg-indigo-500 text-white hover:bg-indigo-600 focus-visible:outline-2 focus-visible:outline-indigo-500 disabled:opacity-50"
212
+ >
213
+ <svg
214
+ viewBox="0 0 16 16"
215
+ fill="currentColor"
216
+ className="h-4 w-4"
217
+ aria-hidden="true"
218
+ >
219
+ <path d="M8 2.5a.75.75 0 0 1 .75.75v7.19l2.47-2.47a.75.75 0 1 1 1.06 1.06l-3.75 3.75a.75.75 0 0 1-1.06 0L3.72 9.03a.75.75 0 1 1 1.06-1.06L7.25 10.44V3.25A.75.75 0 0 1 8 2.5Z" />
220
+ </svg>
221
+ </button>
222
+ )}
223
+ </form>
224
+ </div>
225
+ );
226
+ }
@@ -0,0 +1,273 @@
1
+ // Copied by `eidentic add component` — yours to edit.
2
+ "use client";
3
+
4
+ import React from "react";
5
+ import { useAsyncRun, useRunStatus } from "@eidentic/react";
6
+ import type { AsyncRunStatus } from "@eidentic/react";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Props
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface RunStatusProps {
13
+ /** Agent ID registered in the Eidentic server. */
14
+ agentId: string;
15
+ /**
16
+ * Run ID to watch. When provided, the component polls that run's status.
17
+ * Omit (or pass null) to render a start-button that fires a new run.
18
+ */
19
+ runId?: string | null;
20
+ /**
21
+ * Initial input for new runs. Only used when `runId` is not provided and the
22
+ * start button is clicked. Required when using the start-button variant.
23
+ */
24
+ initialInput?: unknown;
25
+ /** Base URL of the Eidentic server. Defaults to same-origin (""). */
26
+ baseUrl?: string;
27
+ /** Label for the start button. Defaults to "Start run". */
28
+ startLabel?: string;
29
+ /** Class name applied to the outermost container. */
30
+ className?: string;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Spinner
35
+ // ---------------------------------------------------------------------------
36
+
37
+ function Spinner() {
38
+ return (
39
+ <span
40
+ className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-indigo-200 border-t-indigo-500"
41
+ role="status"
42
+ aria-label="Loading"
43
+ />
44
+ );
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Status badge
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const STATUS_STYLES: Record<AsyncRunStatus, string> = {
52
+ idle: "bg-zinc-100 text-zinc-500",
53
+ running: "bg-indigo-50 text-indigo-600",
54
+ completed: "bg-emerald-50 text-emerald-700",
55
+ failed: "bg-red-50 text-red-700",
56
+ aborted: "bg-amber-50 text-amber-700",
57
+ };
58
+
59
+ function StatusPill({ status }: { status: AsyncRunStatus }) {
60
+ return (
61
+ <span
62
+ className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${STATUS_STYLES[status]}`}
63
+ aria-live="polite"
64
+ aria-atomic="true"
65
+ >
66
+ {status === "running" && <Spinner />}
67
+ {status}
68
+ </span>
69
+ );
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Watcher variant (runId provided)
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function RunWatcher({
77
+ agentId,
78
+ runId,
79
+ baseUrl,
80
+ className,
81
+ }: {
82
+ agentId: string;
83
+ runId: string;
84
+ baseUrl: string;
85
+ className: string;
86
+ }) {
87
+ const { status, output, error, isPolling } = useRunStatus(agentId, runId, { baseUrl });
88
+
89
+ const outputText =
90
+ output != null
91
+ ? typeof output === "string"
92
+ ? output
93
+ : JSON.stringify(output, null, 2)
94
+ : null;
95
+
96
+ return (
97
+ <div
98
+ className={`space-y-3 rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm ${className}`}
99
+ role="region"
100
+ aria-label="Run status"
101
+ >
102
+ {/* Header */}
103
+ <div className="flex items-center gap-3">
104
+ <span className="text-sm font-medium text-zinc-700">Run</span>
105
+ <code className="flex-1 truncate rounded bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-500">
106
+ {runId}
107
+ </code>
108
+ <StatusPill status={status} />
109
+ </div>
110
+
111
+ {/* Polling indicator */}
112
+ {isPolling && (
113
+ <p className="text-xs text-zinc-400">Polling for updates…</p>
114
+ )}
115
+
116
+ {/* Output */}
117
+ {outputText && (
118
+ <div>
119
+ <p className="mb-1 text-xs font-medium text-zinc-500">Output</p>
120
+ <pre className="overflow-x-auto rounded-xl bg-zinc-50 p-3 text-xs text-zinc-700 whitespace-pre-wrap break-all">
121
+ {outputText}
122
+ </pre>
123
+ </div>
124
+ )}
125
+
126
+ {/* Error */}
127
+ {error && (
128
+ <div
129
+ role="alert"
130
+ className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700"
131
+ >
132
+ {error}
133
+ </div>
134
+ )}
135
+ </div>
136
+ );
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Starter variant (no runId — shows a start button)
141
+ // ---------------------------------------------------------------------------
142
+
143
+ function RunStarter({
144
+ agentId,
145
+ baseUrl,
146
+ initialInput,
147
+ startLabel,
148
+ className,
149
+ }: {
150
+ agentId: string;
151
+ baseUrl: string;
152
+ initialInput: unknown;
153
+ startLabel: string;
154
+ className: string;
155
+ }) {
156
+ const { start, runId, status, output, error, isPolling } = useAsyncRun(agentId, { baseUrl });
157
+
158
+ async function handleStart() {
159
+ try {
160
+ await start(initialInput ?? "");
161
+ } catch (e) {
162
+ // error is surfaced via the hook state
163
+ void e;
164
+ }
165
+ }
166
+
167
+ const outputText =
168
+ output != null
169
+ ? typeof output === "string"
170
+ ? output
171
+ : JSON.stringify(output, null, 2)
172
+ : null;
173
+
174
+ const isRunning = status === "running";
175
+
176
+ return (
177
+ <div
178
+ className={`space-y-3 rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm ${className}`}
179
+ role="region"
180
+ aria-label="Run status"
181
+ >
182
+ {/* Header */}
183
+ <div className="flex items-center gap-3">
184
+ <span className="text-sm font-medium text-zinc-700">Agent run</span>
185
+ {status !== "idle" && <StatusPill status={status} />}
186
+ <button
187
+ type="button"
188
+ onClick={handleStart}
189
+ disabled={isRunning}
190
+ className="ml-auto rounded-xl bg-indigo-500 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-600 focus-visible:outline-2 focus-visible:outline-indigo-500 disabled:opacity-50"
191
+ aria-label={startLabel}
192
+ >
193
+ {isRunning ? "Running…" : startLabel}
194
+ </button>
195
+ </div>
196
+
197
+ {/* Run ID */}
198
+ {runId && (
199
+ <p className="font-mono text-xs text-zinc-400">
200
+ run: <code>{runId}</code>
201
+ </p>
202
+ )}
203
+
204
+ {/* Polling indicator */}
205
+ {isPolling && (
206
+ <p className="text-xs text-zinc-400">Polling for updates…</p>
207
+ )}
208
+
209
+ {/* Output */}
210
+ {outputText && (
211
+ <div>
212
+ <p className="mb-1 text-xs font-medium text-zinc-500">Output</p>
213
+ <pre className="overflow-x-auto rounded-xl bg-zinc-50 p-3 text-xs text-zinc-700 whitespace-pre-wrap break-all">
214
+ {outputText}
215
+ </pre>
216
+ </div>
217
+ )}
218
+
219
+ {/* Error */}
220
+ {error && (
221
+ <div
222
+ role="alert"
223
+ className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700"
224
+ >
225
+ {error}
226
+ </div>
227
+ )}
228
+ </div>
229
+ );
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // RunStatus — top-level component
234
+ // ---------------------------------------------------------------------------
235
+
236
+ /**
237
+ * Displays the status of an async agent run, or a start button that fires one.
238
+ *
239
+ * Usage (watch an existing run):
240
+ * <RunStatus agentId="my-agent" runId={runId} baseUrl="http://localhost:3000" />
241
+ *
242
+ * Usage (start a new run):
243
+ * <RunStatus agentId="my-agent" initialInput="Summarise the logs" baseUrl="http://localhost:3000" />
244
+ */
245
+ export function RunStatus({
246
+ agentId,
247
+ runId = null,
248
+ initialInput,
249
+ baseUrl = "",
250
+ startLabel = "Start run",
251
+ className = "",
252
+ }: RunStatusProps) {
253
+ if (runId) {
254
+ return (
255
+ <RunWatcher
256
+ agentId={agentId}
257
+ runId={runId}
258
+ baseUrl={baseUrl}
259
+ className={className}
260
+ />
261
+ );
262
+ }
263
+
264
+ return (
265
+ <RunStarter
266
+ agentId={agentId}
267
+ baseUrl={baseUrl}
268
+ initialInput={initialInput}
269
+ startLabel={startLabel}
270
+ className={className}
271
+ />
272
+ );
273
+ }