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.
- package/API-REFERENCE.md +96 -1
- package/ARCHITECTURE.md +83 -32
- package/BENCHMARKS.md +73 -0
- package/CHANGELOG.md +59 -4
- package/CODE-OF-CONDUCT.md +83 -83
- package/CONTRIBUTING.md +92 -97
- package/FAQ.md +132 -105
- package/GLOSSARY.md +34 -0
- package/LICENSE.md +21 -21
- package/PUBLISH.md +82 -77
- package/README.md +220 -6
- package/REFACTOR-LOG.md +40 -40
- package/ROADMAP.md +31 -42
- package/SECURITY-DEFAULTS.md +118 -0
- package/SECURITY.md +80 -79
- package/SUMMARY.md +31 -8
- package/TESTING.md +140 -140
- package/dist/{agent-5D3BVWNK.js → agent-C64T66XT.js} +4 -4
- package/dist/agent-C64T66XT.js.map +1 -0
- package/dist/{chunk-B7WLHC4W.js → chunk-5OD5UWB5.js} +322 -121
- package/dist/chunk-5OD5UWB5.js.map +1 -0
- package/dist/chunk-GOGI3JQD.js +1637 -0
- package/dist/chunk-GOGI3JQD.js.map +1 -0
- package/dist/{chunk-2F66BZYJ.js → chunk-VEZQT5SX.js} +80 -8
- package/dist/chunk-VEZQT5SX.js.map +1 -0
- package/dist/cli/index.js +2058 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/core/index.d.ts +682 -3
- package/dist/core/index.js +1 -1
- package/dist/index.d.ts +102 -14
- package/dist/index.js +55 -29
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/types-BOMap-tI.d.ts +389 -0
- package/docs/PRD03.md +119 -0
- package/docs/PRD06.md +125 -0
- package/docs/SETUP.md +94 -94
- package/docs/TROUBLESHOOTING.md +113 -113
- package/docs/adr/0001-record-architecture-decisions.md +44 -0
- package/docs/adr/0002-plugin-architecture.md +53 -0
- package/docs/adr/0003-recursive-task-decomposition.md +57 -0
- package/docs/adr/0004-local-first-security.md +58 -0
- package/docs/adr/0005-data-directory-layout.md +69 -0
- package/examples/basic.yaml +61 -61
- package/package.json +103 -102
- package/schema/config.schema.json +119 -119
- package/speexor.config.yaml.example +30 -30
- package/dist/agent-5D3BVWNK.js.map +0 -1
- package/dist/chunk-2F66BZYJ.js.map +0 -1
- package/dist/chunk-B7WLHC4W.js.map +0 -1
- package/dist/chunk-SXALZEOJ.js +0 -345
- package/dist/chunk-SXALZEOJ.js.map +0 -1
- 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/
|
|
8
|
-
var debug = Debug("speexor:agent:
|
|
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
|
-
|
|
45
|
+
debug2("OpenCode agent initialized");
|
|
18
46
|
}
|
|
19
47
|
async destroy() {
|
|
20
48
|
this.sessions.clear();
|
|
21
|
-
|
|
49
|
+
debug2("OpenCode agent destroyed");
|
|
22
50
|
}
|
|
23
51
|
async spawn(task, runtime) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
+
debug2(`OpenCode agent killed: ${sessionId}`);
|
|
50
84
|
}
|
|
51
85
|
};
|
|
52
|
-
var
|
|
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
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
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
|
-
|
|
170
|
-
async initialize(context) {
|
|
171
|
-
this.context = context;
|
|
220
|
+
async initialize(_context) {
|
|
172
221
|
try {
|
|
173
222
|
execSync("tmux -V", { stdio: "ignore" });
|
|
174
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
|
206
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
244
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
429
|
-
this.context = context;
|
|
553
|
+
async initialize(_context) {
|
|
430
554
|
try {
|
|
431
555
|
execSync("gh --version", { stdio: "ignore" });
|
|
432
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
500
|
-
this.context = context;
|
|
617
|
+
async initialize(_context) {
|
|
501
618
|
try {
|
|
502
619
|
execSync("gh --version", { stdio: "ignore" });
|
|
503
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
747
|
+
debug11(`Desktop notification: ${title} - ${message}`);
|
|
632
748
|
} catch (error) {
|
|
633
|
-
|
|
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-
|
|
666
|
-
//# sourceMappingURL=chunk-
|
|
866
|
+
//# sourceMappingURL=chunk-5OD5UWB5.js.map
|
|
867
|
+
//# sourceMappingURL=chunk-5OD5UWB5.js.map
|