@sma1lboy/kobe 0.7.12 → 0.7.13
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/dist/cli/index.js +2790 -1843
- package/dist/web-ui/assets/index-CWUdbW-Z.js +10 -0
- package/dist/web-ui/assets/index-DsZl8dp2.css +2 -0
- package/dist/web-ui/assets/routes-KpTc9h5a.js +8 -0
- package/dist/web-ui/assets/routes-O3B6rKf2.css +1 -0
- package/dist/web-ui/favicon.ico +0 -0
- package/dist/web-ui/fonts/JetBrainsMonoNerdFontMono-Regular.ttf +0 -0
- package/dist/web-ui/index.html +14 -0
- package/dist/web-ui/logo192.png +0 -0
- package/dist/web-ui/logo512.png +0 -0
- package/dist/web-ui/manifest.json +25 -0
- package/dist/web-ui/pty-server.mjs +196 -0
- package/dist/web-ui/robots.txt +3 -0
- package/package.json +5 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.kobe-diff{white-space:pre;tab-size:2}.kobe-diff-line{min-height:1.15rem;padding:0 .75rem;display:block}.kobe-diff-add{color:#9aca86;background:#1f2a1f}.kobe-diff-del{color:#d47563;background:#2a1f1c}.kobe-diff-hunk{color:#61aaf2;background:0 0}.kobe-diff-meta{color:#6b665f;background:0 0}.kobe-diff-ctx{color:#a9a39a;background:0 0}.xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>kobe-web</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CWUdbW-Z.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DsZl8dp2.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="app"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"short_name": "TanStack App",
|
|
3
|
+
"name": "Create TanStack App Sample",
|
|
4
|
+
"icons": [
|
|
5
|
+
{
|
|
6
|
+
"src": "favicon.ico",
|
|
7
|
+
"sizes": "64x64 32x32 24x24 16x16",
|
|
8
|
+
"type": "image/x-icon"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"src": "logo192.png",
|
|
12
|
+
"type": "image/png",
|
|
13
|
+
"sizes": "192x192"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"src": "logo512.png",
|
|
17
|
+
"type": "image/png",
|
|
18
|
+
"sizes": "512x512"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"start_url": ".",
|
|
22
|
+
"display": "standalone",
|
|
23
|
+
"theme_color": "#000000",
|
|
24
|
+
"background_color": "#ffffff"
|
|
25
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY server — the node half of the web terminal (node-pty doesn't work under
|
|
3
|
+
* bun, so the live terminals run here as a separate node process).
|
|
4
|
+
*
|
|
5
|
+
* Model: each web PTY tab is identified by a client-generated `tab` id. Its
|
|
6
|
+
* PTY is spawned lazily on first attach (launch spec fetched from the bun
|
|
7
|
+
* bridge by taskId + mode) and kept alive across WebSocket reconnects, so a
|
|
8
|
+
* page refresh re-attaches to the same process. Closing a tab (POST
|
|
9
|
+
* /pty/close) kills its PTY.
|
|
10
|
+
*
|
|
11
|
+
* ws /pty?tab=<id>&taskId=<id>&mode=engine|shell&cols=<n>&rows=<n>
|
|
12
|
+
* POST /pty/close { tab } kill the tab process
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createServer } from "node:http"
|
|
16
|
+
import { spawn } from "node-pty"
|
|
17
|
+
import { WebSocketServer } from "ws"
|
|
18
|
+
|
|
19
|
+
const PORT = Number.parseInt(process.env.KOBE_PTY_PORT ?? "5175", 10)
|
|
20
|
+
const BRIDGE_PORT = Number.parseInt(process.env.KOBE_BRIDGE_PORT ?? "5174", 10)
|
|
21
|
+
const SCROLLBACK_CAP = 256 * 1024 // bytes of recent output replayed on (re)attach
|
|
22
|
+
const HEALTH_PATH = "/__kobe_web"
|
|
23
|
+
const HEALTH_MARKER = "kobe-web"
|
|
24
|
+
|
|
25
|
+
/** tabId → { pty, buffer, sockets } */
|
|
26
|
+
const tabs = new Map()
|
|
27
|
+
|
|
28
|
+
function ptyEnv() {
|
|
29
|
+
const { NO_COLOR: _noColor, ...env } = process.env
|
|
30
|
+
// Codex/CI shells often set NO_COLOR=1. The browser PTY is a real terminal
|
|
31
|
+
// surface, so don't let the launcher process accidentally turn off colors in
|
|
32
|
+
// Claude, Codex, shells, or common CLI tools.
|
|
33
|
+
env.CLICOLOR = env.CLICOLOR ?? "1"
|
|
34
|
+
env.COLORTERM = env.COLORTERM || "truecolor"
|
|
35
|
+
return env
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function fetchSpec(taskId, mode) {
|
|
39
|
+
const path = mode === "shell" ? "/api/terminal-spec" : "/api/engine-spec"
|
|
40
|
+
const res = await fetch(`http://localhost:${BRIDGE_PORT}${path}?taskId=${encodeURIComponent(taskId)}`)
|
|
41
|
+
const json = await res.json()
|
|
42
|
+
if (!res.ok || json.error) throw new Error(json.error ?? `engine-spec failed (${res.status})`)
|
|
43
|
+
return json // { cwd, command: string[] }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function spawnTab(tabId, spec, cols, rows) {
|
|
47
|
+
const [cmd, ...args] = spec.command
|
|
48
|
+
const pty = spawn(cmd, args, {
|
|
49
|
+
name: "xterm-256color",
|
|
50
|
+
cols,
|
|
51
|
+
rows,
|
|
52
|
+
cwd: spec.cwd,
|
|
53
|
+
env: ptyEnv(),
|
|
54
|
+
})
|
|
55
|
+
const entry = { pty, buffer: "", sockets: new Set() }
|
|
56
|
+
pty.onData((data) => {
|
|
57
|
+
entry.buffer = (entry.buffer + data).slice(-SCROLLBACK_CAP)
|
|
58
|
+
for (const ws of entry.sockets) if (ws.readyState === ws.OPEN) ws.send(data)
|
|
59
|
+
})
|
|
60
|
+
pty.onExit(() => {
|
|
61
|
+
for (const ws of entry.sockets) if (ws.readyState === ws.OPEN) ws.close(1000, "engine exited")
|
|
62
|
+
tabs.delete(tabId)
|
|
63
|
+
})
|
|
64
|
+
tabs.set(tabId, entry)
|
|
65
|
+
return entry
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function closeTab(tabId) {
|
|
69
|
+
const entry = tabs.get(tabId)
|
|
70
|
+
if (!entry) return false
|
|
71
|
+
try {
|
|
72
|
+
entry.pty.kill()
|
|
73
|
+
} catch {
|
|
74
|
+
/* already gone */
|
|
75
|
+
}
|
|
76
|
+
tabs.delete(tabId)
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const server = createServer((req, res) => {
|
|
81
|
+
const url = new URL(req.url ?? "/", "http://localhost")
|
|
82
|
+
if (url.pathname === HEALTH_PATH) {
|
|
83
|
+
res.writeHead(200, { "content-type": "text/plain" })
|
|
84
|
+
res.end(HEALTH_MARKER)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
if (req.method === "OPTIONS") {
|
|
88
|
+
res.writeHead(204, {
|
|
89
|
+
"access-control-allow-origin": "*",
|
|
90
|
+
"access-control-allow-methods": "POST, OPTIONS",
|
|
91
|
+
"access-control-allow-headers": "content-type",
|
|
92
|
+
})
|
|
93
|
+
res.end()
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (req.method === "POST" && url.pathname === "/pty/close") {
|
|
97
|
+
let body = ""
|
|
98
|
+
req.on("data", (c) => {
|
|
99
|
+
body += c
|
|
100
|
+
})
|
|
101
|
+
req.on("end", () => {
|
|
102
|
+
let tab
|
|
103
|
+
try {
|
|
104
|
+
tab = JSON.parse(body || "{}").tab
|
|
105
|
+
} catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
const ok = tab ? closeTab(tab) : false
|
|
109
|
+
res.writeHead(200, {
|
|
110
|
+
"content-type": "application/json",
|
|
111
|
+
"access-control-allow-origin": "*",
|
|
112
|
+
})
|
|
113
|
+
res.end(JSON.stringify({ closed: ok }))
|
|
114
|
+
})
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
res.writeHead(404)
|
|
118
|
+
res.end()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const wss = new WebSocketServer({ server, path: "/pty" })
|
|
122
|
+
|
|
123
|
+
wss.on("connection", (ws, req) => {
|
|
124
|
+
const url = new URL(req.url ?? "/", "http://localhost")
|
|
125
|
+
const tabId = url.searchParams.get("tab")
|
|
126
|
+
const taskId = url.searchParams.get("taskId")
|
|
127
|
+
const cols = Number.parseInt(url.searchParams.get("cols") ?? "80", 10) || 80
|
|
128
|
+
const rows = Number.parseInt(url.searchParams.get("rows") ?? "24", 10) || 24
|
|
129
|
+
const mode = url.searchParams.get("mode") === "shell" ? "shell" : "engine"
|
|
130
|
+
|
|
131
|
+
if (!tabId || !taskId) {
|
|
132
|
+
ws.close(1008, "missing tab/taskId")
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
void (async () => {
|
|
137
|
+
let entry = tabs.get(tabId)
|
|
138
|
+
if (!entry) {
|
|
139
|
+
try {
|
|
140
|
+
const spec = await fetchSpec(taskId, mode)
|
|
141
|
+
entry = spawnTab(tabId, spec, cols, rows)
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (ws.readyState === ws.OPEN) {
|
|
144
|
+
ws.send(`\r\nfailed to start ${mode}: ${err?.message ?? err}\r\n`)
|
|
145
|
+
ws.close(1011, "spawn failed")
|
|
146
|
+
}
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
entry.sockets.add(ws)
|
|
152
|
+
// Replay recent output so a (re)attach shows current screen state.
|
|
153
|
+
if (entry.buffer && ws.readyState === ws.OPEN) ws.send(entry.buffer)
|
|
154
|
+
entry.pty.resize(cols, rows)
|
|
155
|
+
|
|
156
|
+
ws.on("message", (raw) => {
|
|
157
|
+
const text = raw.toString()
|
|
158
|
+
if (text.startsWith("{")) {
|
|
159
|
+
try {
|
|
160
|
+
const msg = JSON.parse(text)
|
|
161
|
+
if (msg && msg.type === "resize" && Number.isFinite(msg.cols) && Number.isFinite(msg.rows)) {
|
|
162
|
+
entry.pty.resize(Math.max(1, msg.cols | 0), Math.max(1, msg.rows | 0))
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
/* fall through to raw write */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
entry.pty.write(text)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
ws.on("close", () => {
|
|
173
|
+
entry.sockets.delete(ws)
|
|
174
|
+
// Keep the PTY alive for reconnects; it's killed only via /pty/close.
|
|
175
|
+
})
|
|
176
|
+
})()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
server.listen(PORT, () => {
|
|
180
|
+
process.stdout.write(`kobe pty-server listening on :${PORT} (bridge :${BRIDGE_PORT})\n`)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const shutdown = () => {
|
|
184
|
+
for (const [, entry] of tabs) {
|
|
185
|
+
try {
|
|
186
|
+
entry.pty.kill()
|
|
187
|
+
} catch {
|
|
188
|
+
/* ignore */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
wss.close()
|
|
192
|
+
server.close()
|
|
193
|
+
process.exit(0)
|
|
194
|
+
}
|
|
195
|
+
process.on("SIGINT", shutdown)
|
|
196
|
+
process.on("SIGTERM", shutdown)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@sma1lboy/kobe",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.13",
|
|
5
5
|
"description": "TUI orchestrator for Claude Code (codename)",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"packageManager": "bun@1.3.13",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"test": "bun run test:fast && bun run test:socket",
|
|
34
34
|
"test:fast": "vitest run --passWithNoTests",
|
|
35
35
|
"test:socket": "KOBE_INCLUDE_SOCKET=1 vitest run test/daemon --pool forks --minWorkers=1 --maxWorkers=1 --passWithNoTests",
|
|
36
|
+
"test:behavior": "KOBE_INCLUDE_BEHAVIOR=1 vitest run test/behavior --passWithNoTests",
|
|
36
37
|
"lint": "biome check .",
|
|
37
38
|
"knip": "knip-bun",
|
|
38
39
|
"postinstall": "bun run scripts/check-preview-deps.ts || true",
|
|
@@ -43,15 +44,17 @@
|
|
|
43
44
|
"@ansi-tools/parser": "^1.0.15",
|
|
44
45
|
"@opentui/core": "0.2.4",
|
|
45
46
|
"@opentui/solid": "0.2.4",
|
|
47
|
+
"node-pty": "^1.1.0",
|
|
48
|
+
"ws": "^8.18.0",
|
|
46
49
|
"solid-js": "1.9.12"
|
|
47
50
|
},
|
|
48
51
|
"devDependencies": {
|
|
49
52
|
"@biomejs/biome": "1.9.4",
|
|
53
|
+
"@sma1lboy/kobe-daemon": "0.7.11",
|
|
50
54
|
"@tsconfig/bun": "1.0.9",
|
|
51
55
|
"@types/bun": "1.3.12",
|
|
52
56
|
"@types/node": "25.6.2",
|
|
53
57
|
"knip": "^6.14.2",
|
|
54
|
-
"node-pty": "^1.1.0",
|
|
55
58
|
"typescript": "5.8.2",
|
|
56
59
|
"vitest": "2.1.9"
|
|
57
60
|
},
|