speexor 0.1.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.
Files changed (48) hide show
  1. package/API-REFERENCE.md +201 -0
  2. package/ARCHITECTURE.md +548 -0
  3. package/CHANGELOG.md +52 -0
  4. package/CODE-OF-CONDUCT.md +83 -0
  5. package/CONTRIBUTING.md +98 -0
  6. package/FAQ.md +105 -0
  7. package/LICENSE.md +21 -0
  8. package/PUBLISH.md +77 -0
  9. package/README.md +179 -0
  10. package/REFACTOR-LOG.md +40 -0
  11. package/ROADMAP.md +78 -0
  12. package/SECURITY.md +79 -0
  13. package/SUMMARY.md +46 -0
  14. package/TESTING.md +140 -0
  15. package/dist/agent-5D3BVWNK.js +37 -0
  16. package/dist/agent-5D3BVWNK.js.map +1 -0
  17. package/dist/chunk-2F66BZYJ.js +212 -0
  18. package/dist/chunk-2F66BZYJ.js.map +1 -0
  19. package/dist/chunk-5NA2TFPG.js +3 -0
  20. package/dist/chunk-5NA2TFPG.js.map +1 -0
  21. package/dist/chunk-B7WLHC4W.js +666 -0
  22. package/dist/chunk-B7WLHC4W.js.map +1 -0
  23. package/dist/chunk-SXALZEOJ.js +345 -0
  24. package/dist/chunk-SXALZEOJ.js.map +1 -0
  25. package/dist/cli/index.d.ts +1 -0
  26. package/dist/cli/index.js +287 -0
  27. package/dist/cli/index.js.map +1 -0
  28. package/dist/core/index.d.ts +31 -0
  29. package/dist/core/index.js +4 -0
  30. package/dist/core/index.js.map +1 -0
  31. package/dist/index.d.ts +75 -0
  32. package/dist/index.js +205 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/plugins/index.d.ts +6 -0
  35. package/dist/plugins/index.js +3 -0
  36. package/dist/plugins/index.js.map +1 -0
  37. package/dist/types-0q_okI2g.d.ts +205 -0
  38. package/docs/PRD01.md +264 -0
  39. package/docs/PRD02.md +299 -0
  40. package/docs/PRD03.md +0 -0
  41. package/docs/PRD04.md +349 -0
  42. package/docs/PRD05.md +312 -0
  43. package/docs/SETUP.md +94 -0
  44. package/docs/TROUBLESHOOTING.md +113 -0
  45. package/examples/basic.yaml +61 -0
  46. package/package.json +102 -0
  47. package/schema/config.schema.json +119 -0
  48. package/speexor.config.yaml.example +30 -0
