@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.
@@ -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
@@ -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)
@@ -0,0 +1,3 @@
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
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.12",
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
  },