camelagi 0.5.50 → 0.5.51

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.
Files changed (40) hide show
  1. package/dist/core/version.js +1 -1
  2. package/package.json +5 -2
  3. package/tui/package.json +23 -0
  4. package/tui/src/App.tsx +161 -0
  5. package/tui/src/agent/parse.ts +184 -0
  6. package/tui/src/agent/types.ts +75 -0
  7. package/tui/src/commands/registry.ts +301 -0
  8. package/tui/src/components/ActivityIndicator.tsx +148 -0
  9. package/tui/src/components/ApprovalPrompt.tsx +58 -0
  10. package/tui/src/components/BottomBar.tsx +74 -0
  11. package/tui/src/components/Chat.tsx +98 -0
  12. package/tui/src/components/Divider.tsx +12 -0
  13. package/tui/src/components/HorizontalRule.tsx +12 -0
  14. package/tui/src/components/Input.tsx +126 -0
  15. package/tui/src/components/Markdown.tsx +290 -0
  16. package/tui/src/components/Message.tsx +77 -0
  17. package/tui/src/components/PermissionBanner.tsx +30 -0
  18. package/tui/src/components/Picker.tsx +127 -0
  19. package/tui/src/components/SlashMenu.tsx +46 -0
  20. package/tui/src/components/SubagentBlock.tsx +16 -0
  21. package/tui/src/components/ToolBlock.tsx +24 -0
  22. package/tui/src/components/Welcome.tsx +75 -0
  23. package/tui/src/components/tools/BashTool.tsx +27 -0
  24. package/tui/src/components/tools/DefaultTool.tsx +38 -0
  25. package/tui/src/components/tools/DiffView.tsx +91 -0
  26. package/tui/src/components/tools/EditGroup.tsx +97 -0
  27. package/tui/src/components/tools/EditTool.tsx +41 -0
  28. package/tui/src/components/tools/ReadTool.tsx +41 -0
  29. package/tui/src/components/tools/SearchTool.tsx +27 -0
  30. package/tui/src/components/tools/ToolHeader.tsx +48 -0
  31. package/tui/src/components/tools/WriteTool.tsx +54 -0
  32. package/tui/src/config.ts +6 -0
  33. package/tui/src/hooks/useAgent.ts +202 -0
  34. package/tui/src/main.tsx +12 -0
  35. package/tui/src/models.ts +26 -0
  36. package/tui/src/state/reducer.ts +290 -0
  37. package/tui/src/theme.ts +28 -0
  38. package/tui/src/util/nativeNotify.ts +47 -0
  39. package/tui/src/util/spinner.ts +11 -0
  40. package/tui/tsconfig.json +19 -0
@@ -1,4 +1,4 @@
1
1
  // Auto-synced with package.json version
2
- export const VERSION = "0.5.50";
2
+ export const VERSION = "0.5.51";
3
3
  export const NAME = "camelagi";
4
4
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camelagi",
3
- "version": "0.5.50",
3
+ "version": "0.5.51",
4
4
  "description": "Personal AI agent powered by Claude Agent SDK — manage everything from Telegram",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "lint": "tsc --noEmit",
16
16
  "bundle:gateway": "node esbuild.gateway.mjs",
17
17
  "build:all": "npm run build && npm run bundle:gateway",
18
- "postinstall": "echo '\n \\033[36mCamelAGI\\033[0m installed!\\n\\n Get started:\\n camel setup # First-time setup\\n camel serve # Start server\\n camel chat # Terminal UI\\n'",
18
+ "postinstall": "echo '\n \\033[36mCamelAGI\\033[0m installed!\\n\\n Get started:\\n camel setup # First-time setup\\n camel serve # Start server\\n camel chat # Terminal UI\\n' && (command -v bun >/dev/null 2>&1 && cd tui && bun install --silent 2>/dev/null || true)",
19
19
  "prepublishOnly": "npm run build"
20
20
  },
