@locusai/locus-gateway 0.22.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.
- package/dist/commands.d.ts +14 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/executor.d.ts +44 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/formatter.d.ts +14 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/gateway.d.ts +50 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/index.cjs +592 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +550 -0
- package/dist/router.d.ts +14 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/tracker.d.ts +25 -0
- package/dist/tracker.d.ts.map +1 -0
- package/dist/types.d.ts +180 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +46 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
function __accessProp(key) {
|
|
6
|
+
return this[key];
|
|
7
|
+
}
|
|
8
|
+
var __toCommonJS = (from) => {
|
|
9
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
10
|
+
if (entry)
|
|
11
|
+
return entry;
|
|
12
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (var key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(entry, key))
|
|
16
|
+
__defProp(entry, key, {
|
|
17
|
+
get: __accessProp.bind(from, key),
|
|
18
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
__moduleCache.set(from, entry);
|
|
22
|
+
return entry;
|
|
23
|
+
};
|
|
24
|
+
var __moduleCache;
|
|
25
|
+
var __returnValue = (v) => v;
|
|
26
|
+
function __exportSetter(name, newValue) {
|
|
27
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
28
|
+
}
|
|
29
|
+
var __export = (target, all) => {
|
|
30
|
+
for (var name in all)
|
|
31
|
+
__defProp(target, name, {
|
|
32
|
+
get: all[name],
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
set: __exportSetter.bind(all, name)
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/index.ts
|
|
40
|
+
var exports_src = {};
|
|
41
|
+
__export(exports_src, {
|
|
42
|
+
truncate: () => truncate,
|
|
43
|
+
splitMessage: () => splitMessage,
|
|
44
|
+
getCommandDefinition: () => getCommandDefinition,
|
|
45
|
+
bestFormat: () => bestFormat,
|
|
46
|
+
STREAMING_COMMANDS: () => STREAMING_COMMANDS,
|
|
47
|
+
Gateway: () => Gateway,
|
|
48
|
+
CommandTracker: () => CommandTracker,
|
|
49
|
+
CommandRouter: () => CommandRouter,
|
|
50
|
+
CommandExecutor: () => CommandExecutor,
|
|
51
|
+
COMMAND_REGISTRY: () => COMMAND_REGISTRY
|
|
52
|
+
});
|
|
53
|
+
module.exports = __toCommonJS(exports_src);
|
|
54
|
+
|
|
55
|
+
// src/commands.ts
|
|
56
|
+
var COMMAND_REGISTRY = {
|
|
57
|
+
run: { cliArgs: ["run"], streaming: true },
|
|
58
|
+
status: { cliArgs: ["status"], streaming: false },
|
|
59
|
+
issues: { cliArgs: ["issue", "list"], streaming: false },
|
|
60
|
+
issue: { cliArgs: ["issue", "show"], streaming: false },
|
|
61
|
+
sprint: { cliArgs: ["sprint"], streaming: false },
|
|
62
|
+
plan: { cliArgs: ["plan"], streaming: true },
|
|
63
|
+
review: { cliArgs: ["review"], streaming: true },
|
|
64
|
+
iterate: { cliArgs: ["iterate"], streaming: true },
|
|
65
|
+
discuss: {
|
|
66
|
+
cliArgs: ["discuss"],
|
|
67
|
+
streaming: true,
|
|
68
|
+
requiresArgs: `Please provide a discussion topic.
|
|
69
|
+
|
|
70
|
+
Example: /discuss Should we use Redis or in-memory caching?`
|
|
71
|
+
},
|
|
72
|
+
exec: {
|
|
73
|
+
cliArgs: ["exec"],
|
|
74
|
+
streaming: true,
|
|
75
|
+
requiresArgs: `Please provide a prompt.
|
|
76
|
+
|
|
77
|
+
Example: /exec Add error handling to the API`
|
|
78
|
+
},
|
|
79
|
+
logs: { cliArgs: ["logs"], streaming: false },
|
|
80
|
+
config: { cliArgs: ["config"], streaming: false },
|
|
81
|
+
artifacts: { cliArgs: ["artifacts"], streaming: false }
|
|
82
|
+
};
|
|
83
|
+
var STREAMING_COMMANDS = new Set(Object.entries(COMMAND_REGISTRY).filter(([, def]) => def.streaming).map(([name]) => name));
|
|
84
|
+
function getCommandDefinition(command) {
|
|
85
|
+
return COMMAND_REGISTRY[command] ?? null;
|
|
86
|
+
}
|
|
87
|
+
// src/executor.ts
|
|
88
|
+
var import_node_child_process2 = require("node:child_process");
|
|
89
|
+
var import_node_util = require("node:util");
|
|
90
|
+
|
|
91
|
+
// ../sdk/dist/index.js
|
|
92
|
+
var import_node_child_process = require("node:child_process");
|
|
93
|
+
function invokeLocusStream(args, cwd) {
|
|
94
|
+
return import_node_child_process.spawn("locus", args, {
|
|
95
|
+
cwd: cwd ?? process.cwd(),
|
|
96
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
97
|
+
env: process.env,
|
|
98
|
+
shell: false
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
var colorEnabled = () => process.stderr.isTTY === true && process.env.NO_COLOR === undefined;
|
|
102
|
+
var wrap = (open, close) => (text) => colorEnabled() ? `${open}${text}${close}` : text;
|
|
103
|
+
var bold = wrap("\x1B[1m", "\x1B[22m");
|
|
104
|
+
var dim = wrap("\x1B[2m", "\x1B[22m");
|
|
105
|
+
var red = wrap("\x1B[31m", "\x1B[39m");
|
|
106
|
+
var yellow = wrap("\x1B[33m", "\x1B[39m");
|
|
107
|
+
var cyan = wrap("\x1B[36m", "\x1B[39m");
|
|
108
|
+
var gray = wrap("\x1B[90m", "\x1B[39m");
|
|
109
|
+
function formatData(data) {
|
|
110
|
+
if (!data || Object.keys(data).length === 0)
|
|
111
|
+
return "";
|
|
112
|
+
return ` ${dim(JSON.stringify(data))}`;
|
|
113
|
+
}
|
|
114
|
+
function createLogger(name) {
|
|
115
|
+
const prefix = dim(`[${name}]`);
|
|
116
|
+
return {
|
|
117
|
+
info(msg, data) {
|
|
118
|
+
process.stderr.write(`${bold(cyan("●"))} ${prefix} ${msg}${formatData(data)}
|
|
119
|
+
`);
|
|
120
|
+
},
|
|
121
|
+
warn(msg, data) {
|
|
122
|
+
process.stderr.write(`${bold(yellow("⚠"))} ${prefix} ${yellow(msg)}${formatData(data)}
|
|
123
|
+
`);
|
|
124
|
+
},
|
|
125
|
+
error(msg, data) {
|
|
126
|
+
process.stderr.write(`${bold(red("✗"))} ${prefix} ${red(msg)}${formatData(data)}
|
|
127
|
+
`);
|
|
128
|
+
},
|
|
129
|
+
debug(msg, data) {
|
|
130
|
+
if (!process.env.LOCUS_DEBUG)
|
|
131
|
+
return;
|
|
132
|
+
process.stderr.write(`${gray("⋯")} ${prefix} ${gray(msg)}${formatData(data)}
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/executor.ts
|
|
139
|
+
var exec = import_node_util.promisify(import_node_child_process2.exec);
|
|
140
|
+
var STREAM_UPDATE_INTERVAL = 2000;
|
|
141
|
+
|
|
142
|
+
class CommandExecutor {
|
|
143
|
+
tracker;
|
|
144
|
+
constructor(tracker) {
|
|
145
|
+
this.tracker = tracker;
|
|
146
|
+
}
|
|
147
|
+
getTracker() {
|
|
148
|
+
return this.tracker;
|
|
149
|
+
}
|
|
150
|
+
async executeLocusCommand(sessionId, command, args, callbacks) {
|
|
151
|
+
const definition = getCommandDefinition(command);
|
|
152
|
+
if (!definition) {
|
|
153
|
+
return {
|
|
154
|
+
text: `Unknown command: /${command}`,
|
|
155
|
+
format: "plain",
|
|
156
|
+
exitCode: 1
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (definition.requiresArgs && args.length === 0) {
|
|
160
|
+
return {
|
|
161
|
+
text: definition.requiresArgs,
|
|
162
|
+
format: "plain",
|
|
163
|
+
exitCode: 1
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const conflict = this.tracker.checkExclusiveConflict(sessionId, command);
|
|
167
|
+
if (conflict) {
|
|
168
|
+
return {
|
|
169
|
+
text: formatConflictText(command, conflict),
|
|
170
|
+
format: "plain",
|
|
171
|
+
exitCode: 1
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const fullArgs = [...definition.cliArgs, ...args];
|
|
175
|
+
if (definition.streaming && callbacks) {
|
|
176
|
+
return this.executeStreaming(sessionId, command, args, fullArgs, callbacks);
|
|
177
|
+
}
|
|
178
|
+
return this.executeBuffered(sessionId, command, args, fullArgs);
|
|
179
|
+
}
|
|
180
|
+
async executeGit(sessionId, command, args, gitArgs) {
|
|
181
|
+
const conflict = this.tracker.checkExclusiveConflict(sessionId, command);
|
|
182
|
+
if (conflict) {
|
|
183
|
+
return {
|
|
184
|
+
text: formatConflictText(command, conflict),
|
|
185
|
+
format: "plain",
|
|
186
|
+
exitCode: 1
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const trackingId = this.tracker.track(sessionId, command, args);
|
|
190
|
+
try {
|
|
191
|
+
const { stdout } = await exec(`git ${gitArgs}`, { cwd: process.cwd() });
|
|
192
|
+
return {
|
|
193
|
+
text: stdout,
|
|
194
|
+
format: "plain",
|
|
195
|
+
exitCode: 0
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const errStr = String(error);
|
|
199
|
+
return {
|
|
200
|
+
text: errStr,
|
|
201
|
+
format: "plain",
|
|
202
|
+
exitCode: 1
|
|
203
|
+
};
|
|
204
|
+
} finally {
|
|
205
|
+
this.tracker.untrack(sessionId, trackingId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async executeStreaming(sessionId, command, args, fullArgs, callbacks) {
|
|
209
|
+
const child = invokeLocusStream(fullArgs);
|
|
210
|
+
const trackingId = this.tracker.track(sessionId, command, args, child);
|
|
211
|
+
let output = "";
|
|
212
|
+
let lastUpdateTime = 0;
|
|
213
|
+
let updateTimer = null;
|
|
214
|
+
let messageId = "";
|
|
215
|
+
const displayCmd = `locus ${fullArgs.join(" ")}`;
|
|
216
|
+
const startResult = await callbacks.onStart(formatStreamingText(displayCmd, "", false));
|
|
217
|
+
if (startResult)
|
|
218
|
+
messageId = startResult;
|
|
219
|
+
const pushUpdate = async () => {
|
|
220
|
+
const now = Date.now();
|
|
221
|
+
if (now - lastUpdateTime < STREAM_UPDATE_INTERVAL)
|
|
222
|
+
return;
|
|
223
|
+
lastUpdateTime = now;
|
|
224
|
+
try {
|
|
225
|
+
await callbacks.onUpdate(messageId, formatStreamingText(displayCmd, output, false));
|
|
226
|
+
} catch {}
|
|
227
|
+
};
|
|
228
|
+
child.stdout?.on("data", (chunk) => {
|
|
229
|
+
output += chunk.toString();
|
|
230
|
+
if (updateTimer)
|
|
231
|
+
clearTimeout(updateTimer);
|
|
232
|
+
updateTimer = setTimeout(pushUpdate, STREAM_UPDATE_INTERVAL);
|
|
233
|
+
});
|
|
234
|
+
child.stderr?.on("data", (chunk) => {
|
|
235
|
+
output += chunk.toString();
|
|
236
|
+
});
|
|
237
|
+
return new Promise((resolve) => {
|
|
238
|
+
child.on("close", async (exitCode) => {
|
|
239
|
+
this.tracker.untrack(sessionId, trackingId);
|
|
240
|
+
if (updateTimer)
|
|
241
|
+
clearTimeout(updateTimer);
|
|
242
|
+
const code = exitCode ?? 0;
|
|
243
|
+
await callbacks.onComplete(messageId, formatStreamingText(displayCmd, output, true), code);
|
|
244
|
+
resolve({
|
|
245
|
+
text: output,
|
|
246
|
+
format: "plain",
|
|
247
|
+
streaming: true,
|
|
248
|
+
exitCode: code
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async executeBuffered(sessionId, command, args, fullArgs) {
|
|
254
|
+
const child = invokeLocusStream(fullArgs);
|
|
255
|
+
const trackingId = this.tracker.track(sessionId, command, args, child);
|
|
256
|
+
let output = "";
|
|
257
|
+
child.stdout?.on("data", (chunk) => {
|
|
258
|
+
output += chunk.toString();
|
|
259
|
+
});
|
|
260
|
+
child.stderr?.on("data", (chunk) => {
|
|
261
|
+
output += chunk.toString();
|
|
262
|
+
});
|
|
263
|
+
return new Promise((resolve) => {
|
|
264
|
+
child.on("close", (exitCode) => {
|
|
265
|
+
this.tracker.untrack(sessionId, trackingId);
|
|
266
|
+
const code = exitCode ?? 0;
|
|
267
|
+
resolve({
|
|
268
|
+
text: output,
|
|
269
|
+
format: "plain",
|
|
270
|
+
exitCode: code
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function formatConflictText(blockedCommand, conflict) {
|
|
277
|
+
const running = conflict.runningCommand;
|
|
278
|
+
const runningLabel = `/${running.command}${running.args.length ? ` ${running.args.join(" ")}` : ""}`;
|
|
279
|
+
return `/${blockedCommand} cannot start — ${runningLabel} is already running.
|
|
280
|
+
|
|
281
|
+
Send /cancel to abort it, or wait for it to finish.`;
|
|
282
|
+
}
|
|
283
|
+
function formatStreamingText(command, output, isComplete) {
|
|
284
|
+
const status = isComplete ? "[DONE]" : "[RUNNING]";
|
|
285
|
+
const header = `${status} ${command}`;
|
|
286
|
+
if (!output.trim()) {
|
|
287
|
+
return isComplete ? `${header}
|
|
288
|
+
|
|
289
|
+
Completed.` : `${header}
|
|
290
|
+
|
|
291
|
+
Running...
|
|
292
|
+
|
|
293
|
+
Send /cancel to abort`;
|
|
294
|
+
}
|
|
295
|
+
const lines = output.trim().split(`
|
|
296
|
+
`);
|
|
297
|
+
const lastLines = lines.slice(-30).join(`
|
|
298
|
+
`);
|
|
299
|
+
const hint = isComplete ? "" : `
|
|
300
|
+
|
|
301
|
+
Send /cancel to abort`;
|
|
302
|
+
return `${header}
|
|
303
|
+
|
|
304
|
+
${lastLines}${hint}`;
|
|
305
|
+
}
|
|
306
|
+
// src/formatter.ts
|
|
307
|
+
function bestFormat(capabilities) {
|
|
308
|
+
if (capabilities.supportsHTML)
|
|
309
|
+
return "html";
|
|
310
|
+
if (capabilities.supportsMarkdown)
|
|
311
|
+
return "markdown";
|
|
312
|
+
return "plain";
|
|
313
|
+
}
|
|
314
|
+
function splitMessage(text, maxLength) {
|
|
315
|
+
if (text.length <= maxLength)
|
|
316
|
+
return [text];
|
|
317
|
+
const chunks = [];
|
|
318
|
+
let remaining = text;
|
|
319
|
+
while (remaining.length > 0) {
|
|
320
|
+
if (remaining.length <= maxLength) {
|
|
321
|
+
chunks.push(remaining);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
let splitIdx = remaining.lastIndexOf(`
|
|
325
|
+
`, maxLength);
|
|
326
|
+
if (splitIdx === -1 || splitIdx < maxLength / 2) {
|
|
327
|
+
splitIdx = maxLength;
|
|
328
|
+
}
|
|
329
|
+
chunks.push(remaining.slice(0, splitIdx));
|
|
330
|
+
remaining = remaining.slice(splitIdx);
|
|
331
|
+
}
|
|
332
|
+
return chunks;
|
|
333
|
+
}
|
|
334
|
+
function truncate(text, maxLength) {
|
|
335
|
+
if (text.length <= maxLength)
|
|
336
|
+
return text;
|
|
337
|
+
return `${text.slice(0, maxLength - 20)}
|
|
338
|
+
|
|
339
|
+
... (truncated)`;
|
|
340
|
+
}
|
|
341
|
+
// src/router.ts
|
|
342
|
+
class CommandRouter {
|
|
343
|
+
prefix;
|
|
344
|
+
constructor(prefix = "/") {
|
|
345
|
+
this.prefix = prefix;
|
|
346
|
+
}
|
|
347
|
+
parse(text) {
|
|
348
|
+
const trimmed = text.trim();
|
|
349
|
+
if (!trimmed.startsWith(this.prefix)) {
|
|
350
|
+
return { type: "freetext", text: trimmed };
|
|
351
|
+
}
|
|
352
|
+
const withoutPrefix = trimmed.slice(this.prefix.length);
|
|
353
|
+
const parts = withoutPrefix.split(/\s+/);
|
|
354
|
+
const rawCommand = parts[0] ?? "";
|
|
355
|
+
const command = rawCommand.replace(/@\S+$/, "").toLowerCase();
|
|
356
|
+
if (!command) {
|
|
357
|
+
return { type: "freetext", text: trimmed };
|
|
358
|
+
}
|
|
359
|
+
const args = parts.slice(1);
|
|
360
|
+
return {
|
|
361
|
+
type: "command",
|
|
362
|
+
command,
|
|
363
|
+
args,
|
|
364
|
+
raw: trimmed
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/tracker.ts
|
|
370
|
+
var WORKSPACE_EXCLUSIVE = new Set(["run", "plan", "iterate", "exec"]);
|
|
371
|
+
var GIT_EXCLUSIVE = new Set(["stage", "commit", "checkout", "stash", "pr"]);
|
|
372
|
+
function getExclusivityGroup(command) {
|
|
373
|
+
if (WORKSPACE_EXCLUSIVE.has(command))
|
|
374
|
+
return "workspace";
|
|
375
|
+
if (GIT_EXCLUSIVE.has(command))
|
|
376
|
+
return "git";
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
var nextId = 1;
|
|
380
|
+
|
|
381
|
+
class CommandTracker {
|
|
382
|
+
active = new Map;
|
|
383
|
+
track(sessionId, command, args, childProcess = null) {
|
|
384
|
+
const id = String(nextId++);
|
|
385
|
+
const entry = {
|
|
386
|
+
id,
|
|
387
|
+
command,
|
|
388
|
+
args,
|
|
389
|
+
childProcess,
|
|
390
|
+
startedAt: new Date
|
|
391
|
+
};
|
|
392
|
+
const list = this.active.get(sessionId);
|
|
393
|
+
if (list) {
|
|
394
|
+
list.push(entry);
|
|
395
|
+
} else {
|
|
396
|
+
this.active.set(sessionId, [entry]);
|
|
397
|
+
}
|
|
398
|
+
return id;
|
|
399
|
+
}
|
|
400
|
+
untrack(sessionId, id) {
|
|
401
|
+
const list = this.active.get(sessionId);
|
|
402
|
+
if (!list)
|
|
403
|
+
return;
|
|
404
|
+
const idx = list.findIndex((c) => c.id === id);
|
|
405
|
+
if (idx !== -1)
|
|
406
|
+
list.splice(idx, 1);
|
|
407
|
+
if (list.length === 0)
|
|
408
|
+
this.active.delete(sessionId);
|
|
409
|
+
}
|
|
410
|
+
getActive(sessionId) {
|
|
411
|
+
return this.active.get(sessionId) ?? [];
|
|
412
|
+
}
|
|
413
|
+
checkExclusiveConflict(sessionId, command) {
|
|
414
|
+
const group = getExclusivityGroup(command);
|
|
415
|
+
if (!group)
|
|
416
|
+
return null;
|
|
417
|
+
const list = this.active.get(sessionId);
|
|
418
|
+
if (!list)
|
|
419
|
+
return null;
|
|
420
|
+
for (const entry of list) {
|
|
421
|
+
if (getExclusivityGroup(entry.command) === group) {
|
|
422
|
+
return { blocked: true, runningCommand: entry, group };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
kill(sessionId, id) {
|
|
428
|
+
const list = this.active.get(sessionId);
|
|
429
|
+
if (!list)
|
|
430
|
+
return false;
|
|
431
|
+
const entry = list.find((c) => c.id === id);
|
|
432
|
+
if (!entry)
|
|
433
|
+
return false;
|
|
434
|
+
if (entry.childProcess && !entry.childProcess.killed) {
|
|
435
|
+
entry.childProcess.kill("SIGTERM");
|
|
436
|
+
}
|
|
437
|
+
this.untrack(sessionId, id);
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
killAll(sessionId) {
|
|
441
|
+
const list = this.active.get(sessionId);
|
|
442
|
+
if (!list)
|
|
443
|
+
return 0;
|
|
444
|
+
let killed = 0;
|
|
445
|
+
for (const entry of list) {
|
|
446
|
+
if (entry.childProcess && !entry.childProcess.killed) {
|
|
447
|
+
entry.childProcess.kill("SIGTERM");
|
|
448
|
+
}
|
|
449
|
+
killed++;
|
|
450
|
+
}
|
|
451
|
+
this.active.delete(sessionId);
|
|
452
|
+
return killed;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/gateway.ts
|
|
457
|
+
var logger = createLogger("gateway");
|
|
458
|
+
|
|
459
|
+
class Gateway {
|
|
460
|
+
adapters = new Map;
|
|
461
|
+
router;
|
|
462
|
+
executor;
|
|
463
|
+
tracker;
|
|
464
|
+
onEvent;
|
|
465
|
+
constructor(options = {}) {
|
|
466
|
+
this.router = new CommandRouter;
|
|
467
|
+
this.tracker = new CommandTracker;
|
|
468
|
+
this.executor = new CommandExecutor(this.tracker);
|
|
469
|
+
this.onEvent = options.onEvent;
|
|
470
|
+
}
|
|
471
|
+
register(adapter) {
|
|
472
|
+
if (this.adapters.has(adapter.platform)) {
|
|
473
|
+
throw new Error(`Adapter already registered for platform: ${adapter.platform}`);
|
|
474
|
+
}
|
|
475
|
+
this.adapters.set(adapter.platform, adapter);
|
|
476
|
+
logger.info(`Registered adapter: ${adapter.platform}`);
|
|
477
|
+
}
|
|
478
|
+
getAdapter(platform) {
|
|
479
|
+
return this.adapters.get(platform);
|
|
480
|
+
}
|
|
481
|
+
getRouter() {
|
|
482
|
+
return this.router;
|
|
483
|
+
}
|
|
484
|
+
getExecutor() {
|
|
485
|
+
return this.executor;
|
|
486
|
+
}
|
|
487
|
+
getTracker() {
|
|
488
|
+
return this.tracker;
|
|
489
|
+
}
|
|
490
|
+
async handleMessage(message) {
|
|
491
|
+
this.emit({ type: "message_received", message });
|
|
492
|
+
const adapter = this.adapters.get(message.platform);
|
|
493
|
+
if (!adapter) {
|
|
494
|
+
logger.warn(`No adapter registered for platform: ${message.platform}`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const parsed = this.router.parse(message.text);
|
|
498
|
+
if (parsed.type === "freetext") {
|
|
499
|
+
await this.executeCommand(adapter, message.sessionId, "exec", [parsed.text], message);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
await this.executeCommand(adapter, message.sessionId, parsed.command, parsed.args, message);
|
|
503
|
+
}
|
|
504
|
+
async start() {
|
|
505
|
+
const platforms = Array.from(this.adapters.keys());
|
|
506
|
+
logger.info(`Starting gateway with adapters: ${platforms.join(", ")}`);
|
|
507
|
+
for (const [platform, adapter] of this.adapters) {
|
|
508
|
+
try {
|
|
509
|
+
await adapter.start();
|
|
510
|
+
logger.info(`Started adapter: ${platform}`);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
logger.error(`Failed to start adapter: ${platform}`, {
|
|
513
|
+
error: String(error)
|
|
514
|
+
});
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async stop() {
|
|
520
|
+
for (const [platform, adapter] of this.adapters) {
|
|
521
|
+
try {
|
|
522
|
+
await adapter.stop();
|
|
523
|
+
logger.info(`Stopped adapter: ${platform}`);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
logger.warn(`Error stopping adapter: ${platform}`, {
|
|
526
|
+
error: String(error)
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async executeCommand(adapter, sessionId, command, args, _originalMessage) {
|
|
532
|
+
this.emit({
|
|
533
|
+
type: "command_started",
|
|
534
|
+
sessionId,
|
|
535
|
+
command,
|
|
536
|
+
args
|
|
537
|
+
});
|
|
538
|
+
let streamCallbacks;
|
|
539
|
+
if (adapter.capabilities.supportsStreaming && adapter.edit) {
|
|
540
|
+
const adapterRef = adapter;
|
|
541
|
+
const sentMessageId = "";
|
|
542
|
+
streamCallbacks = {
|
|
543
|
+
async onStart(text) {
|
|
544
|
+
await adapterRef.send(sessionId, {
|
|
545
|
+
text,
|
|
546
|
+
format: "plain"
|
|
547
|
+
});
|
|
548
|
+
return sentMessageId;
|
|
549
|
+
},
|
|
550
|
+
async onUpdate(messageId, text) {
|
|
551
|
+
if (adapterRef.edit) {
|
|
552
|
+
await adapterRef.edit(sessionId, messageId, {
|
|
553
|
+
text,
|
|
554
|
+
format: "plain"
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
async onComplete(messageId, text, _exitCode) {
|
|
559
|
+
if (adapterRef.edit) {
|
|
560
|
+
await adapterRef.edit(sessionId, messageId, {
|
|
561
|
+
text,
|
|
562
|
+
format: "plain"
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const result = await this.executor.executeLocusCommand(sessionId, command, args, streamCallbacks);
|
|
569
|
+
this.emit({
|
|
570
|
+
type: "command_completed",
|
|
571
|
+
sessionId,
|
|
572
|
+
command,
|
|
573
|
+
exitCode: result.exitCode
|
|
574
|
+
});
|
|
575
|
+
if (!result.streaming) {
|
|
576
|
+
await adapter.send(sessionId, {
|
|
577
|
+
text: result.text,
|
|
578
|
+
format: result.format,
|
|
579
|
+
actions: result.actions
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
emit(event) {
|
|
584
|
+
if (this.onEvent) {
|
|
585
|
+
try {
|
|
586
|
+
this.onEvent(event);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
logger.warn("Event handler error", { error: String(error) });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @locusai/locus-gateway — Channel-agnostic message gateway for Locus.
|
|
3
|
+
*
|
|
4
|
+
* Provides the core abstractions for building platform adapters
|
|
5
|
+
* (Telegram, Discord, WhatsApp, etc.) that connect to the Locus CLI.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import {
|
|
10
|
+
* Gateway,
|
|
11
|
+
* CommandRouter,
|
|
12
|
+
* CommandExecutor,
|
|
13
|
+
* CommandTracker,
|
|
14
|
+
* } from "@locusai/locus-gateway";
|
|
15
|
+
* import type {
|
|
16
|
+
* PlatformAdapter,
|
|
17
|
+
* PlatformCapabilities,
|
|
18
|
+
* InboundMessage,
|
|
19
|
+
* OutboundMessage,
|
|
20
|
+
* } from "@locusai/locus-gateway";
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export { COMMAND_REGISTRY, getCommandDefinition, STREAMING_COMMANDS, } from "./commands.js";
|
|
24
|
+
export type { StreamCallbacks } from "./executor.js";
|
|
25
|
+
export { CommandExecutor } from "./executor.js";
|
|
26
|
+
export { bestFormat, splitMessage, truncate } from "./formatter.js";
|
|
27
|
+
export type { GatewayEventHandler, GatewayOptions } from "./gateway.js";
|
|
28
|
+
export { Gateway } from "./gateway.js";
|
|
29
|
+
export { CommandRouter } from "./router.js";
|
|
30
|
+
export { CommandTracker } from "./tracker.js";
|
|
31
|
+
export type { Action, ActiveCommand, Attachment, CommandDefinition, CommandResult, ConflictResult, ExclusivityGroup, FreeText, GatewayEvent, InboundMessage, OutboundMessage, ParsedCommand, PlatformAdapter, PlatformCapabilities, RouteContext, } from "./types.js";
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACpE,YAAY,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAExE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9C,YAAY,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,eAAe,EACf,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,YAAY,GACb,MAAM,YAAY,CAAC"}
|