speexor 0.1.1 → 0.2.1

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 (54) hide show
  1. package/API-REFERENCE.md +96 -1
  2. package/ARCHITECTURE.md +83 -32
  3. package/BENCHMARKS.md +73 -0
  4. package/CHANGELOG.md +59 -4
  5. package/CODE-OF-CONDUCT.md +83 -83
  6. package/CONTRIBUTING.md +92 -97
  7. package/FAQ.md +132 -105
  8. package/GLOSSARY.md +34 -0
  9. package/LICENSE.md +21 -21
  10. package/PUBLISH.md +82 -77
  11. package/README.md +220 -6
  12. package/REFACTOR-LOG.md +40 -40
  13. package/ROADMAP.md +31 -42
  14. package/SECURITY-DEFAULTS.md +118 -0
  15. package/SECURITY.md +80 -79
  16. package/SUMMARY.md +31 -8
  17. package/TESTING.md +140 -140
  18. package/dist/{agent-5D3BVWNK.js → agent-C64T66XT.js} +4 -4
  19. package/dist/agent-C64T66XT.js.map +1 -0
  20. package/dist/{chunk-B7WLHC4W.js → chunk-5OD5UWB5.js} +322 -121
  21. package/dist/chunk-5OD5UWB5.js.map +1 -0
  22. package/dist/chunk-GOGI3JQD.js +1637 -0
  23. package/dist/chunk-GOGI3JQD.js.map +1 -0
  24. package/dist/{chunk-2F66BZYJ.js → chunk-VEZQT5SX.js} +80 -8
  25. package/dist/chunk-VEZQT5SX.js.map +1 -0
  26. package/dist/cli/index.js +2058 -18
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/core/index.d.ts +682 -3
  29. package/dist/core/index.js +1 -1
  30. package/dist/index.d.ts +102 -14
  31. package/dist/index.js +55 -29
  32. package/dist/index.js.map +1 -1
  33. package/dist/plugins/index.d.ts +1 -1
  34. package/dist/plugins/index.js +1 -1
  35. package/dist/types-BOMap-tI.d.ts +389 -0
  36. package/docs/PRD03.md +119 -0
  37. package/docs/PRD06.md +125 -0
  38. package/docs/SETUP.md +94 -94
  39. package/docs/TROUBLESHOOTING.md +113 -113
  40. package/docs/adr/0001-record-architecture-decisions.md +44 -0
  41. package/docs/adr/0002-plugin-architecture.md +53 -0
  42. package/docs/adr/0003-recursive-task-decomposition.md +57 -0
  43. package/docs/adr/0004-local-first-security.md +58 -0
  44. package/docs/adr/0005-data-directory-layout.md +69 -0
  45. package/examples/basic.yaml +61 -61
  46. package/package.json +103 -102
  47. package/schema/config.schema.json +119 -119
  48. package/speexor.config.yaml.example +30 -30
  49. package/dist/agent-5D3BVWNK.js.map +0 -1
  50. package/dist/chunk-2F66BZYJ.js.map +0 -1
  51. package/dist/chunk-B7WLHC4W.js.map +0 -1
  52. package/dist/chunk-SXALZEOJ.js +0 -345
  53. package/dist/chunk-SXALZEOJ.js.map +0 -1
  54. package/dist/types-0q_okI2g.d.ts +0 -205
@@ -3,9 +3,37 @@ import { execSync, spawn } from 'child_process';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { existsSync, mkdirSync, createWriteStream } from 'fs';
5
5
  import { join } from 'path';
6
+ import { EventEmitter } from 'events';
6
7
 
