claude-simple 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.
Files changed (50) hide show
  1. package/README.md +225 -0
  2. package/client.js +139 -0
  3. package/dist/addon-fit.min.js +2 -0
  4. package/dist/app.0.1.0.min.js +825 -0
  5. package/dist/highlight.min.css +10 -0
  6. package/dist/highlight.min.js +1244 -0
  7. package/dist/index.html +16 -0
  8. package/dist/marked.min.js +6 -0
  9. package/dist/style.0.1.0.min.css +1050 -0
  10. package/dist/xterm.min.css +209 -0
  11. package/dist/xterm.min.js +2 -0
  12. package/gateway.js +349 -0
  13. package/package.json +41 -0
  14. package/public/addon-fit.min.js +2 -0
  15. package/public/agent-panel.js +1079 -0
  16. package/public/api.js +78 -0
  17. package/public/app.js +95 -0
  18. package/public/base.css +4 -0
  19. package/public/cache.js +27 -0
  20. package/public/chat.js +1087 -0
  21. package/public/files.js +476 -0
  22. package/public/git.js +130 -0
  23. package/public/index.html +61 -0
  24. package/public/lib.js +2 -0
  25. package/public/router.js +71 -0
  26. package/public/settings.js +90 -0
  27. package/public/shell/boot.js +159 -0
  28. package/public/shell/dashboard.js +269 -0
  29. package/public/shell/hub.js +192 -0
  30. package/public/shell/layout.js +195 -0
  31. package/public/shell/notes.js +323 -0
  32. package/public/shell/session-context.js +19 -0
  33. package/public/shell/session-list.js +442 -0
  34. package/public/shell/session-nav.js +299 -0
  35. package/public/shell/templates.js +342 -0
  36. package/public/shell/util.js +63 -0
  37. package/public/shell.js +589 -0
  38. package/public/state.js +362 -0
  39. package/public/style.css +1048 -0
  40. package/public/terminal.js +177 -0
  41. package/public/xterm.min.css +209 -0
  42. package/public/xterm.min.js +2 -0
  43. package/server.js +12 -0
  44. package/src/agents/claude.js +58 -0
  45. package/src/agents/codex.js +284 -0
  46. package/src/agents/grok.js +318 -0
  47. package/src/agents/meta-agent.js +1952 -0
  48. package/src/agents/models.js +339 -0
  49. package/src/agents/sessions.js +1508 -0
  50. package/src/app.js +1282 -0
