@borgee/agents-host 0.2.33 → 0.2.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -7
- package/dist/agents-host.d.ts +19 -0
- package/dist/agents-host.js +163 -44
- package/dist/chat/chat-control-plane.d.ts +9 -0
- package/dist/chat/sdk-chat-control-plane.d.ts +10 -1
- package/dist/chat/sdk-chat-control-plane.js +9 -0
- package/dist/cli-args.d.ts +1 -1
- package/dist/cli-args.js +5 -0
- package/dist/compatibility-gates.d.ts +1 -0
- package/dist/compatibility-gates.js +2 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +21 -3
- package/dist/context/injection.d.ts +5 -3
- package/dist/context/injection.js +45 -29
- package/dist/context/main-session-delegation.d.ts +1 -0
- package/dist/context/main-session-delegation.js +6 -0
- package/dist/context/prompt.js +25 -35
- package/dist/context/skill-manual.d.ts +13 -0
- package/dist/context/skill-manual.js +18 -0
- package/dist/context/turn-preparation.js +13 -4
- package/dist/gateway/localhost-gateway.js +75 -1
- package/dist/hosted-turn-content.d.ts +15 -0
- package/dist/hosted-turn-content.js +50 -0
- package/dist/local-config.js +10 -1
- package/dist/managed-daemon.d.ts +3 -2
- package/dist/managed-daemon.js +82 -29
- package/dist/plugin-sdk.js +58 -1
- package/dist/plugin-sdk.js.map +2 -2
- package/dist/policy/gateway-authorization.d.ts +18 -3
- package/dist/policy/gateway-authorization.js +33 -1
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +13 -1
- package/dist/providers/claude/cli-client.d.ts +36 -3
- package/dist/providers/claude/cli-client.js +225 -37
- package/dist/providers/codex/adapter.d.ts +3 -1
- package/dist/providers/codex/adapter.js +13 -1
- package/dist/providers/codex/cli-client.d.ts +35 -3
- package/dist/providers/codex/cli-client.js +212 -31
- package/dist/providers/codex/project-doc.js +16 -29
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +13 -1
- package/dist/providers/copilot/cli-client.d.ts +34 -2
- package/dist/providers/copilot/cli-client.js +196 -18
- package/dist/providers/create-provider.d.ts +1 -1
- package/dist/providers/create-provider.js +30 -14
- package/dist/providers/idle-backend-shutdown.d.ts +16 -0
- package/dist/providers/idle-backend-shutdown.js +53 -0
- package/dist/providers/provider-adapter.d.ts +35 -0
- package/dist/providers/provider-adapter.js +44 -1
- package/dist/state-paths.d.ts +9 -1
- package/dist/state-paths.js +22 -3
- package/dist/types.d.ts +40 -2
- package/package.json +2 -2
- package/skills/borgee-agent/SKILL.md +133 -35
- package/skills/borgee-agent/references/errors.md +38 -0
- package/skills/borgee-agent/references/task-properties.md +30 -0
- package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
- package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
- package/skills/borgee-agent/borgee-agent.mjs +0 -507
- package/skills/borgee-agent/borgee-agent.py +0 -438
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { isAbsolute } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const MANUAL = `borgee-agent — read the Borgee channel this turn runs in, and act on its tasks.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
borgee-agent --gateway <absolute path> <command> [arguments]
|
|
10
|
+
borgee-agent --help
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
health
|
|
14
|
+
Gateway reachability.
|
|
15
|
+
bootstrap
|
|
16
|
+
Channel bootstrap snapshot.
|
|
17
|
+
whoami
|
|
18
|
+
This agent's identity in this channel.
|
|
19
|
+
history [--limit <n>] [--before <n>] [--after <n>]
|
|
20
|
+
Recent messages in this channel.
|
|
21
|
+
users
|
|
22
|
+
Participants visible in this channel.
|
|
23
|
+
draft --turn-execution-id <id>
|
|
24
|
+
This turn's host-private in-flight draft.
|
|
25
|
+
send --body <text> --turn-execution-id <id> (--reply-to <message-id> | --mention <user-id>)...
|
|
26
|
+
Post a short auxiliary message. It must address someone.
|
|
27
|
+
mention <user-id> --body <text> --turn-execution-id <id> [--reply-to <message-id>]
|
|
28
|
+
Post a short auxiliary message addressed to one participant.
|
|
29
|
+
task list
|
|
30
|
+
Tasks in this channel. Parent channel only.
|
|
31
|
+
task create --title <text> [--description <text>] [--assignee-id <user-id>]
|
|
32
|
+
Create a task. Parent channel only.
|
|
33
|
+
task get [<task-id>]
|
|
34
|
+
One task with its properties.
|
|
35
|
+
task update [<task-id>] [--status <open|in_progress|in_review|done|cancelled>] [--title <text>] [--description <text>] [--assignee-id <user-id>]
|
|
36
|
+
Update the task. At least one field is required.
|
|
37
|
+
task history <task-id> [--limit <n>] [--before <n>] [--after <n>]
|
|
38
|
+
Messages in that task's thread.
|
|
39
|
+
task set-property [<task-id>] --key <key> --value <value>
|
|
40
|
+
Set one task property.
|
|
41
|
+
task delete-property [<task-id>] --key <key>
|
|
42
|
+
Remove one task property.
|
|
43
|
+
|
|
44
|
+
Inside a task thread every task command with an optional task id acts on this thread's own task when the id is omitted, and task list / task create are refused there. In a parent channel the task id is required.
|
|
45
|
+
|
|
46
|
+
Exit codes: 0 success, 1 gateway failure, 2 usage error.
|
|
47
|
+
|
|
48
|
+
The full manual, including the task property registry and the error table, is SKILL.md in this skill directory.`;
|
|
49
|
+
|
|
50
|
+
// A window value outside the IEEE-754 safe integer range does not survive this CLI: it parses the
|
|
51
|
+
// value to a number and re-stringifies it into the query string, so `1000000000000000000000`
|
|
52
|
+
// leaves as `1e+21` and the gateway's `Number.parseInt(value, 10)` reads it back as `1` — a
|
|
53
|
+
// different page, silently. Both packaged CLIs accept exactly this grammar.
|
|
54
|
+
const INTEGER_ARGUMENT_PATTERN = /^[+-]?[0-9]+$/;
|
|
55
|
+
|
|
56
|
+
// Spelled out instead of `\s` because the Python CLI must trim arguments and append the mention
|
|
57
|
+
// separator on exactly the same inputs, and the two languages disagree on U+00A0, U+3000, U+FEFF
|
|
58
|
+
// and U+001C…U+001F.
|
|
59
|
+
const PORTABLE_WHITESPACE = ' \t\n\r\f\v';
|
|
60
|
+
|
|
61
|
+
function trimArgument(value) {
|
|
62
|
+
let start = 0;
|
|
63
|
+
let end = value.length;
|
|
64
|
+
while (start < end && PORTABLE_WHITESPACE.includes(value[start])) {
|
|
65
|
+
start += 1;
|
|
66
|
+
}
|
|
67
|
+
while (end > start && PORTABLE_WHITESPACE.includes(value[end - 1])) {
|
|
68
|
+
end -= 1;
|
|
69
|
+
}
|
|
70
|
+
return value.slice(start, end);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const COMMANDS = new Set([
|
|
74
|
+
'health',
|
|
75
|
+
'bootstrap',
|
|
76
|
+
'whoami',
|
|
77
|
+
'history',
|
|
78
|
+
'users',
|
|
79
|
+
'draft',
|
|
80
|
+
'send',
|
|
81
|
+
'mention',
|
|
82
|
+
'task',
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const TASK_COMMANDS = new Set([
|
|
86
|
+
'list',
|
|
87
|
+
'create',
|
|
88
|
+
'get',
|
|
89
|
+
'update',
|
|
90
|
+
'history',
|
|
91
|
+
'set-property',
|
|
92
|
+
'delete-property',
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
class UsageError extends Error {}
|
|
96
|
+
|
|
97
|
+
function isNonEmptyString(value) {
|
|
98
|
+
return typeof value === 'string' && trimArgument(value).length > 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseArgs(argv) {
|
|
102
|
+
const positionals = [];
|
|
103
|
+
const mentions = [];
|
|
104
|
+
let gatewayCredentialPath;
|
|
105
|
+
let turnExecutionId;
|
|
106
|
+
let limit;
|
|
107
|
+
let before;
|
|
108
|
+
let after;
|
|
109
|
+
let body;
|
|
110
|
+
let replyToId;
|
|
111
|
+
let title;
|
|
112
|
+
let description;
|
|
113
|
+
let assigneeId;
|
|
114
|
+
let status;
|
|
115
|
+
let propertyKey;
|
|
116
|
+
let propertyValue;
|
|
117
|
+
let index = 0;
|
|
118
|
+
|
|
119
|
+
const takeValue = (flag) => {
|
|
120
|
+
index += 1;
|
|
121
|
+
if (index >= argv.length) {
|
|
122
|
+
throw new UsageError(`Missing value after ${flag}`);
|
|
123
|
+
}
|
|
124
|
+
return argv[index];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const takeInteger = (flag) => {
|
|
128
|
+
const value = takeValue(flag);
|
|
129
|
+
if (!INTEGER_ARGUMENT_PATTERN.test(value)) {
|
|
130
|
+
throw new UsageError(`Invalid integer for ${flag}: ${value}`);
|
|
131
|
+
}
|
|
132
|
+
const parsed = Number(value);
|
|
133
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
134
|
+
throw new UsageError(`Integer out of range for ${flag}: ${value}`);
|
|
135
|
+
}
|
|
136
|
+
return parsed;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
for (; index < argv.length; index += 1) {
|
|
140
|
+
const arg = argv[index];
|
|
141
|
+
if (arg === '--help' || arg === '-h') {
|
|
142
|
+
return { help: true };
|
|
143
|
+
}
|
|
144
|
+
if (!arg.startsWith('-')) {
|
|
145
|
+
positionals.push(arg);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
switch (arg) {
|
|
149
|
+
case '--gateway':
|
|
150
|
+
gatewayCredentialPath = takeValue(arg);
|
|
151
|
+
break;
|
|
152
|
+
case '--turn-execution-id':
|
|
153
|
+
turnExecutionId = trimArgument(takeValue(arg));
|
|
154
|
+
break;
|
|
155
|
+
case '--limit':
|
|
156
|
+
limit = takeInteger(arg);
|
|
157
|
+
break;
|
|
158
|
+
case '--before':
|
|
159
|
+
before = takeInteger(arg);
|
|
160
|
+
break;
|
|
161
|
+
case '--after':
|
|
162
|
+
after = takeInteger(arg);
|
|
163
|
+
break;
|
|
164
|
+
case '--body':
|
|
165
|
+
body = takeValue(arg);
|
|
166
|
+
break;
|
|
167
|
+
case '--reply-to':
|
|
168
|
+
replyToId = takeValue(arg);
|
|
169
|
+
break;
|
|
170
|
+
case '--mention': {
|
|
171
|
+
const mention = trimArgument(takeValue(arg));
|
|
172
|
+
if (mention.length === 0) {
|
|
173
|
+
throw new UsageError('Missing value after --mention');
|
|
174
|
+
}
|
|
175
|
+
mentions.push(mention);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case '--title':
|
|
179
|
+
title = takeValue(arg);
|
|
180
|
+
break;
|
|
181
|
+
case '--description':
|
|
182
|
+
description = takeValue(arg);
|
|
183
|
+
break;
|
|
184
|
+
case '--assignee-id':
|
|
185
|
+
assigneeId = takeValue(arg);
|
|
186
|
+
break;
|
|
187
|
+
case '--status':
|
|
188
|
+
status = takeValue(arg);
|
|
189
|
+
break;
|
|
190
|
+
case '--key':
|
|
191
|
+
propertyKey = trimArgument(takeValue(arg));
|
|
192
|
+
break;
|
|
193
|
+
case '--value':
|
|
194
|
+
propertyValue = takeValue(arg);
|
|
195
|
+
break;
|
|
196
|
+
default:
|
|
197
|
+
throw new UsageError(`Unknown argument: ${arg}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (positionals.length === 0) {
|
|
202
|
+
return { help: false, command: null };
|
|
203
|
+
}
|
|
204
|
+
const command = positionals[0];
|
|
205
|
+
if (!COMMANDS.has(command)) {
|
|
206
|
+
throw new UsageError(`Unknown command: ${command}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let taskCommand = null;
|
|
210
|
+
let taskId = null;
|
|
211
|
+
let mentionTargetId = null;
|
|
212
|
+
let consumed = 1;
|
|
213
|
+
if (command === 'task') {
|
|
214
|
+
if (positionals.length < 2) {
|
|
215
|
+
throw new UsageError(
|
|
216
|
+
'Missing task command; expected one of list, create, get, update, history, set-property, delete-property',
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
taskCommand = positionals[1];
|
|
220
|
+
if (!TASK_COMMANDS.has(taskCommand)) {
|
|
221
|
+
throw new UsageError(`Unknown task command: ${taskCommand}`);
|
|
222
|
+
}
|
|
223
|
+
consumed = 2;
|
|
224
|
+
if (taskCommand !== 'list' && taskCommand !== 'create' && positionals.length > consumed) {
|
|
225
|
+
taskId = trimArgument(positionals[consumed]);
|
|
226
|
+
// Only an absent positional may mean the thread's own task. Treating an empty one as absent
|
|
227
|
+
// would send a write the caller meant for a named task to whatever task the thread resolves.
|
|
228
|
+
if (taskId.length === 0) {
|
|
229
|
+
throw new UsageError(
|
|
230
|
+
`Empty task id for "task ${taskCommand}"; pass a task id or omit the argument entirely`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
consumed += 1;
|
|
234
|
+
}
|
|
235
|
+
} else if (command === 'mention' && positionals.length > consumed) {
|
|
236
|
+
mentionTargetId = trimArgument(positionals[consumed]);
|
|
237
|
+
consumed += 1;
|
|
238
|
+
}
|
|
239
|
+
if (positionals.length > consumed) {
|
|
240
|
+
throw new UsageError(`Unexpected argument: ${positionals[consumed]}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (gatewayCredentialPath === undefined) {
|
|
244
|
+
throw new UsageError('Missing required --gateway <path> argument');
|
|
245
|
+
}
|
|
246
|
+
if (!isAbsolute(gatewayCredentialPath)) {
|
|
247
|
+
throw new UsageError(`--gateway requires an absolute path: ${gatewayCredentialPath}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
help: false,
|
|
252
|
+
command,
|
|
253
|
+
taskCommand,
|
|
254
|
+
taskId,
|
|
255
|
+
mentionTargetId,
|
|
256
|
+
gatewayCredentialPath,
|
|
257
|
+
turnExecutionId,
|
|
258
|
+
limit,
|
|
259
|
+
before,
|
|
260
|
+
after,
|
|
261
|
+
body,
|
|
262
|
+
replyToId,
|
|
263
|
+
mentions,
|
|
264
|
+
title,
|
|
265
|
+
description,
|
|
266
|
+
assigneeId,
|
|
267
|
+
status,
|
|
268
|
+
propertyKey,
|
|
269
|
+
propertyValue,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function commandLabel(parsed) {
|
|
274
|
+
return parsed.command === 'task' ? `task ${parsed.taskCommand}` : parsed.command;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function requireText(value, flag, label) {
|
|
278
|
+
if (!isNonEmptyString(value)) {
|
|
279
|
+
throw new UsageError(`Missing required ${flag} <value> for "${label}"`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function validateCommand(parsed) {
|
|
284
|
+
const label = commandLabel(parsed);
|
|
285
|
+
if (parsed.command === 'draft') {
|
|
286
|
+
requireText(parsed.turnExecutionId, '--turn-execution-id', label);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (parsed.command === 'send') {
|
|
290
|
+
requireText(parsed.body, '--body', label);
|
|
291
|
+
requireText(parsed.turnExecutionId, '--turn-execution-id', label);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (parsed.command === 'mention') {
|
|
295
|
+
if (!isNonEmptyString(parsed.mentionTargetId)) {
|
|
296
|
+
throw new UsageError('Missing required user id for "mention"');
|
|
297
|
+
}
|
|
298
|
+
if (parsed.mentions.length > 0) {
|
|
299
|
+
throw new UsageError('Do not combine the mention command with --mention flags');
|
|
300
|
+
}
|
|
301
|
+
requireText(parsed.body, '--body', label);
|
|
302
|
+
requireText(parsed.turnExecutionId, '--turn-execution-id', label);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (parsed.command !== 'task') {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (parsed.taskCommand === 'create') {
|
|
309
|
+
requireText(parsed.title, '--title', label);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (parsed.taskCommand === 'update') {
|
|
313
|
+
const updated = [parsed.status, parsed.title, parsed.description, parsed.assigneeId];
|
|
314
|
+
if (updated.every((value) => value === undefined)) {
|
|
315
|
+
throw new UsageError(
|
|
316
|
+
'At least one of --status, --title, --description or --assignee-id is required for "task update"',
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (parsed.taskCommand === 'history') {
|
|
322
|
+
if (parsed.taskId === null) {
|
|
323
|
+
throw new UsageError('Missing required task id for "task history"');
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (parsed.taskCommand === 'set-property') {
|
|
328
|
+
requireText(parsed.propertyKey, '--key', label);
|
|
329
|
+
// An empty property value is a legitimate string, so presence of the flag is the requirement.
|
|
330
|
+
if (parsed.propertyValue === undefined) {
|
|
331
|
+
throw new UsageError('Missing required --value <value> for "task set-property"');
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (parsed.taskCommand === 'delete-property') {
|
|
336
|
+
requireText(parsed.propertyKey, '--key', label);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// The credential path is used exactly as handed over: no walk-up, no glob, no environment lookup.
|
|
341
|
+
// Task workspaces live inside the git repository and are not gitignored, so any search would
|
|
342
|
+
// traverse attacker-influenceable directories where a planted file could redirect the gateway base
|
|
343
|
+
// URL or point at another channel's credentials.
|
|
344
|
+
async function readGatewayCredential(gatewayCredentialPath) {
|
|
345
|
+
let bytes;
|
|
346
|
+
try {
|
|
347
|
+
bytes = await readFile(gatewayCredentialPath);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (error.code === 'ENOENT') {
|
|
350
|
+
throw new UsageError(`Gateway credential file not found: ${gatewayCredentialPath}`);
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
let payload;
|
|
355
|
+
try {
|
|
356
|
+
// Decoded strictly: a lone replacement character would silently mangle the channel id the
|
|
357
|
+
// request is issued against, and the Python CLI refuses the same bytes.
|
|
358
|
+
payload = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
359
|
+
} catch {
|
|
360
|
+
throw new UsageError(`${gatewayCredentialPath} is not a Borgee gateway credential`);
|
|
361
|
+
}
|
|
362
|
+
const channelId = payload?.channelId;
|
|
363
|
+
const baseUrl = payload?.localhostGateway?.baseUrl;
|
|
364
|
+
const token = payload?.localhostGateway?.token;
|
|
365
|
+
if (!isNonEmptyString(channelId) || !isNonEmptyString(baseUrl) || !isNonEmptyString(token)) {
|
|
366
|
+
throw new UsageError(`${gatewayCredentialPath} is not a Borgee gateway credential`);
|
|
367
|
+
}
|
|
368
|
+
return { channelId, baseUrl: baseUrl.replace(/\/+$/u, ''), token };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function ensureVisibleMention(body, participantId) {
|
|
372
|
+
if (body.includes(`<@${participantId}>`)) {
|
|
373
|
+
return body;
|
|
374
|
+
}
|
|
375
|
+
const lastCharacter = body.slice(-1);
|
|
376
|
+
const separator = PORTABLE_WHITESPACE.includes(lastCharacter) ? '' : ' ';
|
|
377
|
+
return `${body}${separator}<@${participantId}>`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function ensureVisibleMentions(body, participantIds) {
|
|
381
|
+
return participantIds.reduce(
|
|
382
|
+
(nextBody, participantId) => ensureVisibleMention(nextBody, participantId),
|
|
383
|
+
body,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function buildQuery(entries) {
|
|
388
|
+
const parts = entries
|
|
389
|
+
.filter(([, value]) => value !== undefined && value !== null)
|
|
390
|
+
.map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`);
|
|
391
|
+
return parts.length > 0 ? `?${parts.join('&')}` : '';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function historyQuery(parsed) {
|
|
395
|
+
return buildQuery([
|
|
396
|
+
['limit', parsed.limit],
|
|
397
|
+
['before', parsed.before],
|
|
398
|
+
['after', parsed.after],
|
|
399
|
+
]);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function buildMessageBody(parsed) {
|
|
403
|
+
const mentions = parsed.command === 'mention' ? [parsed.mentionTargetId] : parsed.mentions;
|
|
404
|
+
const requestBody = {
|
|
405
|
+
body: mentions.length > 0 ? ensureVisibleMentions(parsed.body, mentions) : parsed.body,
|
|
406
|
+
turnExecutionId: parsed.turnExecutionId,
|
|
407
|
+
};
|
|
408
|
+
const replyToId = typeof parsed.replyToId === 'string' ? trimArgument(parsed.replyToId) : '';
|
|
409
|
+
if (replyToId.length > 0) {
|
|
410
|
+
requestBody.replyToId = replyToId;
|
|
411
|
+
}
|
|
412
|
+
return requestBody;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function buildTaskCreateBody(parsed) {
|
|
416
|
+
return {
|
|
417
|
+
title: parsed.title,
|
|
418
|
+
...(parsed.description !== undefined ? { description: parsed.description } : {}),
|
|
419
|
+
...(parsed.assigneeId !== undefined ? { assigneeId: parsed.assigneeId } : {}),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function buildTaskUpdateBody(parsed) {
|
|
424
|
+
return {
|
|
425
|
+
...(parsed.status !== undefined ? { status: parsed.status } : {}),
|
|
426
|
+
...(parsed.title !== undefined ? { title: parsed.title } : {}),
|
|
427
|
+
...(parsed.description !== undefined ? { description: parsed.description } : {}),
|
|
428
|
+
...(parsed.assigneeId !== undefined ? { assigneeId: parsed.assigneeId } : {}),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Which task a bare `task` verb addresses is the gateway's decision, taken from the token binding:
|
|
433
|
+
// in a task thread `current-task` resolves that thread's task, and in a parent channel it answers
|
|
434
|
+
// not_found. The CLI therefore never inspects the turn's task assignment state.
|
|
435
|
+
function resolveTaskRequest(parsed, baseUrl, channel) {
|
|
436
|
+
if (parsed.taskCommand === 'list') {
|
|
437
|
+
return { url: `${baseUrl}/v1/channels/${channel}/tasks`, method: 'GET', body: null };
|
|
438
|
+
}
|
|
439
|
+
if (parsed.taskCommand === 'create') {
|
|
440
|
+
return {
|
|
441
|
+
url: `${baseUrl}/v1/channels/${channel}/tasks`,
|
|
442
|
+
method: 'POST',
|
|
443
|
+
body: buildTaskCreateBody(parsed),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (parsed.taskCommand === 'history') {
|
|
447
|
+
return {
|
|
448
|
+
url: `${baseUrl}/v1/tasks/${encodeURIComponent(parsed.taskId)}/history${historyQuery(parsed)}`,
|
|
449
|
+
method: 'GET',
|
|
450
|
+
body: null,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const taskUrl =
|
|
454
|
+
parsed.taskId === null
|
|
455
|
+
? `${baseUrl}/v1/channels/${channel}/current-task`
|
|
456
|
+
: `${baseUrl}/v1/tasks/${encodeURIComponent(parsed.taskId)}`;
|
|
457
|
+
if (parsed.taskCommand === 'get') {
|
|
458
|
+
return { url: taskUrl, method: 'GET', body: null };
|
|
459
|
+
}
|
|
460
|
+
if (parsed.taskCommand === 'update') {
|
|
461
|
+
return { url: taskUrl, method: 'PATCH', body: buildTaskUpdateBody(parsed) };
|
|
462
|
+
}
|
|
463
|
+
const propertyUrl = `${taskUrl}/properties/${encodeURIComponent(parsed.propertyKey)}`;
|
|
464
|
+
if (parsed.taskCommand === 'set-property') {
|
|
465
|
+
return { url: propertyUrl, method: 'PUT', body: { value: parsed.propertyValue } };
|
|
466
|
+
}
|
|
467
|
+
return { url: propertyUrl, method: 'DELETE', body: null };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function resolveRequest(credential, parsed) {
|
|
471
|
+
const baseUrl = credential.baseUrl;
|
|
472
|
+
const channel = encodeURIComponent(credential.channelId);
|
|
473
|
+
if (parsed.command === 'health') {
|
|
474
|
+
return { url: `${baseUrl}/health`, method: 'GET', body: null };
|
|
475
|
+
}
|
|
476
|
+
if (parsed.command === 'bootstrap') {
|
|
477
|
+
return { url: `${baseUrl}/v1/channels/${channel}/bootstrap`, method: 'GET', body: null };
|
|
478
|
+
}
|
|
479
|
+
if (parsed.command === 'whoami') {
|
|
480
|
+
return { url: `${baseUrl}/v1/channels/${channel}/me`, method: 'GET', body: null };
|
|
481
|
+
}
|
|
482
|
+
if (parsed.command === 'history') {
|
|
483
|
+
return {
|
|
484
|
+
url: `${baseUrl}/v1/channels/${channel}/history${historyQuery(parsed)}`,
|
|
485
|
+
method: 'GET',
|
|
486
|
+
body: null,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (parsed.command === 'users') {
|
|
490
|
+
return { url: `${baseUrl}/v1/channels/${channel}/users`, method: 'GET', body: null };
|
|
491
|
+
}
|
|
492
|
+
if (parsed.command === 'draft') {
|
|
493
|
+
const query = buildQuery([['turnExecutionId', parsed.turnExecutionId]]);
|
|
494
|
+
return { url: `${baseUrl}/v1/channels/${channel}/draft${query}`, method: 'GET', body: null };
|
|
495
|
+
}
|
|
496
|
+
if (parsed.command === 'send' || parsed.command === 'mention') {
|
|
497
|
+
return {
|
|
498
|
+
url: `${baseUrl}/v1/channels/${channel}/messages`,
|
|
499
|
+
method: 'POST',
|
|
500
|
+
body: buildMessageBody(parsed),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
return resolveTaskRequest(parsed, baseUrl, channel);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function callGateway(credential, request) {
|
|
507
|
+
const headers = { authorization: `Bearer ${credential.token}` };
|
|
508
|
+
// The gateway has no redirect routes, so a 3xx means something other than the gateway answered.
|
|
509
|
+
// Following it would carry the channel token off the loopback address, so it is reported instead.
|
|
510
|
+
const httpRequest = { method: request.method, headers, redirect: 'manual' };
|
|
511
|
+
if (request.body !== null) {
|
|
512
|
+
headers['content-type'] = 'application/json; charset=utf-8';
|
|
513
|
+
httpRequest.body = JSON.stringify(request.body);
|
|
514
|
+
}
|
|
515
|
+
let response;
|
|
516
|
+
try {
|
|
517
|
+
response = await fetch(request.url, httpRequest);
|
|
518
|
+
} catch (error) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
`Cannot reach the localhost gateway at ${credential.baseUrl}: ${error.cause?.message ?? error.message}`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
const bodyText = await response.text();
|
|
524
|
+
const responseBody = bodyText.length > 0 ? JSON.parse(bodyText) : null;
|
|
525
|
+
if (!response.ok) {
|
|
526
|
+
throw new Error(`Gateway request failed with ${response.status}: ${JSON.stringify(responseBody)}`);
|
|
527
|
+
}
|
|
528
|
+
return responseBody;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function main() {
|
|
532
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
533
|
+
if (parsed.help === true) {
|
|
534
|
+
process.stdout.write(`${MANUAL}\n`);
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
if (parsed.command === null) {
|
|
538
|
+
process.stderr.write(`${MANUAL}\n`);
|
|
539
|
+
return 2;
|
|
540
|
+
}
|
|
541
|
+
validateCommand(parsed);
|
|
542
|
+
const credential = await readGatewayCredential(parsed.gatewayCredentialPath);
|
|
543
|
+
const result = await callGateway(credential, resolveRequest(credential, parsed));
|
|
544
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
process.exitCode = await main();
|
|
550
|
+
} catch (error) {
|
|
551
|
+
process.stderr.write(`error: ${error.message}\n`);
|
|
552
|
+
process.exitCode = error instanceof UsageError ? 2 : 1;
|
|
553
|
+
}
|