@sma1lboy/kobe 0.5.12 → 0.5.15

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/README.md CHANGED
@@ -28,11 +28,10 @@ that's the gap kobe fills.
28
28
 
29
29
  [![npm](https://img.shields.io/npm/v/%40sma1lboy%2Fkobe.svg)](https://www.npmjs.com/package/@sma1lboy/kobe)
30
30
 
31
- You need three things on `PATH`:
31
+ You need two things on `PATH`:
32
32
 
33
33
  - [**Bun**](https://bun.sh) ≥ 1.0 — kobe's renderer is opentui, which uses Bun-FFI.
34
34
  - [**`claude`** CLI](https://docs.anthropic.com/en/docs/claude-code) — the engine kobe drives. Run `claude --version` to confirm it's installed and signed in.
35
- - **`tmux`** — the embedded terminal pane uses one tmux session per task. On macOS: `brew install tmux`.
36
35
 
37
36
  Then:
38
37
 
@@ -148,9 +147,7 @@ and confirm `claude --version` works in the same shell you launched kobe from.
148
147
  **`bun: command not found`** — install [Bun](https://bun.sh) (`curl -fsSL https://bun.sh/install | bash`).
149
148
  kobe's renderer requires Bun ≥ 1.0; it does not run under Node.
150
149
 
151
- **The terminal pane is blank / errors about tmux** install `tmux` (`brew install tmux` on macOS).
152
- The embedded terminal is one tmux session per task; without `tmux` on `PATH`,
153
- the pane stays empty but the rest of kobe still works.
150
+ **The terminal pane is blank** kobe starts your `$SHELL` through Bun's native PTY. Confirm `$SHELL` points at an installed shell, your Bun version supports `Bun.spawn({ terminal })`, and the active task's worktree path still exists. `KOBE_TERMINAL_BACKEND=pipe` is available only as a fallback.
154
151
 
155
152
  **`posix_spawnp failed` when running `bun run test:behavior`** — on macOS arm64,
156
153
  Bun's installer occasionally ships `node-pty`'s prebuilt `spawn-helper` without
package/dist/bin/kobed.js CHANGED
@@ -1,6 +1,36 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
3
5
  var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
13
+ var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
21
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
22
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
+ for (let key of __getOwnPropNames(mod))
24
+ if (!__hasOwnProp.call(to, key))
25
+ __defProp(to, key, {
26
+ get: __accessProp.bind(mod, key),
27
+ enumerable: true
28
+ });
29
+ if (canCache)
30
+ cache.set(mod, to);
31
+ return to;
32
+ };
33
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
4
34
  var __returnValue = (v) => v;
5
35
  function __exportSetter(name, newValue) {
6
36
  this[name] = __returnValue.bind(null, newValue);
@@ -99,30 +129,36 @@ class KobeDaemonClient {
99
129
  nextId = 1;
100
130
  pending = new Map;
101
131
  handlers = new Map;
132
+ lifecycleHandlers = new Map;
133
+ connecting = null;
134
+ disposed = false;
102
135
  constructor(socketPath) {
103
136
  this.socketPath = socketPath;
104
137
  }
105
138
  connect() {
106
139
  if (this.socket)
107
140
  return Promise.resolve();
108
- return new Promise((resolve, reject) => {
109
- const socket = connect(this.socketPath);
110
- this.socket = socket;
111
- socket.once("connect", resolve);
112
- socket.once("error", reject);
113
- socket.on("data", (chunk) => this.onData(chunk.toString("utf8")));
114
- socket.on("close", () => {
115
- this.socket = null;
116
- for (const pending of this.pending.values())
117
- pending.reject(new Error("daemon connection closed"));
118
- this.pending.clear();
119
- });
120
- });
141
+ if (this.disposed)
142
+ return Promise.reject(new Error("daemon client disposed"));
143
+ if (this.connecting)
144
+ return this.connecting;
145
+ const p = this.openSocket();
146
+ this.connecting = p;
147
+ const cleanup = () => {
148
+ if (this.connecting === p)
149
+ this.connecting = null;
150
+ };
151
+ p.then(cleanup, cleanup);
152
+ return p;
121
153
  }
122
154
  close() {
155
+ this.disposed = true;
123
156
  this.socket?.end();
124
157
  this.socket = null;
125
158
  }
159
+ forceDisconnect() {
160
+ this.socket?.destroy();
161
+ }
126
162
  on(name, handler) {
127
163
  let set = this.handlers.get(name);
128
164
  if (!set) {
@@ -136,6 +172,19 @@ class KobeDaemonClient {
136
172
  this.handlers.delete(name);
137
173
  };
138
174
  }
175
+ onLifecycle(name, handler) {
176
+ let set = this.lifecycleHandlers.get(name);
177
+ if (!set) {
178
+ set = new Set;
179
+ this.lifecycleHandlers.set(name, set);
180
+ }
181
+ set.add(handler);
182
+ return () => {
183
+ set?.delete(handler);
184
+ if (set?.size === 0)
185
+ this.lifecycleHandlers.delete(name);
186
+ };
187
+ }
139
188
  async request(name, payload) {
140
189
  await this.connect();
141
190
  const socket = this.socket;
@@ -148,6 +197,44 @@ class KobeDaemonClient {
148
197
  socket.write(frameToLine({ type: "request", id, name, payload }));
149
198
  return promise;
150
199
  }
200
+ openSocket() {
201
+ return new Promise((resolve, reject) => {
202
+ const socket = connect(this.socketPath);
203
+ this.socket = socket;
204
+ const onConnect = () => {
205
+ socket.off("error", onError);
206
+ resolve();
207
+ };
208
+ const onError = (err) => {
209
+ socket.off("connect", onConnect);
210
+ if (this.socket === socket)
211
+ this.socket = null;
212
+ reject(err);
213
+ };
214
+ socket.once("connect", onConnect);
215
+ socket.once("error", onError);
216
+ socket.on("data", (chunk) => this.onData(chunk.toString("utf8")));
217
+ socket.on("close", () => this.onSocketClose(socket));
218
+ });
219
+ }
220
+ onSocketClose(which) {
221
+ if (this.socket !== which && this.socket !== null)
222
+ return;
223
+ this.socket = null;
224
+ for (const pending of this.pending.values())
225
+ pending.reject(new Error("daemon connection closed"));
226
+ this.pending.clear();
227
+ this.emitLifecycle("close");
228
+ }
229
+ emitLifecycle(name) {
230
+ for (const handler of this.lifecycleHandlers.get(name) ?? []) {
231
+ try {
232
+ handler();
233
+ } catch (err) {
234
+ console.error(`[kobe] lifecycle handler for "${name}" threw:`, err);
235
+ }
236
+ }
237
+ }
151
238
  onData(chunk) {
152
239
  this.buffer += chunk;
153
240
  let nl = this.buffer.indexOf(`
@@ -192,11 +279,10 @@ import { spawn } from "child_process";
192
279
  import { existsSync } from "fs";
193
280
  import { dirname, join as join2, resolve } from "path";
194
281
  import { fileURLToPath } from "url";
195
- async function connectOrStartDaemon() {
282
+ async function ensureDaemonReachable() {
196
283
  const socketPath = defaultDaemonSocketPath();
197
- const client = new KobeDaemonClient(socketPath);
198
- if (await canConnect(client))
199
- return client;
284
+ if (await testCanConnect(socketPath))
285
+ return socketPath;
200
286
  const { entry, runWithBun } = resolveKobedEntry();
201
287
  const child = runWithBun ? spawn(process.execPath, [entry, "start"], {
202
288
  detached: true,
@@ -211,24 +297,26 @@ async function connectOrStartDaemon() {
211
297
  const deadline = Date.now() + 5000;
212
298
  let lastErr;
213
299
  while (Date.now() < deadline) {
214
- const next = new KobeDaemonClient(socketPath);
215
- try {
216
- await next.connect();
217
- return next;
218
- } catch (err) {
219
- lastErr = err;
220
- next.close();
221
- await new Promise((resolve2) => setTimeout(resolve2, 100));
222
- }
300
+ if (await testCanConnect(socketPath))
301
+ return socketPath;
302
+ await new Promise((resolveTimer) => setTimeout(resolveTimer, 100));
223
303
  }
224
- throw new Error(`kobe: daemon did not start at ${socketPath}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
304
+ throw new Error(`kobe: daemon did not start at ${socketPath}: ${lastErr instanceof Error ? lastErr.message : "timeout"}`);
225
305
  }
226
- async function canConnect(client) {
306
+ async function connectOrStartDaemon() {
307
+ const socketPath = await ensureDaemonReachable();
308
+ const client = new KobeDaemonClient(socketPath);
309
+ await client.connect();
310
+ return client;
311
+ }
312
+ async function testCanConnect(socketPath) {
313
+ const probe = new KobeDaemonClient(socketPath);
227
314
  try {
228
- await client.connect();
315
+ await probe.connect();
316
+ probe.close();
229
317
  return true;
230
318
  } catch {
231
- client.close();
319
+ probe.close();
232
320
  return false;
233
321
  }
234
322
  }
@@ -2613,9 +2701,6 @@ function kobeStateDir() {
2613
2701
  function kvStatePath() {
2614
2702
  return join5(homeDir(), ".config", "kobe", "state.json");
2615
2703
  }
2616
- function tmuxBin() {
2617
- return process.env.KOBE_TMUX_BIN ?? "tmux";
2618
- }
2619
2704
  var init_env = () => {};
2620
2705
 
2621
2706
  // src/state/repos.ts