21
21
  "dependencies": {
@@ -58,6 +58,9 @@
58
58
  "files": [
59
59
  "dist/",
60
60
  "dashboard/",
61
+ "tui/src/",
62
+ "tui/package.json",
63
+ "tui/tsconfig.json",
61
64
  "camelagi.mjs",
62
65
  "config.example.yaml",
63
66
  "README.md",
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "camelagi-tui",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "description": "CamelAGI terminal UI (OpenTUI + Bun). Connects to the CamelAGI gateway via WebSocket.",
7
+ "scripts": {
8
+ "dev": "bun src/main.tsx",
9
+ "lint": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@opentui/core": "^0.1.27",
13
+ "@opentui/react": "^0.1.27",
14
+ "diff": "^7.0.0",
15
+ "react": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/diff": "^7.0.0",
19
+ "@types/node": "^22.0.0",
20
+ "@types/react": "^19.0.0",
21
+ "typescript": "^5.6.0"
22
+ }
23
+ }
@@ -0,0 +1,161 @@
1
+ // Root component. Owns settings state (model/effort/cwd) and the optional
2
+ // picker overlay; wires everything to useAgent (WebSocket to gateway).
3
+
4
+ import { useCallback, useState } from "react"
5
+ import { useKeyboard } from "@opentui/react"
6
+ import { useAgent } from "./hooks/useAgent.js"
7
+ import { Chat } from "./components/Chat.js"
8
+ import { Input } from "./components/Input.js"
9
+ import { ApprovalPrompt } from "./components/ApprovalPrompt.js"
10
+ import { Welcome } from "./components/Welcome.js"
11
+ import { nextMode } from "./components/PermissionBanner.js"
12
+ import { ActivityIndicator } from "./components/ActivityIndicator.js"
13
+ import { HorizontalRule } from "./components/HorizontalRule.js"
14
+ import { BottomBar } from "./components/BottomBar.js"
15
+ import { SlashMenu } from "./components/SlashMenu.js"
16
+ import { Picker, type PickerItem } from "./components/Picker.js"
17
+ import { findCommand, type CommandContext, type Settings } from "./commands/registry.js"
18
+ import { DEFAULT_MODEL } from "./config.js"
19
+ import type { SlashCommand } from "./commands/registry.js"
20
+
21
+ const VERSION = "0.5.49"
22
+
23
+ export interface AppProps {
24
+ model: string
25
+ cwd: string
26
+ wsUrl?: string
27
+ token?: string
28
+ }
29
+
30
+ interface PickerState {
31
+ title: string
32
+ items: PickerItem[]
33
+ initialIndex?: number
34
+ onSelect: (value: string) => void
35
+ }
36
+
37
+ export function App(props: AppProps) {
38
+ const [settings, setSettingsState] = useState<Settings>({
39
+ model: props.model || DEFAULT_MODEL,
40
+ effort: "high",
41
+ cwd: props.cwd,
42
+ })
43
+ const setSettings = useCallback(
44
+ (patch: Partial<Settings>) => setSettingsState(s => ({ ...s, ...patch })),
45
+ [],
46
+ )
47
+
48
+ const agent = useAgent({
49
+ model: settings.model,
50
+ effort: settings.effort,
51
+ cwd: settings.cwd,
52
+ wsUrl: props.wsUrl,
53
+ token: props.token,
54
+ })
55
+
56
+ const [slashState, setSlashState] = useState<{ matches: SlashCommand[]; selectedIndex: number; argMode?: boolean } | null>(null)
57
+ const [picker, setPicker] = useState<PickerState | null>(null)
58
+
59
+ const cmdCtx: CommandContext = {
60
+ pushSystem: (text, tone) => agent.pushSystem(text, tone),
61
+ agent: {
62
+ state: agent.state,
63
+ clear: agent.clear,
64
+ abort: agent.abort,
65
+ setPermissionMode: agent.setPermissionMode,
66
+ switchModel: agent.switchModel,
67
+ wsSend: agent.wsSend,
68
+ sessionId: agent.sessionId,
69
+ },
70
+ settings,
71
+ setSettings,
72
+ openPicker: opts => setPicker(opts),
73
+ exit: () => process.exit(0),
74
+ }
75
+
76
+ const handleSlash = useCallback((name: string, args: string[]) => {
77
+ const cmd = findCommand(name)
78
+ if (!cmd) {
79
+ agent.pushSystem(`Unknown command: /${name}`, "warn")
80
+ return
81
+ }
82
+ void cmd.run(cmdCtx, args)
83
+ }, [cmdCtx, agent])
84
+
85
+ const handleCyclePermission = useCallback(() => {
86
+ const next = nextMode(agent.state.permissionMode)
87
+ agent.setPermissionMode(next)
88
+ }, [agent])
89
+
90
+ useKeyboard(key => {
91
+ if (key.ctrl && key.name === "c") {
92
+ if (agent.state.status !== "idle") agent.abort()
93
+ else process.exit(0)
94
+ }
95
+ if (key.ctrl && key.name === "l") {
96
+ agent.clear()
97
+ }
98
+ })
99
+
100
+ const busy = agent.state.status !== "idle" && agent.state.status !== "error"
101
+
102
+ const welcome = (
103
+ <Welcome
104
+ cwd={settings.cwd}
105
+ model={settings.model}
106
+ version={VERSION}
107
+ />
108
+ )
109
+
110
+ const overlayOpen = picker !== null || agent.state.pendingApproval !== null
111
+
112
+ return (
113
+ <box flexDirection="column" width="100%" height="100%">
114
+ <Chat entries={agent.state.entries} header={welcome} />
115
+ {picker ? (
116
+ <Picker
117
+ title={picker.title}
118
+ items={picker.items}
119
+ initialIndex={picker.initialIndex}
120
+ onSelect={value => {
121
+ const cb = picker.onSelect
122
+ setPicker(null)
123
+ cb(value)
124
+ }}
125
+ onCancel={() => setPicker(null)}
126
+ />
127
+ ) : null}
128
+ {agent.state.pendingApproval ? (
129
+ <ApprovalPrompt
130
+ request={agent.state.pendingApproval}
131
+ onResolve={behavior => agent.respondToApproval(behavior)}
132
+ />
133
+ ) : null}
134
+ {!overlayOpen ? (
135
+ <>
136
+ <ActivityIndicator
137
+ active={busy}
138
+ startedAt={agent.state.runStartedAt}
139
+ label={agent.state.activityLabel}
140
+ liveTokens={agent.state.liveTokens}
141
+ />
142
+ <HorizontalRule />
143
+ <Input
144
+ disabled={busy}
145
+ onSubmit={agent.submit}
146
+ onSlash={handleSlash}
147
+ onAbort={agent.abort}
148
+ onCyclePermission={handleCyclePermission}
149
+ onSlashState={setSlashState}
150
+ />
151
+ <HorizontalRule />
152
+ {slashState && slashState.matches.length > 0 ? (
153
+ <SlashMenu commands={slashState.matches} selectedIndex={slashState.selectedIndex} argMode={slashState.argMode} />
154
+ ) : (
155
+ <BottomBar state={agent.state} />
156
+ )}
157
+ </>
158
+ ) : null}
159
+ </box>
160
+ )
161
+ }
@@ -0,0 +1,184 @@
1
+ // Normalize raw stream-json events from node-host/host.mjs into the typed
2
+ // AgentEvent union. Direct port of liquidagente-desktop/src/lib/localAgent.ts:58-188.
3
+ // Keep these two in lockstep — divergence here means the TUI silently
4
+ // misses events the React app handles.
5
+
6
+ import type { AgentEvent, UsageInfo } from "./types.js"
7
+
8
+ type Raw = Record<string, unknown>
9
+
10
+ const PERMISSION_DENIAL_MARKERS = [
11
+ "requested permissions",
12
+ "requires approval",
13
+ "haven't granted",
14
+ "multiple operations",
15
+ "permission denied",
16
+ "not allowed",
17
+ ]
18
+
19
+ export function parseEvent(raw: Raw): AgentEvent[] {
20
+ const out: AgentEvent[] = []
21
+ const type = raw.type as string
22
+
23
+ if (type === "error" || type === "host_error") {
24
+ out.push({ type: "error", message: String(raw.message ?? "unknown error") })
25
+ return out
26
+ }
27
+
28
+ if (type === "approval-request") {
29
+ out.push({
30
+ type: "approval_request",
31
+ request: {
32
+ id: String(raw.id ?? ""),
33
+ tool: String(raw.tool ?? ""),
34
+ input: (raw.input as Record<string, unknown>) ?? {},
35
+ blockedPath: raw.blockedPath as string | undefined,
36
+ decisionReason: raw.decisionReason as string | undefined,
37
+ },
38
+ })
39
+ return out
40
+ }
41
+
42
+ if (type === "system") {
43
+ const subtype = raw.subtype as string
44
+ if (subtype === "init") {
45
+ out.push({ type: "init", sessionId: String(raw.session_id ?? "") })
46
+ } else if (subtype === "task_started") {
47
+ out.push({
48
+ type: "subagent_start",
49
+ agentId: String(raw.agent_id ?? "subagent"),
50
+ taskId: raw.task_id as string | undefined,
51
+ })
52
+ } else if (subtype === "task_progress") {
53
+ const ms = raw.duration_ms as number | undefined
54
+ out.push({
55
+ type: "subagent_progress",
56
+ agentId: String(raw.agent_id ?? "subagent"),
57
+ toolCount: raw.tool_count as number | undefined,
58
+ duration: ms ? Math.round(ms / 1000) : undefined,
59
+ })
60
+ } else if (subtype === "task_notification") {
61
+ out.push({
62
+ type: "subagent_done",
63
+ agentId: String(raw.agent_id ?? "subagent"),
64
+ toolUseId: raw.tool_use_id as string | undefined,
65
+ })
66
+ }
67
+ return out
68
+ }
69
+
70
+ if (type === "assistant") {
71
+ const msg = raw.message as Raw | undefined
72
+ const content = msg?.content as Array<Raw> | undefined
73
+ if (!content) return out
74
+ for (const block of content) {
75
+ if (block.type === "text" && block.text) {
76
+ out.push({ type: "stream_text", text: String(block.text) })
77
+ } else if (block.type === "tool_use") {
78
+ out.push({
79
+ type: "tool_call",
80
+ id: String(block.id ?? ""),
81
+ name: String(block.name ?? ""),
82
+ args: (block.input as Record<string, unknown>) ?? {},
83
+ })
84
+ }
85
+ }
86
+ return out
87
+ }
88
+
89
+ if (type === "user") {
90
+ const msg = raw.message as Raw | undefined
91
+ const content = msg?.content as Array<Raw> | undefined
92
+ if (!content) return out
93
+ for (const block of content) {
94
+ if (block.type !== "tool_result") continue
95
+ const resultContent = block.content
96
+ let preview = ""
97
+ if (typeof resultContent === "string") {
98
+ preview = resultContent
99
+ } else if (Array.isArray(resultContent)) {
100
+ preview = (resultContent as Array<Raw>)
101
+ .filter(b => b.type === "text")
102
+ .map(b => String(b.text ?? ""))
103
+ .join("\n")
104
+ }
105
+ const isError = block.is_error === true
106
+ const isPermissionDenial = isError && PERMISSION_DENIAL_MARKERS.some(m => preview.includes(m))
107
+ if (isPermissionDenial) {
108
+ out.push({
109
+ type: "permission_denied",
110
+ id: String(block.tool_use_id ?? ""),
111
+ message: preview,
112
+ })
113
+ }
114
+ out.push({
115
+ type: "tool_result",
116
+ id: String(block.tool_use_id ?? ""),
117
+ preview: preview.slice(0, 2000),
118
+ isError,
119
+ })
120
+ }
121
+ return out
122
+ }
123
+
124
+ if (type === "result") {
125
+ const usage = parseUsage(raw.usage as Raw | undefined)
126
+ if (usage) out.push({ type: "usage", usage })
127
+ out.push({
128
+ type: "done",
129
+ response: String(raw.result ?? ""),
130
+ subtype: raw.subtype as string | undefined,
131
+ usage,
132
+ })
133
+ return out
134
+ }
135
+
136
+ if (type === "tool_use") {
137
+ out.push({
138
+ type: "tool_call",
139
+ id: String(raw.tool_use_id ?? ""),
140
+ name: String(raw.name ?? ""),
141
+ args: (raw.input as Record<string, unknown>) ?? {},
142
+ })
143
+ return out
144
+ }
145
+
146
+ if (type === "tool_result") {
147
+ out.push({
148
+ type: "tool_result",
149
+ id: String(raw.tool_use_id ?? ""),
150
+ preview: String(raw.content ?? ""),
151
+ })
152
+ return out
153
+ }
154
+
155
+ if (type === "stream_event") {
156
+ const event = raw.event as Raw | undefined
157
+ if (!event) return out
158
+ if (event.type === "content_block_start" && (event.content_block as Raw)?.type === "thinking") {
159
+ out.push({ type: "thinking", state: "start" })
160
+ } else if (event.type === "content_block_delta") {
161
+ const delta = event.delta as Raw | undefined
162
+ if (delta?.type === "text_delta" && delta.text) {
163
+ out.push({ type: "stream_text", text: String(delta.text) })
164
+ } else if (delta?.type === "thinking_delta" && delta.thinking) {
165
+ out.push({ type: "thinking_delta", text: String(delta.thinking) })
166
+ }
167
+ } else if (event.type === "content_block_stop") {
168
+ out.push({ type: "thinking", state: "end" })
169
+ }
170
+ return out
171
+ }
172
+
173
+ return out
174
+ }
175
+
176
+ function parseUsage(u: Raw | undefined): UsageInfo | undefined {
177
+ if (!u) return undefined
178
+ return {
179
+ inputTokens: Number(u.input_tokens ?? 0),
180
+ outputTokens: Number(u.output_tokens ?? 0),
181
+ cacheReadTokens: Number(u.cache_read_input_tokens ?? 0),
182
+ cacheWriteTokens: Number(u.cache_creation_input_tokens ?? 0),
183
+ }
184
+ }
@@ -0,0 +1,75 @@
1
+ // Agent protocol types. Mirrors node-host/host.mjs in/out shapes and the
2
+ // normalized event shape produced by parse.ts (which is itself a port of
3
+ // liquidagente-desktop/src/lib/localAgent.ts:58-188).
4
+
5
+ export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "plan"
6
+
7
+ /** Which agent runtime to spawn.
8
+ * - cli: the `claude` binary directly. Auth via claude-cli login. Same path
9
+ * the desktop uses by default. Best for users who already ran
10
+ * `claude login`.
11
+ * - sdk: node-host/host.mjs (the Node sidecar that wraps the Agent SDK).
12
+ * Needed for direct API key usage or gateway-proxied auth. */
13
+ export type AgentRuntime = "cli" | "sdk"
14
+
15
+ /** Config passed to host.mjs via LIQUIDAGENTE_CONFIG env var (sdk mode) or
16
+ * translated to CLI args (cli mode). */
17
+ export interface AgentConfig {
18
+ prompt: string
19
+ model: string
20
+ cwd: string
21
+ runtime?: AgentRuntime
22
+ baseUrl?: string
23
+ authToken?: string
24
+ systemPrompt?: string
25
+ permissionMode?: PermissionMode
26
+ maxTurns?: number
27
+ effort?: string
28
+ sessionId?: string
29
+ resume?: boolean
30
+ allowedTools?: string[]
31
+ disallowedTools?: string[]
32
+ tools?: string[]
33
+ agentId?: string
34
+ }
35
+
36
+ export interface ApprovalRequest {
37
+ id: string
38
+ tool: string
39
+ input: Record<string, unknown>
40
+ blockedPath?: string
41
+ decisionReason?: string
42
+ }
43
+
44
+ export interface UsageInfo {
45
+ inputTokens: number
46
+ outputTokens: number
47
+ cacheReadTokens: number
48
+ cacheWriteTokens: number
49
+ }
50
+
51
+ export type AgentEvent =
52
+ | { type: "init"; sessionId: string }
53
+ | { type: "stream_text"; text: string }
54
+ | { type: "thinking"; state: "start" | "end" }
55
+ | { type: "thinking_delta"; text: string }
56
+ | { type: "tool_call"; id: string; name: string; args: Record<string, unknown> }
57
+ | { type: "tool_result"; id: string; preview: string; isError?: boolean }
58
+ | { type: "approval_request"; request: ApprovalRequest }
59
+ | { type: "permission_denied"; id: string; message: string }
60
+ | { type: "subagent_start"; agentId: string; taskId?: string }
61
+ | { type: "subagent_progress"; agentId: string; toolCount?: number; duration?: number }
62
+ | { type: "subagent_done"; agentId: string; toolUseId?: string }
63
+ | { type: "usage"; usage: UsageInfo }
64
+ | { type: "done"; response: string; subtype?: string; usage?: UsageInfo }
65
+ | { type: "error"; message: string }
66
+
67
+ export type ApprovalBehavior = "allow" | "deny"
68
+
69
+ /** Sent on host.mjs stdin in response to an approval-request. */
70
+ export interface ApprovalResponse {
71
+ type: "approval-response"
72
+ id: string
73
+ behavior: ApprovalBehavior
74
+ message?: string
75
+ }