package/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # Claude Simple UI (multi-agent)
2
+
3
+ A lightweight web UI for **Claude Code**, **OpenAI Codex**, and **Grok**. No build step — pure ES Modules in the browser.
4
+
5
+ Inspired by [cc-connect](https://github.com/chenhg5/cc-connect) agent adapters: Claude via the Agent SDK, Codex via `codex exec --json`, Grok via `grok --output-format streaming-json`.
6
+
7
+ ## Deployment modes
8
+
9
+ | Mode | When to use | Hub (中心端) needed? | How to run |
10
+ |------|-------------|----------------------|------------|
11
+ | **Standalone(本地 / 单机)** | 本机开发、只有一台机器 | **不需要** | `node server.js` |
12
+ | **Hub + Edge(多机)** | 多台机器、一个公网入口、统一 WebUI | **需要** | Hub: Go 二进制;每台业务机: `npm run client` |
13
+ | **Edge only** | 已有 Hub,本机只当业务节点 | 需要已有 Hub | `npm run client` |
14
+
15
+ **本地跑不需要中心端。** 单机默认就是 Standalone:UI + 后端 + agent 都在同一个 Node 进程里。
16
+
17
+ 前端会探测 `GET /api/hub`:没有该接口即按单机模式工作,逻辑与多机无关。
18
+
19
+ ```
20
+ Standalone (default) Multi-machine (optional)
21
+ ───────────────────── ────────────────────────
22
+ Browser → node server.js Browser → Go Hub (public/)
23
+ (UI + agents) ├─► client.js @ machine A
24
+ ├─► client.js @ machine B
25
+ └─► client.js @ machine C
26
+ ```
27
+
28
+ ## Quick Start (local — no hub)
29
+
30
+ ```bash
31
+ npm install
32
+ node server.js
33
+ # open http://localhost:3000
34
+ ```
35
+
36
+ Or with auto-reload:
37
+
38
+ ```bash
39
+ npm run dev
40
+ ```
41
+
42
+ First run creates an account. Token persists 7 days — no repeated login.
43
+
44
+ Requires CLIs on `PATH` for the agents you use:
45
+
46
+ ```bash
47
+ # Claude Code — install Claude Code CLI
48
+ # Codex
49
+ npm install -g @openai/codex
50
+ # Grok (xAI) — install grok CLI and run `grok login`
51
+ ```
52
+
53
+ ### Environment Variables (standalone / edge Node app)
54
+
55
+ | Variable | Default | Description |
56
+ | ---------------------- | --------------------------------- | ---------------------------------------- |
57
+ | `PORT` | `3000` | HTTP port (standalone) |
58
+ | `AGENTS` | `claude,codex,grok` | Enabled agents (comma-separated) |
59
+ | `CLAUDE_CONFIG_DIRS` | `~/.claude` | Claude Code config dirs, comma-separated |
60
+ | `CLAUDE_CLI_PATH` | `claude` | Path to Claude Code executable |
61
+ | `CODEX_CLI_PATH` | `codex` | Path to Codex CLI |
62
+ | `CODEX_HOME` | `~/.codex` | Codex home (sessions, config) |
63
+ | `GROK_CLI_PATH` | `grok` | Path to Grok CLI |
64
+ | `GROK_HOME` | `~/.grok` | Grok home (sessions) |
65
+ | `JWT_SECRET` | persisted in `.credentials.json` | Override JWT signing key |
66
+ | `AUTH_USERNAME` / `AUTH_PASSWORD` | — | Bootstrap first user from env |
67
+
68
+ Edge-only env (when connecting to a hub):
69
+
70
+ | Variable | Description |
71
+ | ---------------- | ------------------------------------------------ |
72
+ | `GATEWAY_URL` | Hub control WS, e.g. `wss://host/machine-connect` |
73
+ | `MACHINE_TOKEN`| Shared secret with hub |
74
+ | `MACHINE_ID` | Unique id for this host (default: hostname) |
75
+ | `LOCAL_PORT` | Local bind port (default `13000`, `127.0.0.1`) |
76
+
77
+ ## Features
78
+
79
+ ### Chat
80
+
81
+ - Multi-agent: pick **Claude / Codex / Grok** in options or New Session
82
+ - Slash commands: `/agent`, `/model`, `/effort`, `/clear`
83
+ - Stream responses via WebSocket
84
+ - Markdown rendering with syntax-highlighted code blocks
85
+ - Tool use cards (expandable), permission request banners
86
+ - Long messages auto-collapse (user: 200 chars, assistant: 500 chars)
87
+ - Token usage bar after each turn: `↑12.3k ↓2.1k · 8.5k cached · $0.023`
88
+ - Shell commands via `!cmd` prefix (runs in session cwd)
89
+
90
+ ### Sessions & projects
91
+
92
+ - **Projects** view: group by working directory; show agent mix (CC/CX/GX), last activity title
93
+ - **Recent** timeline: chronological “what did I do” by day
94
+ - Filters: agent (All/Claude/Codex/Grok) + time range (Today / 7d / 14d / 30d / All) + **★ Fav**
95
+ - Search: metadata (path, title, session notes) + **deep content peek** via `GET /api/activity?q=…`
96
+ - Double-click a project header to open that cwd
97
+ - **Session conversion**: convert / clone sessions between Claude Code, Codex, and Grok
98
+ - **Project Goal + Notes**: one-line goal and multi-line notes per project (`.project_notes.json`)
99
+ - **Session favorites & notes**: star sessions; per-session notes (`.session_meta.json`)
100
+ - **Thin sessions**: ≤2 user turns show a `thin·N` badge
101
+ - Session history loads on click; sidebar is resizable
102
+
103
+ ### Files preview
104
+
105
+ - Text / code: syntax highlighting (highlight.js)
106
+ - **docx / xlsx / pptx / pdf** preview (client-side libraries; binary over base64, ≤15MB)
107
+
108
+ ### Workspaces
109
+
110
+ - Multiple Claude Code config dirs (different accounts, MCPs, settings) — Claude only
111
+ - Manage via ⚙️ → Settings: add name + path, delete
112
+ - Workspace tab bar appears when more than one workspace is configured
113
+
114
+ ### Files / Git / Shell tabs
115
+
116
+ - **Files**: browse, upload, download, delete; custom root (`~` supported)
117
+ - **Git**: status, staged/unstaged diff, graph log; custom root
118
+ - **Shell**: interactive PTY via xterm.js + Python helper (`vim` / `htop` etc.)
119
+
120
+ ### Meta Agent(内置 AI 助手)
121
+
122
+ Top bar **✦ Agent** — OpenAI-compatible tool agent (DeepSeek / OpenAI / local gateway…).
123
+
124
+ | Feature | Description |
125
+ |---------|-------------|
126
+ | **工作报告** | 手动生成今日 / 近 N 日「我做了啥」;可选整点自动日报 |
127
+ | **项目问答** | 搜会话、读摘要、项目 goal/notes、git status/log |
128
+ | **代码** | `write_file` + `run_command` 生成并运行(有危险命令拦截) |
129
+ | **VLM** | 粘贴/选图 → 上传为 `[imgN](路径)` 文本引用;需要时 `analyze_images`;`thread_id` 支持多轮追问 |
130
+
131
+ API Key 存在服务端 `.ai_config.json`(已 gitignore)。配置入口:Agent 面板 ⚙。
132
+
133
+ **Session 持久化(同一 sessionId;多图 = 多个 VLM thread 文件)**
134
+
135
+ ```
136
+ .ai/sessions/ # 或 AI_DATA_DIR/sessions
137
+ index.json
138
+ {sessionId}/
139
+ meta.json
140
+ messages.jsonl # 对话(一行一条,含 tool)
141
+ vlm/
142
+ {threadId}.jsonl # 每个看图线程单独文件
143
+ # 多图/多次 analyze → 多个 threadId
144
+ # 文件内多行 = 该线程的多轮追问
145
+ ```
146
+
147
+ 删除会话会整目录删除。旧版 `session.json`、`vlm.jsonl`、`vlm/*.json`、全局 `.ai/vlm/` 会自动迁移。
148
+
149
+ Meta Agent 的实现入口:`src/agents/meta-agent.js`、`src/app.js`(`/api/ai/*`)与 `public/agent-panel.js`。
150
+
151
+ ## Multi-machine (optional)
152
+
153
+ Only needed when sessions live on **several hosts** and you want **one public WebUI**.
154
+
155
+ For full hub/edge setup, env vars, and routing details see **[gateway/README.md](gateway/README.md)**.
156
+
157
+ ### Quick overview
158
+
159
+ | Role | Tech | Responsibility |
160
+ |------|------|----------------|
161
+ | **Hub (中心端)** | **Go** binary `claude-gateway` + `public/` | Public UI, login, merge sessions, route by `machineId` |
162
+ | **Edge (业务机)** | **Node** `npm run client` | Agents, local sessions, files, git, shell on that machine |
163
+ | **Standalone** | **Node** `node server.js` | Everything on one machine — **no hub** |
164
+
165
+ ```bash
166
+ # --- Hub (public VPS) ---
167
+ cd gateway && make linux-amd64
168
+ MACHINE_TOKEN=shared-secret \
169
+ HUB_USERNAME=admin HUB_PASSWORD=login-pass \
170
+ PUBLIC_DIR=/opt/claude-simple/public \
171
+ GATEWAY_ADDR=0.0.0.0:8080 \
172
+ ./dist/claude-gateway-linux-amd64
173
+
174
+ # --- Edge on machine A ---
175
+ MACHINE_TOKEN=shared-secret MACHINE_ID=laptop-a \
176
+ GATEWAY_URL=wss://ui.example.com/machine-connect npm run client
177
+
178
+ # --- Edge on machine B ---
179
+ MACHINE_TOKEN=shared-secret MACHINE_ID=build-b \
180
+ GATEWAY_URL=wss://ui.example.com/machine-connect npm run client
181
+ ```
182
+
183
+ In hub mode the UI:
184
+
185
+ - **Remembers** last machine in `localStorage` (no picker on every refresh)
186
+ - Full-screen picker only when first time or last machine is offline
187
+ - Top bar **Machines** menu: online/offline list + switch anytime
188
+ - Session list is filtered to the selected machine
189
+
190
+ An edge may still run `node server.js` for a **local-only** UI on that host (independent of the hub).
191
+
192
+ ### Why Node on edges? (and Go later)
193
+
194
+ - **Hub is Go** — binary deploy, no Node on the public VPS.
195
+ - **Edge stays Node for now** — Claude Agent SDK, session scanners, and streaming agents are implemented in the existing Node app.
196
+ - A full Go rewrite of the edge is **not planned short-term** (install convenience can later use a Go wrapper around the Node app if needed).
197
+
198
+ Legacy Node hub: `npm run gateway` → `gateway.js` (prefer the Go binary).
199
+
200
+ ## Stack
201
+
202
+ - **App backend (standalone / edge)**: Node.js + Express + `ws` + `@anthropic-ai/claude-agent-sdk` + Python PTY helper
203
+ - **Hub (optional multi-machine)**: Go (`gateway/`) — static UI, auth, fan-in sessions, HTTP/WS relay
204
+ - **Frontend**: ES modules + [mini-react](https://github.com/forechoandlook/mini-react) + DaisyUI + Tailwind + xterm.js
205
+ - **Storage**: IndexedDB (session cache) + JSON files (credentials, workspaces, project notes, session meta)
206
+ - **No build step** (dev): browser loads `public/*.js` directly; optional `npm run build` for `dist/`
207
+
208
+ ## Agent backends
209
+
210
+ | Agent | How it runs | Resume | Session store |
211
+ | ------ | ----------------------------------------------------- | ----------------- | ---------------------------------------- |
212
+ | Claude | `@anthropic-ai/claude-agent-sdk` → Claude Code CLI | SDK `resume` | `~/.claude/projects/**/*.jsonl` |
213
+ | Codex | `codex exec --json` / `codex exec resume <id>` | thread_id | `~/.codex/sessions/**/rollout-*.jsonl` |
214
+ | Grok | `grok -p … --output-format streaming-json` | `--resume <id>` | `~/.grok/sessions/<cwd>/<id>/` |
215
+
216
+ ## Scripts
217
+
218
+ | Script | Description |
219
+ |--------|-------------|
220
+ | `npm run dev` | Standalone with `--watch` |
221
+ | `npm start` / `npm run serve` | Production standalone |
222
+ | `npm run build` | Bundle frontend into `dist/` |
223
+ | `npm run client` | Edge worker (requires hub) |
224
+ | `npm run gateway` | Legacy Node hub |
225
+ | `npm run gateway:build` | Build Go hub via `gateway/Makefile` |
package/client.js ADDED
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ import { createApp, init } from './src/app.js';
3
+ import { WebSocket } from 'ws';
4
+ import os from 'os';
5
+ import crypto from 'crypto';
6
+
7
+ const GATEWAY_URL = process.env.GATEWAY_URL; // e.g. wss://your-server.com/machine-connect
8
+ const MACHINE_ID = process.env.MACHINE_ID || os.hostname();
9
+ const MACHINE_TOKEN = process.env.MACHINE_TOKEN; // shared secret with gateway
10
+ const LOCAL_PORT = parseInt(process.env.LOCAL_PORT || '13000');
11
+
12
+ if (!GATEWAY_URL) { console.error('GATEWAY_URL is required'); process.exit(1); }
13
+ if (!MACHINE_TOKEN) { console.error('MACHINE_TOKEN is required'); process.exit(1); }
14
+
15
+ // ─── Start local app (only binds to 127.0.0.1) ───────────────────────────────
16
+ await init();
17
+ const { server } = createApp();
18
+ await new Promise(resolve => server.listen(LOCAL_PORT, '127.0.0.1', resolve));
19
+ console.log(`[client] Local app listening on 127.0.0.1:${LOCAL_PORT}`);
20
+
21
+ // ─── Active WS tunnels: tunnelId → local WebSocket ───────────────────────────
22
+ const tunnels = new Map();
23
+
24
+ // ─── Gateway connection ───────────────────────────────────────────────────────
25
+ let controlWs = null;
26
+ let reconnectDelay = 1000;
27
+
28
+ function sendControl(msg) {
29
+ if (controlWs?.readyState === WebSocket.OPEN) {
30
+ controlWs.send(JSON.stringify(msg));
31
+ }
32
+ }
33
+
34
+ function connect() {
35
+ console.log(`[client] Connecting to gateway ${GATEWAY_URL} ...`);
36
+ controlWs = new WebSocket(GATEWAY_URL, {
37
+ headers: { 'x-machine-token': MACHINE_TOKEN },
38
+ });
39
+
40
+ controlWs.on('open', () => {
41
+ reconnectDelay = 1000;
42
+ console.log(`[client] Connected. Registering as "${MACHINE_ID}"`);
43
+ sendControl({
44
+ type: 'register',
45
+ machineId: MACHINE_ID,
46
+ meta: { hostname: os.hostname(), platform: os.platform() },
47
+ });
48
+ });
49
+
50
+ controlWs.on('message', async (raw) => {
51
+ let msg;
52
+ try { msg = JSON.parse(raw.toString()); } catch { return; }
53
+
54
+ // ── HTTP proxy ────────────────────────────────────────────────────────────
55
+ if (msg.type === 'http-req') {
56
+ try {
57
+ const url = `http://127.0.0.1:${LOCAL_PORT}${msg.path}`;
58
+ const res = await fetch(url, {
59
+ method: msg.method,
60
+ headers: { ...msg.headers, host: `127.0.0.1:${LOCAL_PORT}` },
61
+ body: msg.body || undefined,
62
+ });
63
+ const body = await res.text();
64
+ sendControl({
65
+ type: 'http-res',
66
+ reqId: msg.reqId,
67
+ status: res.status,
68
+ headers: Object.fromEntries(res.headers.entries()),
69
+ body,
70
+ });
71
+ } catch (e) {
72
+ sendControl({ type: 'http-res', reqId: msg.reqId, status: 502, headers: {}, body: e.message });
73
+ }
74
+ return;
75
+ }
76
+
77
+ // ── WS tunnel open ────────────────────────────────────────────────────────
78
+ if (msg.type === 'ws-open') {
79
+ const { tunnelId, path, query } = msg;
80
+ const localUrl = `ws://127.0.0.1:${LOCAL_PORT}${path}${query || ''}`;
81
+ const localWs = new WebSocket(localUrl);
82
+
83
+ localWs.on('open', () => {
84
+ tunnels.set(tunnelId, localWs);
85
+ sendControl({ type: 'ws-ready', tunnelId });
86
+ });
87
+
88
+ localWs.on('message', (data) => {
89
+ sendControl({ type: 'ws-msg', tunnelId, data: data.toString() });
90
+ });
91
+
92
+ localWs.on('close', () => {
93
+ tunnels.delete(tunnelId);
94
+ sendControl({ type: 'ws-close', tunnelId });
95
+ });
96
+
97
+ localWs.on('error', (e) => {
98
+ tunnels.delete(tunnelId);
99
+ sendControl({ type: 'ws-error', tunnelId, message: e.message });
100
+ });
101
+
102
+ return;
103
+ }
104
+
105
+ // ── WS tunnel message (gateway → local) ───────────────────────────────────
106
+ if (msg.type === 'ws-msg') {
107
+ const localWs = tunnels.get(msg.tunnelId);
108
+ if (localWs?.readyState === WebSocket.OPEN) localWs.send(msg.data);
109
+ return;
110
+ }
111
+
112
+ // ── WS tunnel close (gateway side closed) ─────────────────────────────────
113
+ if (msg.type === 'ws-close') {
114
+ const localWs = tunnels.get(msg.tunnelId);
115
+ tunnels.delete(msg.tunnelId);
116
+ if (localWs?.readyState === WebSocket.OPEN) localWs.close();
117
+ return;
118
+ }
119
+
120
+ if (msg.type === 'ping') {
121
+ sendControl({ type: 'pong' });
122
+ }
123
+ });
124
+
125
+ controlWs.on('close', () => {
126
+ console.log(`[client] Disconnected. Reconnecting in ${reconnectDelay}ms ...`);
127
+ // Close all open tunnels
128
+ for (const localWs of tunnels.values()) {
129
+ try { localWs.close(); } catch {}
130
+ }
131
+ tunnels.clear();
132
+ setTimeout(connect, reconnectDelay);
133
+ reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
134
+ });
135
+
136
+ controlWs.on('error', e => console.error('[client] WS error:', e.message));
137
+ }
138
+
139
+ connect();
@@ -0,0 +1,2 @@
1
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
2
+ //# sourceMappingURL=xterm-addon-fit.js.map