@@ -0,0 +1,666 @@
1
+ import Debug from 'debug';
2
+ import { execSync, spawn } from 'child_process';
3
+ import { randomUUID } from 'crypto';
4
+ import { existsSync, mkdirSync, createWriteStream } from 'fs';
5
+ import { join } from 'path';
6
+
7
+ // src/plugins/agent/opencode.ts
8
+ var debug = Debug("speexor:agent:opencode");
9
+ var OpenCodeAgent = class {
10
+ name = "opencode-agent";
11
+ version = "0.1.0";
12
+ type = "agent";
13
+ sessions = /* @__PURE__ */ new Map();
14
+ context;
15
+ async initialize(context) {
16
+ this.context = context;
17
+ debug("OpenCode agent initialized");
18
+ }
19
+ async destroy() {
20
+ this.sessions.clear();
21
+ debug("OpenCode agent destroyed");
22
+ }
23
+ 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;
36
+ }
37
+ async sendInput(sessionId, input) {
38
+ const session = this.sessions.get(sessionId);
39
+ if (!session) throw new Error(`Session ${sessionId} not found`);
40
+ debug(`Input sent to session ${sessionId}: ${input.substring(0, 100)}...`);
41
+ }
42
+ async getStatus(sessionId) {
43
+ const session = this.sessions.get(sessionId);
44
+ if (!session) return "error";
45
+ return "running";
46
+ }
47
+ async kill(sessionId) {
48
+ this.sessions.delete(sessionId);
49
+ debug(`OpenCode agent killed: ${sessionId}`);
50
+ }
51
+ };
52
+ var debug2 = Debug("speexor:agent:claude-code");
53
+ var ClaudeCodeAgent = class {
54
+ name = "claude-code-agent";
55
+ version = "0.1.0";
56
+ type = "agent";
57
+ sessions = /* @__PURE__ */ new Map();
58
+ context;
59
+ async initialize(context) {
60
+ this.context = context;
61
+ debug2("Claude Code agent initialized");
62
+ }
63
+ async destroy() {
64
+ this.sessions.clear();
65
+ }
66
+ 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;
79
+ }
80
+ async sendInput(sessionId, input) {
81
+ const session = this.sessions.get(sessionId);
82
+ if (!session) throw new Error(`Session ${sessionId} not found`);
83
+ }
84
+ async getStatus(sessionId) {
85
+ return this.sessions.has(sessionId) ? "running" : "error";
86
+ }
87
+ async kill(sessionId) {
88
+ this.sessions.delete(sessionId);
89
+ }
90
+ };
91
+ var debug3 = Debug("speexor:agent:aider");
92
+ var AiderAgent = class {
93
+ name = "aider-agent";
94
+ version = "0.1.0";
95
+ type = "agent";
96
+ sessions = /* @__PURE__ */ new Map();
97
+ context;
98
+ async initialize(context) {
99
+ this.context = context;
100
+ debug3("Aider agent initialized");
101
+ }
102
+ async destroy() {
103
+ this.sessions.clear();
104
+ }
105
+ 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;
117
+ }
118
+ async sendInput(sessionId, input) {
119
+ }
120
+ async getStatus(sessionId) {
121
+ return this.sessions.has(sessionId) ? "running" : "error";
122
+ }
123
+ async kill(sessionId) {
124
+ this.sessions.delete(sessionId);
125
+ }
126
+ };
127
+ var debug4 = Debug("speexor:agent:codex");
128
+ var CodexAgent = class {
129
+ name = "codex-agent";
130
+ version = "0.1.0";
131
+ type = "agent";
132
+ sessions = /* @__PURE__ */ new Map();
133
+ context;
134
+ async initialize(context) {
135
+ this.context = context;
136
+ debug4("Codex agent initialized");
137
+ }
138
+ async destroy() {
139
+ this.sessions.clear();
140
+ }
141
+ 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;
153
+ }
154
+ async sendInput(sessionId, input) {
155
+ }
156
+ async getStatus(sessionId) {
157
+ return this.sessions.has(sessionId) ? "running" : "error";
158
+ }
159
+ async kill(sessionId) {
160
+ this.sessions.delete(sessionId);
161
+ }
162
+ };
163
+ var debug5 = Debug("speexor:runtime:tmux");
164
+ var TmuxRuntime = class {
165
+ name = "tmux-runtime";
166
+ version = "0.1.0";
167
+ type = "runtime";
168
+ sessions = /* @__PURE__ */ new Map();
169
+ context;
170
+ async initialize(context) {
171
+ this.context = context;
172
+ try {
173
+ execSync("tmux -V", { stdio: "ignore" });
174
+ debug5("tmux available");
175
+ } catch {
176
+ console.warn("tmux not found \u2014 will fall back to process runtime");
177
+ throw new Error("tmux not available on this system");
178
+ }
179
+ }
180
+ async destroy() {
181
+ for (const [id] of this.sessions) {
182
+ await this.destroySession(id).catch(() => {
183
+ });
184
+ }
185
+ }
186
+ async createSession(worktreePath) {
187
+ const id = `tmux-${randomUUID().slice(0, 8)}`;
188
+ const sessionName = `speexor-${id}`;
189
+ try {
190
+ execSync(`tmux new-session -d -s ${sessionName} -c ${worktreePath}`, { stdio: "pipe" });
191
+ } catch (error) {
192
+ throw new Error(`Failed to create tmux session: ${error}`);
193
+ }
194
+ const session = {
195
+ id,
196
+ type: "tmux",
197
+ worktreePath,
198
+ createdAt: /* @__PURE__ */ new Date()
199
+ };
200
+ this.sessions.set(id, session);
201
+ debug5(`tmux session created: ${id} at ${worktreePath}`);
202
+ return session;
203
+ }
204
+ async destroySession(sessionId) {
205
+ const session = this.sessions.get(sessionId);
206
+ if (!session) return;
207
+ try {
208
+ execSync(`tmux kill-session -t speexor-${sessionId}`, { stdio: "ignore" });
209
+ } catch {
210
+ }
211
+ this.sessions.delete(sessionId);
212
+ debug5(`tmux session destroyed: ${sessionId}`);
213
+ }
214
+ async sendInput(sessionId, input) {
215
+ const escaped = input.replace(/'/g, "'\\''");
216
+ execSync(`tmux send-keys -t speexor-${sessionId} '${escaped}' Enter`, { stdio: "ignore" });
217
+ }
218
+ async getOutput(sessionId) {
219
+ try {
220
+ return execSync(`tmux capture-pane -t speexor-${sessionId} -p`, { encoding: "utf-8" });
221
+ } catch {
222
+ return "";
223
+ }
224
+ }
225
+ getLiveStream(sessionId) {
226
+ throw new Error("Live stream not implemented for tmux");
227
+ }
228
+ async getStatus(sessionId) {
229
+ try {
230
+ execSync(`tmux has-session -t speexor-${sessionId}`, { stdio: "ignore" });
231
+ return "running";
232
+ } catch {
233
+ return "stopped";
234
+ }
235
+ }
236
+ };
237
+ var debug6 = Debug("speexor:runtime:process");
238
+ var ProcessRuntime = class {
239
+ name = "process-runtime";
240
+ version = "0.1.0";
241
+ type = "runtime";
242
+ sessions = /* @__PURE__ */ new Map();
243
+ context;
244
+ async initialize(context) {
245
+ this.context = context;
246
+ debug6("Process runtime initialized");
247
+ }
248
+ async destroy() {
249
+ for (const [id] of this.sessions) {
250
+ await this.destroySession(id).catch(() => {
251
+ });
252
+ }
253
+ }
254
+ async createSession(worktreePath) {
255
+ const id = `proc-${randomUUID().slice(0, 8)}`;
256
+ const logsDir = join(process.cwd(), ".speexor", "logs");
257
+ if (!existsSync(logsDir)) {
258
+ mkdirSync(logsDir, { recursive: true });
259
+ }
260
+ const logStream = createWriteStream(join(logsDir, `${id}.log`), { flags: "a" });
261
+ const child = spawn(process.env.SHELL || "bash", [], {
262
+ cwd: worktreePath,
263
+ stdio: ["pipe", "pipe", "pipe"],
264
+ env: { ...process.env, TERM: "xterm-256color" }
265
+ });
266
+ child.stdout?.on("data", (data) => {
267
+ logStream.write(`[stdout] ${data.toString()}`);
268
+ });
269
+ child.stderr?.on("data", (data) => {
270
+ logStream.write(`[stderr] ${data.toString()}`);
271
+ });
272
+ child.on("exit", (code) => {
273
+ logStream.write(`[exit] Process exited with code ${code}
274
+ `);
275
+ logStream.end();
276
+ });
277
+ const session = {
278
+ id,
279
+ type: "process",
280
+ worktreePath,
281
+ pid: child.pid,
282
+ createdAt: /* @__PURE__ */ new Date()
283
+ };
284
+ this.sessions.set(id, { session, process: child });
285
+ debug6(`Process session created: ${id} (PID: ${child.pid})`);
286
+ return session;
287
+ }
288
+ async destroySession(sessionId) {
289
+ const entry = this.sessions.get(sessionId);
290
+ if (!entry) return;
291
+ entry.process.kill("SIGTERM");
292
+ setTimeout(() => {
293
+ try {
294
+ entry.process.kill("SIGKILL");
295
+ } catch {
296
+ }
297
+ }, 5e3);
298
+ this.sessions.delete(sessionId);
299
+ debug6(`Process session destroyed: ${sessionId}`);
300
+ }
301
+ async sendInput(sessionId, input) {
302
+ const entry = this.sessions.get(sessionId);
303
+ if (!entry) throw new Error(`Session ${sessionId} not found`);
304
+ entry.process.stdin?.write(`${input}
305
+ `);
306
+ }
307
+ async getOutput(sessionId) {
308
+ const entry = this.sessions.get(sessionId);
309
+ if (!entry) return "";
310
+ return `Session ${sessionId} (PID: ${entry.process.pid})`;
311
+ }
312
+ getLiveStream(sessionId) {
313
+ throw new Error("Live stream not implemented for process runtime");
314
+ }
315
+ async getStatus(sessionId) {
316
+ const entry = this.sessions.get(sessionId);
317
+ if (!entry) return "stopped";
318
+ const exited = entry.process.exitCode !== null;
319
+ return exited ? "stopped" : "running";
320
+ }
321
+ };
322
+ var debug7 = Debug("speexor:workspace:git-worktree");
323
+ var GitWorktreeWorkspace = class {
324
+ name = "git-worktree-workspace";
325
+ version = "0.1.0";
326
+ type = "workspace";
327
+ sessions = /* @__PURE__ */ new Map();
328
+ context;
329
+ worktreesDir = ".speexor/worktrees";
330
+ async initialize(context) {
331
+ this.context = context;
332
+ const dir = join(process.cwd(), this.worktreesDir);
333
+ if (!existsSync(dir)) {
334
+ mkdirSync(dir, { recursive: true });
335
+ }
336
+ try {
337
+ execSync("git rev-parse --git-dir", { stdio: "ignore" });
338
+ } catch {
339
+ throw new Error("Not a git repository. Run `speexor start <repo>` first.");
340
+ }
341
+ debug7("Git worktree workspace initialized");
342
+ }
343
+ async destroy() {
344
+ const stale = await this.cleanupStale();
345
+ if (stale.length > 0) {
346
+ debug7(`Cleaned up ${stale.length} stale worktree(s)`);
347
+ }
348
+ }
349
+ async createWorktree(task) {
350
+ const branch = `speexor/${task.id}`;
351
+ const worktreePath = join(process.cwd(), this.worktreesDir, task.id);
352
+ try {
353
+ execSync(`git show-ref --verify --quiet refs/heads/${branch}`, { stdio: "ignore" });
354
+ } catch {
355
+ execSync(`git branch ${branch}`, { stdio: "pipe" });
356
+ }
357
+ try {
358
+ execSync(`git worktree add ${worktreePath} ${branch}`, { stdio: "pipe" });
359
+ } catch (error) {
360
+ throw new Error(`Failed to create worktree at ${worktreePath}: ${error}`);
361
+ }
362
+ const session = {
363
+ id: `wt-${task.id}-${randomUUID().slice(0, 8)}`,
364
+ taskId: task.id,
365
+ repository: task.repository,
366
+ branch,
367
+ path: worktreePath,
368
+ createdAt: /* @__PURE__ */ new Date()
369
+ };
370
+ this.sessions.set(session.id, session);
371
+ debug7(`Worktree created: ${branch} at ${worktreePath}`);
372
+ return session;
373
+ }
374
+ async removeWorktree(sessionId) {
375
+ const session = this.sessions.get(sessionId);
376
+ if (!session) return;
377
+ try {
378
+ execSync(`git worktree remove ${session.path}`, { stdio: "pipe" });
379
+ } catch {
380
+ try {
381
+ execSync(`git worktree remove --force ${session.path}`, { stdio: "pipe" });
382
+ } catch {
383
+ }
384
+ }
385
+ this.sessions.delete(sessionId);
386
+ debug7(`Worktree removed: ${sessionId}`);
387
+ }
388
+ getWorktreePath(sessionId) {
389
+ const session = this.sessions.get(sessionId);
390
+ if (!session) throw new Error(`Session ${sessionId} not found`);
391
+ return session.path;
392
+ }
393
+ async listActive() {
394
+ return Array.from(this.sessions.values());
395
+ }
396
+ async cleanupStale() {
397
+ const cleaned = [];
398
+ try {
399
+ const output = execSync("git worktree list --porcelain", { encoding: "utf-8" });
400
+ const lines = output.split("\n");
401
+ for (const line of lines) {
402
+ if (line.startsWith("worktree ")) {
403
+ const path = line.slice(9).trim();
404
+ if (path.includes(this.worktreesDir)) {
405
+ const isActive = Array.from(this.sessions.values()).some((s) => s.path === path);
406
+ if (!isActive && existsSync(path)) {
407
+ try {
408
+ execSync(`git worktree remove --force "${path}"`, { stdio: "pipe" });
409
+ cleaned.push(path);
410
+ } catch {
411
+ }
412
+ }
413
+ }
414
+ }
415
+ }
416
+ } catch {
417
+ }
418
+ return cleaned;
419
+ }
420
+ };
421
+ var debug8 = Debug("speexor:tracker:github");
422
+ var GitHubTracker = class {
423
+ name = "github-tracker";
424
+ version = "0.1.0";
425
+ type = "tracker";
426
+ context;
427
+ handlers = [];
428
+ async initialize(context) {
429
+ this.context = context;
430
+ try {
431
+ execSync("gh --version", { stdio: "ignore" });
432
+ debug8("GitHub CLI available");
433
+ } catch {
434
+ throw new Error("GitHub CLI (gh) not found. Install from https://cli.github.com/");
435
+ }
436
+ }
437
+ async destroy() {
438
+ this.handlers = [];
439
+ }
440
+ async fetchIssues(filter) {
441
+ const state = filter?.state ?? "open";
442
+ const labels = filter?.labels?.length ? `--label "${filter.labels.join(",")}"` : "";
443
+ const limit = filter?.limit ?? 30;
444
+ try {
445
+ const output = execSync(
446
+ `gh issue list --state ${state} ${labels} --limit ${limit} --json number,title,body,state,labels,url,createdAt,updatedAt`,
447
+ { encoding: "utf-8" }
448
+ );
449
+ const issues = JSON.parse(output).map((i) => ({
450
+ id: String(i.number),
451
+ title: i.title,
452
+ description: i.body ?? "",
453
+ state: i.state,
454
+ labels: i.labels?.map((l) => l.name) ?? [],
455
+ url: i.url,
456
+ createdAt: new Date(i.createdAt),
457
+ updatedAt: new Date(i.updatedAt)
458
+ }));
459
+ return issues;
460
+ } catch (error) {
461
+ debug8("Failed to fetch issues:", error);
462
+ return [];
463
+ }
464
+ }
465
+ async getIssue(id) {
466
+ try {
467
+ const output = execSync(`gh issue view ${id} --json number,title,body,state,labels,url,createdAt,updatedAt`, { encoding: "utf-8" });
468
+ const i = JSON.parse(output);
469
+ return {
470
+ id: String(i.number),
471
+ title: i.title,
472
+ description: i.body ?? "",
473
+ state: i.state,
474
+ labels: i.labels?.map((l) => l.name) ?? [],
475
+ url: i.url,
476
+ createdAt: new Date(i.createdAt),
477
+ updatedAt: new Date(i.updatedAt)
478
+ };
479
+ } catch {
480
+ return null;
481
+ }
482
+ }
483
+ onEvent(handler) {
484
+ this.handlers.push(handler);
485
+ }
486
+ emit(event) {
487
+ for (const handler of this.handlers) {
488
+ handler(event);
489
+ }
490
+ }
491
+ };
492
+ var debug9 = Debug("speexor:scm:github");
493
+ var GitHubSCM = class {
494
+ name = "github-scm";
495
+ version = "0.1.0";
496
+ type = "scm";
497
+ context;
498
+ handlers = [];
499
+ async initialize(context) {
500
+ this.context = context;
501
+ try {
502
+ execSync("gh --version", { stdio: "ignore" });
503
+ debug9("GitHub CLI available");
504
+ } catch {
505
+ throw new Error("GitHub CLI (gh) not found");
506
+ }
507
+ }
508
+ async destroy() {
509
+ this.handlers = [];
510
+ }
511
+ async createBranch(baseBranch, newBranch) {
512
+ execSync(`git fetch origin ${baseBranch}`, { stdio: "pipe" });
513
+ execSync(`git checkout -b ${newBranch} origin/${baseBranch}`, { stdio: "pipe" });
514
+ }
515
+ async commitAndPush(branch, message) {
516
+ execSync("git add -A", { stdio: "pipe" });
517
+ execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: "pipe" });
518
+ execSync(`git push origin ${branch}`, { stdio: "pipe" });
519
+ const sha = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
520
+ return sha;
521
+ }
522
+ async createPullRequest(title, description, head, base) {
523
+ const escapedBody = description.replace(/"/g, '\\"').replace(/\n/g, "\\n");
524
+ const output = execSync(
525
+ `gh pr create --title "${title}" --body "${escapedBody}" --head ${head} --base ${base} --json number,title,url,state,headRefName,baseRefName,createdAt`,
526
+ { encoding: "utf-8" }
527
+ );
528
+ const pr = JSON.parse(output);
529
+ return {
530
+ id: String(pr.number),
531
+ url: pr.url,
532
+ title: pr.title,
533
+ state: pr.state,
534
+ headBranch: pr.headRefName,
535
+ baseBranch: pr.baseRefName,
536
+ createdAt: new Date(pr.createdAt)
537
+ };
538
+ }
539
+ async getPRStatus(prId) {
540
+ const output = execSync(
541
+ `gh pr view ${prId} --json number,state,mergeable,reviews,statusCheckRollup --jq '{state,mergeable,reviews,checks: .statusCheckRollup}'`,
542
+ { encoding: "utf-8" }
543
+ );
544
+ const data = JSON.parse(output);
545
+ return {
546
+ id: prId,
547
+ state: data.state,
548
+ mergeable: data.mergeable === "MERGEABLE",
549
+ ciStatus: this.determineCIStatus(data.checks),
550
+ reviewStatus: this.determineReviewStatus(data.reviews)
551
+ };
552
+ }
553
+ async getPRComments(prId) {
554
+ const output = execSync(
555
+ `gh pr view ${prId} --json comments --jq '.comments[] | {id: .id, author: .author.name, body: .body, createdAt: .createdAt}'`,
556
+ { encoding: "utf-8" }
557
+ );
558
+ const lines = output.trim().split("\n");
559
+ return lines.filter((l) => l.trim()).map((line) => {
560
+ try {
561
+ return JSON.parse(line);
562
+ } catch {
563
+ return null;
564
+ }
565
+ }).filter((c) => c !== null);
566
+ }
567
+ async getCIRuns(prId) {
568
+ const output = execSync(
569
+ `gh pr view ${prId} --json statusCheckRollup --jq '.statusCheckRollup[] | {id: .databaseId, name: .name, status: .status, conclusion: .conclusion, url: .detailsUrl}'`,
570
+ { encoding: "utf-8" }
571
+ );
572
+ const lines = output.trim().split("\n");
573
+ return lines.filter((l) => l.trim()).map((line) => {
574
+ try {
575
+ return JSON.parse(line);
576
+ } catch {
577
+ return null;
578
+ }
579
+ }).filter((c) => c !== null);
580
+ }
581
+ async mergePR(prId, method = "squash") {
582
+ execSync(`gh pr merge ${prId} --${method}`, { stdio: "pipe" });
583
+ }
584
+ onEvent(handler) {
585
+ this.handlers.push(handler);
586
+ }
587
+ determineCIStatus(checks) {
588
+ if (!checks || checks.length === 0) return "unknown";
589
+ const allPassed = checks.every((c) => c.conclusion === "success");
590
+ const anyFailed = checks.some((c) => c.conclusion === "failure" || c.conclusion === "cancelled");
591
+ const anyPending = checks.some((c) => c.status === "in_progress" || c.status === "queued");
592
+ if (anyFailed) return "failing";
593
+ if (anyPending) return "pending";
594
+ if (allPassed) return "passing";
595
+ return "unknown";
596
+ }
597
+ determineReviewStatus(reviews) {
598
+ if (!reviews || reviews.length === 0) return "none";
599
+ const latestState = reviews[reviews.length - 1]?.state;
600
+ if (latestState === "APPROVED") return "approved";
601
+ if (latestState === "CHANGES_REQUESTED") return "changes-requested";
602
+ if (latestState === "PENDING") return "pending";
603
+ return "none";
604
+ }
605
+ };
606
+ var debug10 = Debug("speexor:notifier:desktop");
607
+ var DesktopNotifier = class {
608
+ name = "desktop-notifier";
609
+ version = "0.1.0";
610
+ type = "notifier";
611
+ async initialize(_context) {
612
+ debug10("Desktop notifier initialized");
613
+ }
614
+ async destroy() {
615
+ }
616
+ async notify(level, title, message) {
617
+ const platform = process.platform;
618
+ try {
619
+ if (platform === "darwin") {
620
+ const icon = level === "error" ? "\u26A0\uFE0F" : level === "success" ? "\u2705" : level === "warn" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
621
+ execSync(
622
+ `osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${icon} ${title.replace(/"/g, '\\"')}"'`,
623
+ { stdio: "ignore" }
624
+ );
625
+ } else if (platform === "win32") {
626
+ const icon = level === "error" ? "Warning" : "Information";
627
+ execSync(`powershell -c "New-BurntToastNotification -Text '${title}', '${message}'"`, { stdio: "ignore" });
628
+ } else if (platform === "linux") {
629
+ execSync(`notify-send "${title}" "${message}"`, { stdio: "ignore" });
630
+ }
631
+ debug10(`Desktop notification: ${title} - ${message}`);
632
+ } catch (error) {
633
+ debug10(`Failed to send notification: ${error}`);
634
+ }
635
+ }
636
+ };
637
+
638
+ // src/plugins/index.ts
639
+ function loadAllPlugins() {
640
+ const plugins = [
641
+ // Agent adapters
642
+ new OpenCodeAgent(),
643
+ new ClaudeCodeAgent(),
644
+ new AiderAgent(),
645
+ new CodexAgent(),
646
+ // Runtime
647
+ new TmuxRuntime(),
648
+ new ProcessRuntime(),
649
+ // Workspace
650
+ new GitWorktreeWorkspace(),
651
+ // Tracker
652
+ new GitHubTracker(),
653
+ // SCM
654
+ new GitHubSCM(),
655
+ // Notifier
656
+ new DesktopNotifier()
657
+ ];
658
+ return plugins;
659
+ }
660
+ function loadPluginByType(type) {
661
+ return loadAllPlugins().find((p) => p.type === type);
662
+ }
663
+
664
+ export { loadAllPlugins, loadPluginByType };
665
+ //# sourceMappingURL=chunk-B7WLHC4W.js.map
666
+ //# sourceMappingURL=chunk-B7WLHC4W.js.map