7
- // src/plugins/agent/opencode.ts
8
- var debug = Debug("speexor:agent:opencode");
8
+ // src/plugins/agent/retry.ts
9
+ var debug = Debug("speexor:agent:retry");
10
+ var DEFAULT_RETRY = {
11
+ maxRetries: 4,
12
+ baseDelayMs: 1e3,
13
+ maxDelayMs: 8e3
14
+ };
15
+ async function withRetry(fn, label, options = DEFAULT_RETRY) {
16
+ let lastError;
17
+ for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
18
+ try {
19
+ const value = await fn();
20
+ if (attempt > 0) {
21
+ debug(`${label}: succeeded on attempt ${attempt + 1}/${options.maxRetries + 1}`);
22
+ }
23
+ return { ok: true, value };
24
+ } catch (err) {
25
+ lastError = err instanceof Error ? err : new Error(String(err));
26
+ if (attempt < options.maxRetries) {
27
+ const delay = Math.min(options.baseDelayMs * Math.pow(2, attempt), options.maxDelayMs);
28
+ debug(`${label}: attempt ${attempt + 1}/${options.maxRetries + 1} failed \u2014 retrying in ${delay}ms: ${lastError.message}`);
29
+ await new Promise((resolve) => setTimeout(resolve, delay));
30
+ }
31
+ }
32
+ }
33
+ debug(`${label}: all ${options.maxRetries + 1} attempts failed: ${lastError.message}`);
34
+ return { ok: false, error: lastError };
35
+ }
36
+ var debug2 = Debug("speexor:agent:opencode");
9
37
  var OpenCodeAgent = class {
10
38
  name = "opencode-agent";
11
39
  version = "0.1.0";
@@ -14,30 +42,36 @@ var OpenCodeAgent = class {
14
42
  context;
15
43
  async initialize(context) {
16
44
  this.context = context;
17
- debug("OpenCode agent initialized");
45
+ debug2("OpenCode agent initialized");
18
46
  }
19
47
  async destroy() {
20
48
  this.sessions.clear();
21
- debug("OpenCode agent destroyed");
49
+ debug2("OpenCode agent destroyed");
22
50
  }
23
51
  async spawn(task, runtime) {
24
- const session = {
25
- id: `oc-${task.id}-${Date.now()}`,
26
- taskId: task.id,
27
- provider: "opencode",
28
- status: "running",
29
- startedAt: /* @__PURE__ */ new Date(),
30
- runtimeSessionId: runtime.id
31
- };
32
- this.sessions.set(session.id, { task, runtime });
33
- await this.context.eventBus.emit("agent:spawned", { sessionId: session.id, task: task.id });
34
- debug(`OpenCode agent spawned: ${session.id}`);
35
- return session;
52
+ const result = await withRetry(async () => {
53
+ const session = {
54
+ id: `oc-${task.id}-${Date.now()}`,
55
+ taskId: task.id,
56
+ provider: "opencode",
57
+ status: "running",
58
+ startedAt: /* @__PURE__ */ new Date(),
59
+ runtimeSessionId: runtime.id
60
+ };
61
+ this.sessions.set(session.id, { task, runtime });
62
+ await this.context.eventBus.emit("agent:spawned", { sessionId: session.id, task: task.id });
63
+ return session;
64
+ }, "opencode:spawn");
65
+ if (!result.ok) {
66
+ throw result.error;
67
+ }
68
+ debug2(`OpenCode agent spawned: ${result.value.id}`);
69
+ return result.value;
36
70
  }
37
71
  async sendInput(sessionId, input) {
38
72
  const session = this.sessions.get(sessionId);
39
73
  if (!session) throw new Error(`Session ${sessionId} not found`);
40
- debug(`Input sent to session ${sessionId}: ${input.substring(0, 100)}...`);
74
+ debug2(`Input sent to session ${sessionId}: ${input.substring(0, 100)}...`);
41
75
  }
42
76
  async getStatus(sessionId) {
43
77
  const session = this.sessions.get(sessionId);
@@ -46,10 +80,10 @@ var OpenCodeAgent = class {
46
80
  }
47
81
  async kill(sessionId) {
48
82
  this.sessions.delete(sessionId);
49
- debug(`OpenCode agent killed: ${sessionId}`);
83
+ debug2(`OpenCode agent killed: ${sessionId}`);
50
84
  }
51
85
  };
52
- var debug2 = Debug("speexor:agent:claude-code");
86
+ var debug3 = Debug("speexor:agent:claude-code");
53
87
  var ClaudeCodeAgent = class {
54
88
  name = "claude-code-agent";
55
89
  version = "0.1.0";
@@ -58,28 +92,35 @@ var ClaudeCodeAgent = class {
58
92
  context;
59
93
  async initialize(context) {
60
94
  this.context = context;
61
- debug2("Claude Code agent initialized");
95
+ debug3("Claude Code agent initialized");
62
96
  }
63
97
  async destroy() {
64
98
  this.sessions.clear();
65
99
  }
66
100
  async spawn(task, runtime) {
67
- const session = {
68
- id: `cc-${task.id}-${Date.now()}`,
69
- taskId: task.id,
70
- provider: "claude-code",
71
- status: "running",
72
- startedAt: /* @__PURE__ */ new Date(),
73
- runtimeSessionId: runtime.id
74
- };
75
- this.sessions.set(session.id, { task, runtime });
76
- await this.context.eventBus.emit("agent:spawned", { sessionId: session.id, task: task.id });
77
- debug2(`Claude Code agent spawned: ${session.id}`);
78
- return session;
101
+ const result = await withRetry(async () => {
102
+ const session = {
103
+ id: `cc-${task.id}-${Date.now()}`,
104
+ taskId: task.id,
105
+ provider: "claude-code",
106
+ status: "running",
107
+ startedAt: /* @__PURE__ */ new Date(),
108
+ runtimeSessionId: runtime.id
109
+ };
110
+ this.sessions.set(session.id, { task, runtime });
111
+ await this.context.eventBus.emit("agent:spawned", { sessionId: session.id, task: task.id });
112
+ return session;
113
+ }, "claude-code:spawn");
114
+ if (!result.ok) {
115
+ throw result.error;
116
+ }
117
+ debug3(`Claude Code agent spawned: ${result.value.id}`);
118
+ return result.value;
79
119
  }
80
120
  async sendInput(sessionId, input) {
81
121
  const session = this.sessions.get(sessionId);
82
122
  if (!session) throw new Error(`Session ${sessionId} not found`);
123
+ debug3(`sendInput to ${sessionId}: ${input.slice(0, 100)}`);
83
124
  }
84
125
  async getStatus(sessionId) {
85
126
  return this.sessions.has(sessionId) ? "running" : "error";
@@ -88,34 +129,39 @@ var ClaudeCodeAgent = class {
88
129
  this.sessions.delete(sessionId);
89
130
  }
90
131
  };
91
- var debug3 = Debug("speexor:agent:aider");
132
+ var debug4 = Debug("speexor:agent:aider");
92
133
  var AiderAgent = class {
93
134
  name = "aider-agent";
94
135
  version = "0.1.0";
95
136
  type = "agent";
96
137
  sessions = /* @__PURE__ */ new Map();
97
- context;
98
- async initialize(context) {
99
- this.context = context;
100
- debug3("Aider agent initialized");
138
+ async initialize(_context) {
139
+ debug4("Aider agent initialized");
101
140
  }
102
141
  async destroy() {
103
142
  this.sessions.clear();
104
143
  }
105
144
  async spawn(task, runtime) {
106
- const session = {
107
- id: `ai-${task.id}-${Date.now()}`,
108
- taskId: task.id,
109
- provider: "aider",
110
- status: "running",
111
- startedAt: /* @__PURE__ */ new Date(),
112
- runtimeSessionId: runtime.id
113
- };
114
- this.sessions.set(session.id, { task, runtime });
115
- debug3(`Aider agent spawned: ${session.id}`);
116
- return session;
145
+ const result = await withRetry(async () => {
146
+ const session = {
147
+ id: `ai-${task.id}-${Date.now()}`,
148
+ taskId: task.id,
149
+ provider: "aider",
150
+ status: "running",
151
+ startedAt: /* @__PURE__ */ new Date(),
152
+ runtimeSessionId: runtime.id
153
+ };
154
+ this.sessions.set(session.id, { task, runtime });
155
+ return session;
156
+ }, "aider:spawn");
157
+ if (!result.ok) {
158
+ throw result.error;
159
+ }
160
+ debug4(`Aider agent spawned: ${result.value.id}`);
161
+ return result.value;
117
162
  }
118
163
  async sendInput(sessionId, input) {
164
+ debug4(`sendInput to ${sessionId}: ${input.slice(0, 50)}`);
119
165
  }
120
166
  async getStatus(sessionId) {
121
167
  return this.sessions.has(sessionId) ? "running" : "error";
@@ -124,34 +170,39 @@ var AiderAgent = class {
124
170
  this.sessions.delete(sessionId);
125
171
  }
126
172
  };
127
- var debug4 = Debug("speexor:agent:codex");
173
+ var debug5 = Debug("speexor:agent:codex");
128
174
  var CodexAgent = class {
129
175
  name = "codex-agent";
130
176
  version = "0.1.0";
131
177
  type = "agent";
132
178
  sessions = /* @__PURE__ */ new Map();
133
- context;
134
- async initialize(context) {
135
- this.context = context;
136
- debug4("Codex agent initialized");
179
+ async initialize(_context) {
180
+ debug5("Codex agent initialized");
137
181
  }
138
182
  async destroy() {
139
183
  this.sessions.clear();
140
184
  }
141
185
  async spawn(task, runtime) {
142
- const session = {
143
- id: `cx-${task.id}-${Date.now()}`,
144
- taskId: task.id,
145
- provider: "codex",
146
- status: "running",
147
- startedAt: /* @__PURE__ */ new Date(),
148
- runtimeSessionId: runtime.id
149
- };
150
- this.sessions.set(session.id, { task, runtime });
151
- debug4(`Codex agent spawned: ${session.id}`);
152
- return session;
186
+ const result = await withRetry(async () => {
187
+ const session = {
188
+ id: `cx-${task.id}-${Date.now()}`,
189
+ taskId: task.id,
190
+ provider: "codex",
191
+ status: "running",
192
+ startedAt: /* @__PURE__ */ new Date(),
193
+ runtimeSessionId: runtime.id
194
+ };
195
+ this.sessions.set(session.id, { task, runtime });
196
+ return session;
197
+ }, "codex:spawn");
198
+ if (!result.ok) {
199
+ throw result.error;
200
+ }
201
+ debug5(`Codex agent spawned: ${result.value.id}`);
202
+ return result.value;
153
203
  }
154
204
  async sendInput(sessionId, input) {
205
+ debug5(`sendInput to ${sessionId}: ${input.slice(0, 100)}`);
155
206
  }
156
207
  async getStatus(sessionId) {
157
208
  return this.sessions.has(sessionId) ? "running" : "error";
@@ -160,18 +211,16 @@ var CodexAgent = class {
160
211
  this.sessions.delete(sessionId);
161
212
  }
162
213
  };
163
- var debug5 = Debug("speexor:runtime:tmux");
214
+ var debug6 = Debug("speexor:runtime:tmux");
164
215
  var TmuxRuntime = class {
165
216
  name = "tmux-runtime";
166
217
  version = "0.1.0";
167
218
  type = "runtime";
168
219
  sessions = /* @__PURE__ */ new Map();
169
- context;
170
- async initialize(context) {
171
- this.context = context;
220
+ async initialize(_context) {
172
221
  try {
173
222
  execSync("tmux -V", { stdio: "ignore" });
174
- debug5("tmux available");
223
+ debug6("tmux available");
175
224
  } catch {
176
225
  console.warn("tmux not found \u2014 will fall back to process runtime");
177
226
  throw new Error("tmux not available on this system");
@@ -197,19 +246,21 @@ var TmuxRuntime = class {
197
246
  worktreePath,
198
247
  createdAt: /* @__PURE__ */ new Date()
199
248
  };
200
- this.sessions.set(id, session);
201
- debug5(`tmux session created: ${id} at ${worktreePath}`);
249
+ const entry = { session, lastOutput: "", destroyed: false };
250
+ this.sessions.set(id, entry);
251
+ debug6(`tmux session created: ${id} at ${worktreePath}`);
202
252
  return session;
203
253
  }
204
254
  async destroySession(sessionId) {
205
- const session = this.sessions.get(sessionId);
206
- if (!session) return;
255
+ const entry = this.sessions.get(sessionId);
256
+ if (!entry) return;
257
+ entry.destroyed = true;
207
258
  try {
208
259
  execSync(`tmux kill-session -t speexor-${sessionId}`, { stdio: "ignore" });
209
260
  } catch {
210
261
  }
211
262
  this.sessions.delete(sessionId);
212
- debug5(`tmux session destroyed: ${sessionId}`);
263
+ debug6(`tmux session destroyed: ${sessionId}`);
213
264
  }
214
265
  async sendInput(sessionId, input) {
215
266
  const escaped = input.replace(/'/g, "'\\''");
@@ -222,8 +273,35 @@ var TmuxRuntime = class {
222
273
  return "";
223
274
  }
224
275
  }
225
- getLiveStream(sessionId) {
226
- throw new Error("Live stream not implemented for tmux");
276
+ async *getLiveStream(sessionId) {
277
+ const entry = this.sessions.get(sessionId);
278
+ if (!entry || entry.destroyed) {
279
+ throw new Error(`Session ${sessionId} not found or destroyed`);
280
+ }
281
+ const POLL_INTERVAL_MS = 500;
282
+ let lastOutput = entry.lastOutput;
283
+ try {
284
+ while (!entry.destroyed) {
285
+ try {
286
+ const currentOutput = execSync(`tmux capture-pane -t speexor-${sessionId} -p -S -`, {
287
+ encoding: "utf-8",
288
+ stdio: "pipe"
289
+ });
290
+ if (currentOutput && currentOutput !== lastOutput) {
291
+ const newContent = lastOutput ? currentOutput.slice(lastOutput.length) : currentOutput;
292
+ lastOutput = currentOutput;
293
+ entry.lastOutput = currentOutput;
294
+ if (newContent) {
295
+ yield newContent;
296
+ }
297
+ }
298
+ } catch {
299
+ break;
300
+ }
301
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
302
+ }
303
+ } finally {
304
+ }
227
305
  }
228
306
  async getStatus(sessionId) {
229
307
  try {
@@ -234,16 +312,14 @@ var TmuxRuntime = class {
234
312
  }
235
313
  }
236
314
  };
237
- var debug6 = Debug("speexor:runtime:process");
315
+ var debug7 = Debug("speexor:runtime:process");
238
316
  var ProcessRuntime = class {
239
317
  name = "process-runtime";
240
318
  version = "0.1.0";
241
319
  type = "runtime";
242
320
  sessions = /* @__PURE__ */ new Map();
243
- context;
244
- async initialize(context) {
245
- this.context = context;
246
- debug6("Process runtime initialized");
321
+ async initialize(_context) {
322
+ debug7("Process runtime initialized");
247
323
  }
248
324
  async destroy() {
249
325
  for (const [id] of this.sessions) {
@@ -258,21 +334,28 @@ var ProcessRuntime = class {
258
334
  mkdirSync(logsDir, { recursive: true });
259
335
  }
260
336
  const logStream = createWriteStream(join(logsDir, `${id}.log`), { flags: "a" });
261
- const child = spawn(process.env.SHELL || "bash", [], {
337
+ const emitter = new EventEmitter();
338
+ const child = spawn(process.env.SHELL || process.env.COMSPEC || "bash", [], {
262
339
  cwd: worktreePath,
263
340
  stdio: ["pipe", "pipe", "pipe"],
264
341
  env: { ...process.env, TERM: "xterm-256color" }
265
342
  });
266
343
  child.stdout?.on("data", (data) => {
267
- logStream.write(`[stdout] ${data.toString()}`);
344
+ const text = data.toString();
345
+ logStream.write(`[stdout] ${text}`);
346
+ emitter.emit("data", text);
268
347
  });
269
348
  child.stderr?.on("data", (data) => {
270
- logStream.write(`[stderr] ${data.toString()}`);
349
+ const text = data.toString();
350
+ logStream.write(`[stderr] ${text}`);
351
+ emitter.emit("data", text);
271
352
  });
272
353
  child.on("exit", (code) => {
273
354
  logStream.write(`[exit] Process exited with code ${code}
274
355
  `);
275
356
  logStream.end();
357
+ emitter.emit("exit", code);
358
+ emitter.removeAllListeners();
276
359
  });
277
360
  const session = {
278
361
  id,
@@ -281,13 +364,16 @@ var ProcessRuntime = class {
281
364
  pid: child.pid,
282
365
  createdAt: /* @__PURE__ */ new Date()
283
366
  };
284
- this.sessions.set(id, { session, process: child });
285
- debug6(`Process session created: ${id} (PID: ${child.pid})`);
367
+ const entry = { session, process: child, emitter, logStream, destroyed: false };
368
+ this.sessions.set(id, entry);
369
+ debug7(`Process session created: ${id} (PID: ${child.pid})`);
286
370
  return session;
287
371
  }
288
372
  async destroySession(sessionId) {
289
373
  const entry = this.sessions.get(sessionId);
290
374
  if (!entry) return;
375
+ entry.destroyed = true;
376
+ entry.emitter.removeAllListeners();
291
377
  entry.process.kill("SIGTERM");
292
378
  setTimeout(() => {
293
379
  try {
@@ -296,7 +382,7 @@ var ProcessRuntime = class {
296
382
  }
297
383
  }, 5e3);
298
384
  this.sessions.delete(sessionId);
299
- debug6(`Process session destroyed: ${sessionId}`);
385
+ debug7(`Process session destroyed: ${sessionId}`);
300
386
  }
301
387
  async sendInput(sessionId, input) {
302
388
  const entry = this.sessions.get(sessionId);
@@ -309,8 +395,50 @@ var ProcessRuntime = class {
309
395
  if (!entry) return "";
310
396
  return `Session ${sessionId} (PID: ${entry.process.pid})`;
311
397
  }
312
- getLiveStream(sessionId) {
313
- throw new Error("Live stream not implemented for process runtime");
398
+ async *getLiveStream(sessionId) {
399
+ const entry = this.sessions.get(sessionId);
400
+ if (!entry || entry.destroyed) {
401
+ throw new Error(`Session ${sessionId} not found or destroyed`);
402
+ }
403
+ const { emitter } = entry;
404
+ let buffer = [];
405
+ let resolve = null;
406
+ let done = false;
407
+ const onData = (data) => {
408
+ if (resolve) {
409
+ const r = resolve;
410
+ resolve = null;
411
+ r({ value: data, done: false });
412
+ } else {
413
+ buffer.push(data);
414
+ }
415
+ };
416
+ const onExit = () => {
417
+ done = true;
418
+ if (resolve) {
419
+ const r = resolve;
420
+ resolve = null;
421
+ r({ value: "", done: true });
422
+ }
423
+ };
424
+ emitter.on("data", onData);
425
+ emitter.on("exit", onExit);
426
+ try {
427
+ while (!done) {
428
+ if (buffer.length > 0) {
429
+ yield buffer.shift();
430
+ } else {
431
+ const result = await new Promise((res) => {
432
+ resolve = res;
433
+ });
434
+ if (result.done) break;
435
+ yield result.value;
436
+ }
437
+ }
438
+ } finally {
439
+ emitter.off("data", onData);
440
+ emitter.off("exit", onExit);
441
+ }
314
442
  }
315
443
  async getStatus(sessionId) {
316
444
  const entry = this.sessions.get(sessionId);
@@ -319,16 +447,14 @@ var ProcessRuntime = class {
319
447
  return exited ? "stopped" : "running";
320
448
  }
321
449
  };
322
- var debug7 = Debug("speexor:workspace:git-worktree");
450
+ var debug8 = Debug("speexor:workspace:git-worktree");
323
451
  var GitWorktreeWorkspace = class {
324
452
  name = "git-worktree-workspace";
325
453
  version = "0.1.0";
326
454
  type = "workspace";
327
455
  sessions = /* @__PURE__ */ new Map();
328
- context;
329
456
  worktreesDir = ".speexor/worktrees";
330
- async initialize(context) {
331
- this.context = context;
457
+ async initialize(_context) {
332
458
  const dir = join(process.cwd(), this.worktreesDir);
333
459
  if (!existsSync(dir)) {
334
460
  mkdirSync(dir, { recursive: true });
@@ -338,12 +464,12 @@ var GitWorktreeWorkspace = class {
338
464
  } catch {
339
465
  throw new Error("Not a git repository. Run `speexor start <repo>` first.");
340
466
  }
341
- debug7("Git worktree workspace initialized");
467
+ debug8("Git worktree workspace initialized");
342
468
  }
343
469
  async destroy() {
344
470
  const stale = await this.cleanupStale();
345
471
  if (stale.length > 0) {
346
- debug7(`Cleaned up ${stale.length} stale worktree(s)`);
472
+ debug8(`Cleaned up ${stale.length} stale worktree(s)`);
347
473
  }
348
474
  }
349
475
  async createWorktree(task) {
@@ -368,7 +494,7 @@ var GitWorktreeWorkspace = class {
368
494
  createdAt: /* @__PURE__ */ new Date()
369
495
  };
370
496
  this.sessions.set(session.id, session);
371
- debug7(`Worktree created: ${branch} at ${worktreePath}`);
497
+ debug8(`Worktree created: ${branch} at ${worktreePath}`);
372
498
  return session;
373
499
  }
374
500
  async removeWorktree(sessionId) {
@@ -383,7 +509,7 @@ var GitWorktreeWorkspace = class {
383
509
  }
384
510
  }
385
511
  this.sessions.delete(sessionId);
386
- debug7(`Worktree removed: ${sessionId}`);
512
+ debug8(`Worktree removed: ${sessionId}`);
387
513
  }
388
514
  getWorktreePath(sessionId) {
389
515
  const session = this.sessions.get(sessionId);
@@ -418,18 +544,16 @@ var GitWorktreeWorkspace = class {
418
544
  return cleaned;
419
545
  }
420
546
  };
421
- var debug8 = Debug("speexor:tracker:github");
547
+ var debug9 = Debug("speexor:tracker:github");
422
548
  var GitHubTracker = class {
423
549
  name = "github-tracker";
424
550
  version = "0.1.0";
425
551
  type = "tracker";
426
- context;
427
552
  handlers = [];
428
- async initialize(context) {
429
- this.context = context;
553
+ async initialize(_context) {
430
554
  try {
431
555
  execSync("gh --version", { stdio: "ignore" });
432
- debug8("GitHub CLI available");
556
+ debug9("GitHub CLI available");
433
557
  } catch {
434
558
  throw new Error("GitHub CLI (gh) not found. Install from https://cli.github.com/");
435
559
  }
@@ -458,7 +582,7 @@ var GitHubTracker = class {
458
582
  }));
459
583
  return issues;
460
584
  } catch (error) {
461
- debug8("Failed to fetch issues:", error);
585
+ debug9("Failed to fetch issues:", error);
462
586
  return [];
463
587
  }
464
588
  }
@@ -483,24 +607,17 @@ var GitHubTracker = class {
483
607
  onEvent(handler) {
484
608
  this.handlers.push(handler);
485
609
  }
486
- emit(event) {
487
- for (const handler of this.handlers) {
488
- handler(event);
489
- }
490
- }
491
610
  };
492
- var debug9 = Debug("speexor:scm:github");
611
+ var debug10 = Debug("speexor:scm:github");
493
612
  var GitHubSCM = class {
494
613
  name = "github-scm";
495
614
  version = "0.1.0";
496
615
  type = "scm";
497
- context;
498
616
  handlers = [];
499
- async initialize(context) {
500
- this.context = context;
617
+ async initialize(_context) {
501
618
  try {
502
619
  execSync("gh --version", { stdio: "ignore" });
503
- debug9("GitHub CLI available");
620
+ debug10("GitHub CLI available");
504
621
  } catch {
505
622
  throw new Error("GitHub CLI (gh) not found");
506
623
  }
@@ -603,13 +720,13 @@ var GitHubSCM = class {
603
720
  return "none";
604
721
  }
605
722
  };
606
- var debug10 = Debug("speexor:notifier:desktop");
723
+ var debug11 = Debug("speexor:notifier:desktop");
607
724
  var DesktopNotifier = class {
608
725
  name = "desktop-notifier";
609
726
  version = "0.1.0";
610
727
  type = "notifier";
611
728
  async initialize(_context) {
612
- debug10("Desktop notifier initialized");
729
+ debug11("Desktop notifier initialized");
613
730
  }
614
731
  async destroy() {
615
732
  }
@@ -623,15 +740,97 @@ var DesktopNotifier = class {
623
740
  { stdio: "ignore" }
624
741
  );
625
742
  } else if (platform === "win32") {
626
- const icon = level === "error" ? "Warning" : "Information";
627
743
  execSync(`powershell -c "New-BurntToastNotification -Text '${title}', '${message}'"`, { stdio: "ignore" });
628
744
  } else if (platform === "linux") {
629
745
  execSync(`notify-send "${title}" "${message}"`, { stdio: "ignore" });
630
746
  }
631
- debug10(`Desktop notification: ${title} - ${message}`);
747
+ debug11(`Desktop notification: ${title} - ${message}`);
632
748
  } catch (error) {
633
- debug10(`Failed to send notification: ${error}`);
749
+ debug11(`Failed to send notification: ${error}`);
750
+ }
751
+ }
752
+ };
753
+ var debug12 = Debug("speexor:terminal");
754
+ var TerminalPluginImpl = class {
755
+ name = "terminal";
756
+ version = "0.1.0";
757
+ type = "terminal";
758
+ sessions = /* @__PURE__ */ new Map();
759
+ context;
760
+ async initialize(context) {
761
+ this.context = context;
762
+ debug12("Terminal plugin initialized");
763
+ }
764
+ async destroy() {
765
+ for (const [sessionId] of this.sessions) {
766
+ await this.detach(sessionId).catch(() => {
767
+ });
634
768
  }
769
+ this.sessions.clear();
770
+ debug12("Terminal plugin destroyed");
771
+ }
772
+ async attach(sessionId) {
773
+ if (this.sessions.has(sessionId)) return;
774
+ const child = spawn(process.env.SHELL || process.env.COMSPEC || "cmd.exe", [], {
775
+ stdio: ["pipe", "pipe", "pipe"],
776
+ env: { ...process.env, TERM: "xterm-256color" }
777
+ });
778
+ const session = {
779
+ sessionId,
780
+ process: child,
781
+ handlers: /* @__PURE__ */ new Set(),
782
+ buffer: ""
783
+ };
784
+ child.stdout?.on("data", (data) => {
785
+ const text = data.toString();
786
+ session.buffer += text;
787
+ for (const handler of session.handlers) {
788
+ handler(text);
789
+ }
790
+ });
791
+ child.stderr?.on("data", (data) => {
792
+ const text = data.toString();
793
+ session.buffer += text;
794
+ for (const handler of session.handlers) {
795
+ handler(text);
796
+ }
797
+ });
798
+ child.on("exit", (code) => {
799
+ debug12(`Terminal session ${sessionId} exited with code ${code}`);
800
+ this.sessions.delete(sessionId);
801
+ this.context.eventBus.emit("terminal:detached", {
802
+ sessionId,
803
+ exitCode: code
804
+ });
805
+ });
806
+ this.sessions.set(sessionId, session);
807
+ this.context.eventBus.emit("terminal:attached", { sessionId });
808
+ debug12(`Terminal session attached: ${sessionId}`);
809
+ }
810
+ async detach(sessionId) {
811
+ const session = this.sessions.get(sessionId);
812
+ if (!session) return;
813
+ session.process.kill("SIGTERM");
814
+ setTimeout(() => {
815
+ try {
816
+ session.process.kill("SIGKILL");
817
+ } catch {
818
+ }
819
+ }, 5e3);
820
+ session.handlers.clear();
821
+ this.sessions.delete(sessionId);
822
+ this.context.eventBus.emit("terminal:detached", { sessionId });
823
+ debug12(`Terminal session detached: ${sessionId}`);
824
+ }
825
+ async write(sessionId, data) {
826
+ const session = this.sessions.get(sessionId);
827
+ if (!session) throw new Error(`Terminal session ${sessionId} not found`);
828
+ session.process.stdin?.write(data);
829
+ }
830
+ onData(sessionId, handler) {
831
+ const session = this.sessions.get(sessionId);
832
+ if (!session) throw new Error(`Terminal session ${sessionId} not found`);
833
+ session.handlers.add(handler);
635
834
  }
636
835
  };
637
836
 
@@ -653,7 +852,9 @@ function loadAllPlugins() {
653
852
  // SCM
654
853
  new GitHubSCM(),
655
854
  // Notifier
656
- new DesktopNotifier()
855
+ new DesktopNotifier(),
856
+ // Terminal
857
+ new TerminalPluginImpl()
657
858
  ];
658
859
  return plugins;
659
860
  }
@@ -662,5 +863,5 @@ function loadPluginByType(type) {
662
863
  }
663
864
 
664
865
  export { loadAllPlugins, loadPluginByType };
665
- //# sourceMappingURL=chunk-B7WLHC4W.js.map
666
- //# sourceMappingURL=chunk-B7WLHC4W.js.map
866
+ //# sourceMappingURL=chunk-5OD5UWB5.js.map
867
+ //# sourceMappingURL=chunk-5OD5UWB5.js.map