ever-terminal 1.0.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 +21 -0
- package/README.md +266 -0
- package/dist/claude/provider.js +234 -0
- package/dist/claude/session.js +719 -0
- package/dist/claude/summarize.js +97 -0
- package/dist/cli.js +414 -0
- package/dist/codex/app-server.js +300 -0
- package/dist/codex/memory.js +61 -0
- package/dist/codex/provider.js +362 -0
- package/dist/codex/session.js +1091 -0
- package/dist/codex/status.js +16 -0
- package/dist/codex/storage.js +83 -0
- package/dist/codex/summarize.js +69 -0
- package/dist/debug.js +9 -0
- package/dist/expose/providers/bore.js +14 -0
- package/dist/expose/providers/ngrok.js +35 -0
- package/dist/expose/providers/pinggy.js +22 -0
- package/dist/expose/registry.js +22 -0
- package/dist/expose/run.js +75 -0
- package/dist/expose/types.js +1 -0
- package/dist/index.js +78 -0
- package/dist/logger.js +44 -0
- package/dist/routes/core.js +290 -0
- package/dist/routes/events.js +104 -0
- package/dist/session.js +18 -0
- package/dist/startup/common.js +318 -0
- package/dist/startup/instance.js +89 -0
- package/dist/summary-format.js +17 -0
- package/dist/types.js +1 -0
- package/dist/update.js +56 -0
- package/dist/util/spawn-shim.js +25 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xiang Tan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# Ever Terminal
|
|
2
|
+
|
|
3
|
+
`ever-terminal` is designed to be used with the Even App.
|
|
4
|
+
|
|
5
|
+
> Building glasses-native apps instead of mirroring your laptop? See [@evenrealities/even_hub_sdk](https://www.npmjs.com/package/@evenrealities/even_hub_sdk).
|
|
6
|
+
|
|
7
|
+
Supports macOS, Linux, Windows.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- **Node.js 18+** — check with `node --version`. Install from [nodejs.org](https://nodejs.org), or `brew install node` (macOS), or your distro's package manager (Linux).
|
|
14
|
+
- An **Even Realities G2** + **R1 ring**, paired through the Even app (iOS/Android).
|
|
15
|
+
- Optional but recommended: a [Tailscale](https://tailscale.com) account signed in on both your laptop and phone — gives you a stable private network without needing the public-tunnel providers below.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g ever-terminal
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Verify the install:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
ever-terminal --version
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Update to the latest release:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g ever-terminal@latest
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
ever-terminal
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
By default the server starts on `http://localhost:3456`.
|
|
46
|
+
|
|
47
|
+
Useful startup options:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
ever-terminal --cwd /path/to/project
|
|
51
|
+
ever-terminal --port 8080
|
|
52
|
+
ever-terminal --token mytoken123
|
|
53
|
+
ever-terminal --name my-laptop
|
|
54
|
+
ever-terminal --provider codex
|
|
55
|
+
ever-terminal --expose pinggy
|
|
56
|
+
ever-terminal --expose bore
|
|
57
|
+
ever-terminal --expose ngrok
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## How it works
|
|
63
|
+
|
|
64
|
+
ever-terminal runs a local HTTP server on `:3456` (configurable via `--port`), spawns your AI agent (Claude Code or Codex) as a child process, captures its streaming output, renders it onto the G2's 576×288 canvas, and translates R1 ring gestures back into keyboard events for the agent.
|
|
65
|
+
|
|
66
|
+
The Even app connects to your laptop over your chosen transport. By default it binds to your detected LAN address (same Wi-Fi). Pass `--tailscale` to bind on your Tailscale tailnet instead (stable across networks, end-to-end WireGuard), `-i <interface>` to pin a specific network interface, or `--expose pinggy` / `--expose bore` / `--expose ngrok` to open a temporary public tunnel.
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
┌──────────────────┐
|
|
70
|
+
│ your laptop │
|
|
71
|
+
[ claude / codex ] ─│ ever-terminal │
|
|
72
|
+
│ :3456 │
|
|
73
|
+
└────────┬─────────┘
|
|
74
|
+
│
|
|
75
|
+
┌───────────────┴───────────────┐
|
|
76
|
+
│ transport (pick one) │
|
|
77
|
+
│ default: LAN (same Wi-Fi) │
|
|
78
|
+
│ --tailscale │
|
|
79
|
+
│ --expose pinggy │
|
|
80
|
+
│ --expose bore │
|
|
81
|
+
│ --expose ngrok │
|
|
82
|
+
└───────────────┬───────────────┘
|
|
83
|
+
│
|
|
84
|
+
┌────────┴─────────┐
|
|
85
|
+
│ Even app │
|
|
86
|
+
└──┬────────────┬──┘
|
|
87
|
+
(display) (input)
|
|
88
|
+
BLE ↓ ↑ BLE
|
|
89
|
+
┌────┴────┐ ┌────┴────┐
|
|
90
|
+
│ G2 │ │ R1 │
|
|
91
|
+
│ 576×288 │ │ ring │
|
|
92
|
+
└─────────┘ └─────────┘
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The agent is *your* binary running on *your* laptop. ever-terminal is a renderer + input bridge, not a runtime.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## CLI
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
ever-terminal [command] [options]
|
|
103
|
+
|
|
104
|
+
Commands:
|
|
105
|
+
ever-terminal start
|
|
106
|
+
ever-terminal complete <shell>
|
|
107
|
+
|
|
108
|
+
Local network options:
|
|
109
|
+
--tailscale
|
|
110
|
+
-i, --interface, --if <name>
|
|
111
|
+
|
|
112
|
+
Quick public expose options:
|
|
113
|
+
--expose <provider> Quick public expose provider (`pinggy`, `bore`, `ngrok`)
|
|
114
|
+
|
|
115
|
+
Options:
|
|
116
|
+
-p, --port <n>
|
|
117
|
+
-t, --token <str>
|
|
118
|
+
-n, --name <str>
|
|
119
|
+
-d, --cwd <path>
|
|
120
|
+
--provider <name> claude, codex (default: claude)
|
|
121
|
+
--log-file [path]
|
|
122
|
+
--verbose
|
|
123
|
+
-h, --help
|
|
124
|
+
-v, --version
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
ever-terminal
|
|
128
|
+
ever-terminal -p 8080
|
|
129
|
+
ever-terminal -t mytoken123
|
|
130
|
+
ever-terminal --expose pinggy
|
|
131
|
+
ever-terminal --expose ngrok
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Quick public expose helpers are intended for simple temporary sharing, not long-term use.
|
|
135
|
+
For stable setups, prefer a proper network path such as Tailscale or a production tunnel configuration.
|
|
136
|
+
|
|
137
|
+
Current quick expose providers:
|
|
138
|
+
|
|
139
|
+
### pinggy
|
|
140
|
+
|
|
141
|
+
No install required — uses your system's SSH client. Just pass `--expose pinggy`.
|
|
142
|
+
|
|
143
|
+
Behind the scenes:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
ssh -p 443 -R0:localhost:<port> a.pinggy.io
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Pinggy is a hosted SSH-tunnel service; the public URL appears in your terminal once the tunnel is up.
|
|
150
|
+
|
|
151
|
+
### bore
|
|
152
|
+
|
|
153
|
+
Self-hostable, written in Rust. GitHub: [ekzhang/bore](https://github.com/ekzhang/bore).
|
|
154
|
+
|
|
155
|
+
Install:
|
|
156
|
+
|
|
157
|
+
- **macOS:** `brew install bore-cli` *(or)* `cargo install bore-cli`
|
|
158
|
+
- **Linux:** `cargo install bore-cli` *(or)* download a prebuilt binary from [releases](https://github.com/ekzhang/bore/releases) and put `bore` on your `$PATH`
|
|
159
|
+
- **Windows:** download `bore-vX.Y.Z-x86_64-pc-windows-msvc.zip` from [releases](https://github.com/ekzhang/bore/releases), extract `bore.exe`, add its folder to your `PATH`
|
|
160
|
+
|
|
161
|
+
Verify with `bore --version`. Then `--expose bore` runs:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
bore local <port> --to bore.pub
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### ngrok
|
|
168
|
+
|
|
169
|
+
Hosted tunnel service, free tier available. Site: [ngrok.com](https://ngrok.com).
|
|
170
|
+
|
|
171
|
+
Install:
|
|
172
|
+
|
|
173
|
+
- **macOS:** `brew install ngrok/ngrok/ngrok`
|
|
174
|
+
- **Linux:** see the apt/yum repos at [ngrok.com/download](https://ngrok.com/download) — or download the tarball, extract, and put `ngrok` on your `$PATH`
|
|
175
|
+
- **Windows:** download the zip from [ngrok.com/download](https://ngrok.com/download), extract `ngrok.exe`, add its folder to your `PATH`
|
|
176
|
+
|
|
177
|
+
One-time setup — sign up at [ngrok.com](https://ngrok.com), copy your authtoken from the dashboard, then:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
ngrok config add-authtoken <your-token>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Verify with `ngrok --version`. Then `--expose ngrok` runs:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
ngrok http <port>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Shell completion (example usage):
|
|
190
|
+
|
|
191
|
+
```text
|
|
192
|
+
ever-terminal complete zsh
|
|
193
|
+
ever-terminal complete bash
|
|
194
|
+
ever-terminal complete fish
|
|
195
|
+
ever-terminal complete powershell
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Each form prints the completion script for that shell.
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
## Flow
|
|
202
|
+
|
|
203
|
+
1. Start the server with `ever-terminal`. It prints a connection URL, a token, and a QR code in the terminal.
|
|
204
|
+
2. Open the Even app on your phone and scan the QR code — or paste the URL and token manually if scanning isn't convenient.
|
|
205
|
+
3. Your G2 glasses and R1 ring connect via BLE through the Even app. The agent's output streams onto the glasses, and ring gestures route back to the agent as keyboard events.
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
## Providers
|
|
209
|
+
|
|
210
|
+
The server can drive all supported providers concurrently; `--provider <name>`
|
|
211
|
+
just chooses the **default**. Each provider wraps its own CLI — make sure the
|
|
212
|
+
binary is installed and authenticated beforehand.
|
|
213
|
+
|
|
214
|
+
| Provider | Required CLI | Auth | Override path env |
|
|
215
|
+
|------------|-----------------|--------------------------------|--------------------|
|
|
216
|
+
| `claude` | `claude` | `claude login` (Claude Code) | n/a |
|
|
217
|
+
| `codex` | `codex` | `codex` first-run wizard | n/a |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Common problems
|
|
222
|
+
|
|
223
|
+
| **Symptom** | **Cause** | **Fix** |
|
|
224
|
+
|-----------------------------------------------------------|---------------------------------------------------------|------------------------------------------------------------------------------------------|
|
|
225
|
+
| Phone app shows "Server unreachable" | Laptop and phone on different transports | Match: both on Tailscale (`--tailscale`) or both on the same Wi-Fi |
|
|
226
|
+
| `EADDRINUSE :3456` | Another ever-terminal already running | `lsof -i :3456` → kill old one, or pass `--port <other>` |
|
|
227
|
+
| `command not found: claude` or `codex` | Agent binary not on `$PATH` | Install per the Providers table; verify with `which claude` / `which codex` |
|
|
228
|
+
| `--expose pinggy` hangs | Pinggy edge timing out | Switch to `--expose bore`, `--expose ngrok`, or Tailscale |
|
|
229
|
+
| Token rotates every restart | Not passing `--token` | `ever-terminal --token my-fixed-token` |
|
|
230
|
+
| Output truncated mid-stream | Agent printed something the 576×288 layout can't render | Re-run with `--verbose --log-file ./debug.log` and open an issue with the offending line |
|
|
231
|
+
|
|
232
|
+
For anything else: `ever-terminal --verbose --log-file ./debug.log`, reproduce, attach the log to an email to software@evenrealities.com .
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Changelog
|
|
237
|
+
|
|
238
|
+
### 0.8.1
|
|
239
|
+
|
|
240
|
+
- optimize codex session history performance for large session (windows)
|
|
241
|
+
- add `--expose ngrok` support (need to sign in elsewhere first)
|
|
242
|
+
|
|
243
|
+
### 0.8.0
|
|
244
|
+
|
|
245
|
+
- codex now only starts a background process when necessary
|
|
246
|
+
|
|
247
|
+
### 0.7.9
|
|
248
|
+
|
|
249
|
+
- add debug timing for codex history api
|
|
250
|
+
- fix middle deny behavior in multiple permission requests
|
|
251
|
+
|
|
252
|
+
### 0.7.8
|
|
253
|
+
|
|
254
|
+
- add update check api
|
|
255
|
+
|
|
256
|
+
### 0.7.7
|
|
257
|
+
|
|
258
|
+
- improve codex support with `ever-terminal codex` wrapper which can sync
|
|
259
|
+
messages between codex cli and glasses
|
|
260
|
+
- internal refactoring
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { listSessions, getSessionMessages, } from "@anthropic-ai/claude-agent-sdk";
|
|
5
|
+
import { ClaudeSession } from "./session.js";
|
|
6
|
+
/** Find the jsonl file for a Claude session by scanning project dirs. */
|
|
7
|
+
function findSessionFile(sessionId) {
|
|
8
|
+
const claudeDir = join(homedir(), ".claude", "projects");
|
|
9
|
+
if (!existsSync(claudeDir))
|
|
10
|
+
return null;
|
|
11
|
+
for (const dir of readdirSync(claudeDir)) {
|
|
12
|
+
const p = join(claudeDir, dir, `${sessionId}.jsonl`);
|
|
13
|
+
if (existsSync(p))
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
/** Read last line of a jsonl file. */
|
|
19
|
+
function readLastLine(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
const data = readFileSync(filePath, "utf8");
|
|
22
|
+
const lines = data.trimEnd().split("\n");
|
|
23
|
+
if (lines.length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
return JSON.parse(lines[lines.length - 1]);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Determine session status by reading the jsonl file directly. */
|
|
32
|
+
export function claudeSessionStatus(sessionId) {
|
|
33
|
+
const file = findSessionFile(sessionId);
|
|
34
|
+
if (!file)
|
|
35
|
+
return "idle";
|
|
36
|
+
const last = readLastLine(file);
|
|
37
|
+
if (!last)
|
|
38
|
+
return "idle";
|
|
39
|
+
if (last.type === "last-prompt")
|
|
40
|
+
return "idle";
|
|
41
|
+
if (last.type === "system" &&
|
|
42
|
+
(last.subtype === "stop_hook_summary" || last.subtype === "turn_duration"))
|
|
43
|
+
return "idle";
|
|
44
|
+
if (last.type === "assistant" &&
|
|
45
|
+
last.message?.stop_reason &&
|
|
46
|
+
last.message.stop_reason !== "tool_use")
|
|
47
|
+
return "idle";
|
|
48
|
+
if (last.type === "result")
|
|
49
|
+
return "idle";
|
|
50
|
+
if (last.type === "permission-mode")
|
|
51
|
+
return "idle";
|
|
52
|
+
if (last.type === "user") {
|
|
53
|
+
const c = last.message?.content;
|
|
54
|
+
const text = typeof c === "string" ? c : c?.[0]?.text ?? "";
|
|
55
|
+
if (text.includes("Request interrupted by user"))
|
|
56
|
+
return "idle";
|
|
57
|
+
}
|
|
58
|
+
if (last.timestamp) {
|
|
59
|
+
const ageMs = Date.now() - new Date(last.timestamp).getTime();
|
|
60
|
+
if (ageMs > 120_000)
|
|
61
|
+
return "idle";
|
|
62
|
+
}
|
|
63
|
+
return "busy";
|
|
64
|
+
}
|
|
65
|
+
// ── Provider factory ────────────────────────────────────
|
|
66
|
+
export function createClaudeProvider(emit) {
|
|
67
|
+
const sessions = new Map();
|
|
68
|
+
function makeSession(sessionId) {
|
|
69
|
+
if (sessionId) {
|
|
70
|
+
const existing = sessions.get(sessionId);
|
|
71
|
+
if (existing)
|
|
72
|
+
return existing;
|
|
73
|
+
}
|
|
74
|
+
const session = new ClaudeSession(emit);
|
|
75
|
+
session.onIdReady((sid) => {
|
|
76
|
+
if (!sessions.has(sid))
|
|
77
|
+
sessions.set(sid, session);
|
|
78
|
+
});
|
|
79
|
+
if (sessionId)
|
|
80
|
+
sessions.set(sessionId, session);
|
|
81
|
+
return session;
|
|
82
|
+
}
|
|
83
|
+
async function prompt(sessionId, text, cwd) {
|
|
84
|
+
let session;
|
|
85
|
+
if (sessionId)
|
|
86
|
+
session = sessions.get(sessionId);
|
|
87
|
+
if (sessionId && !session && !findSessionFile(sessionId)) {
|
|
88
|
+
throw Object.assign(new Error(`Claude session not found: ${sessionId}`), { statusCode: 404 });
|
|
89
|
+
}
|
|
90
|
+
if (!session) {
|
|
91
|
+
session = makeSession(sessionId);
|
|
92
|
+
await session.start(sessionId, cwd);
|
|
93
|
+
}
|
|
94
|
+
// Emit user_prompt when session ID is known
|
|
95
|
+
session.onIdReady((sid) => {
|
|
96
|
+
emit(sid, { type: "user_prompt", text });
|
|
97
|
+
});
|
|
98
|
+
if (session.busy) {
|
|
99
|
+
session.enqueue(text);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
session.run(text).catch((err) => {
|
|
103
|
+
console.error(`[claude-provider] run failed: ${err.message}`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const resolvedId = session.id ??
|
|
107
|
+
(await session.waitForId(10000).catch(() => null)) ??
|
|
108
|
+
"";
|
|
109
|
+
return { sessionId: resolvedId, provider: "claude" };
|
|
110
|
+
}
|
|
111
|
+
function respondPermission(sessionId, decision) {
|
|
112
|
+
sessions.get(sessionId)?.respondPermission(decision);
|
|
113
|
+
}
|
|
114
|
+
function respondQuestion(sessionId, answer) {
|
|
115
|
+
sessions.get(sessionId)?.respondQuestion(answer);
|
|
116
|
+
}
|
|
117
|
+
function interrupt(sessionId) {
|
|
118
|
+
sessions.get(sessionId)?.interrupt();
|
|
119
|
+
}
|
|
120
|
+
function getStatus(sessionId) {
|
|
121
|
+
const session = sessions.get(sessionId);
|
|
122
|
+
if (!session)
|
|
123
|
+
return null;
|
|
124
|
+
return { state: session.status, provider: "claude" };
|
|
125
|
+
}
|
|
126
|
+
async function getSessionStatus(sessionId) {
|
|
127
|
+
const session = sessions.get(sessionId);
|
|
128
|
+
if (session)
|
|
129
|
+
return session.status;
|
|
130
|
+
return claudeSessionStatus(sessionId);
|
|
131
|
+
}
|
|
132
|
+
async function listClaudeSessions(limit, cwd) {
|
|
133
|
+
const infos = await listSessions(cwd ? { dir: cwd, limit } : { limit });
|
|
134
|
+
return infos.map((info) => ({
|
|
135
|
+
id: info.sessionId,
|
|
136
|
+
title: (info.customTitle ||
|
|
137
|
+
info.summary ||
|
|
138
|
+
info.firstPrompt ||
|
|
139
|
+
"").slice(0, 64),
|
|
140
|
+
timestamp: new Date(info.lastModified).toISOString(),
|
|
141
|
+
cwd: info.cwd || "",
|
|
142
|
+
provider: "claude",
|
|
143
|
+
status: null,
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
async function getInfo() {
|
|
147
|
+
const { exec } = await import("node:child_process");
|
|
148
|
+
const { promisify } = await import("node:util");
|
|
149
|
+
const execAsync = promisify(exec);
|
|
150
|
+
let version = "";
|
|
151
|
+
try {
|
|
152
|
+
const { stdout } = await execAsync("claude --version", {
|
|
153
|
+
timeout: 3000,
|
|
154
|
+
});
|
|
155
|
+
version = stdout.trim().replace(" (Claude Code)", "");
|
|
156
|
+
}
|
|
157
|
+
catch { }
|
|
158
|
+
let account = {};
|
|
159
|
+
let model = "";
|
|
160
|
+
try {
|
|
161
|
+
const recent = await listSessions({ limit: 3 });
|
|
162
|
+
for (const info of recent) {
|
|
163
|
+
const messages = await getSessionMessages(info.sessionId);
|
|
164
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
165
|
+
const entry = messages[i];
|
|
166
|
+
if (entry.type !== "assistant")
|
|
167
|
+
continue;
|
|
168
|
+
const m = entry.message?.model;
|
|
169
|
+
if (m) {
|
|
170
|
+
model = m;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (model)
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch { }
|
|
179
|
+
let modelDisplay = "";
|
|
180
|
+
if (model) {
|
|
181
|
+
const parts = model
|
|
182
|
+
.replace("claude-", "")
|
|
183
|
+
.replace(/-\d{8,}$/, "")
|
|
184
|
+
.split("-");
|
|
185
|
+
const name = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
|
|
186
|
+
const ver = parts.slice(1).join(".");
|
|
187
|
+
modelDisplay = name + (ver ? " " + ver : "");
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const { stdout } = await execAsync("claude auth status", {
|
|
191
|
+
timeout: 5000,
|
|
192
|
+
});
|
|
193
|
+
const auth = JSON.parse(stdout.trim());
|
|
194
|
+
account = {
|
|
195
|
+
email: auth.email ?? "",
|
|
196
|
+
organization: auth.orgName ?? "",
|
|
197
|
+
subscriptionType: auth.subscriptionType ?? "",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
catch { }
|
|
201
|
+
return {
|
|
202
|
+
account,
|
|
203
|
+
model: modelDisplay || "Unknown",
|
|
204
|
+
version: version || "Unknown",
|
|
205
|
+
provider: "claude",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const MAX_HISTORY_ITEMS = 10;
|
|
209
|
+
async function getHistory(sessionId, limit) {
|
|
210
|
+
const messages = await getSessionMessages(sessionId);
|
|
211
|
+
const reduced = messages.reduce(function (acc, msg) {
|
|
212
|
+
const contents = msg.message?.content;
|
|
213
|
+
for (const content of contents) {
|
|
214
|
+
if (content?.type === "text") {
|
|
215
|
+
acc.push({ role: msg.type, text: content.text });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return acc;
|
|
219
|
+
}, []);
|
|
220
|
+
const returnCount = Math.min(limit, MAX_HISTORY_ITEMS);
|
|
221
|
+
return reduced.slice(-returnCount);
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
listSessions: listClaudeSessions,
|
|
225
|
+
getSessionStatus,
|
|
226
|
+
getInfo,
|
|
227
|
+
getHistory,
|
|
228
|
+
prompt,
|
|
229
|
+
respondPermission,
|
|
230
|
+
respondQuestion,
|
|
231
|
+
interrupt,
|
|
232
|
+
getStatus,
|
|
233
|
+
};
|
|
234
|
+
}
|