@borgee/agents-host 0.2.97 → 0.2.101
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/agents-host.d.ts +8 -0
- package/dist/agents-host.js +258 -64
- package/dist/chat/chat-control-plane.d.ts +13 -3
- package/dist/chat/sdk-chat-control-plane.d.ts +30 -5
- package/dist/chat/sdk-chat-control-plane.js +131 -3
- package/dist/context/skill-manual.js +1 -1
- package/dist/execution-telemetry.d.ts +96 -0
- package/dist/execution-telemetry.js +583 -0
- package/dist/gateway/channel-file-workspace.d.ts +15 -0
- package/dist/gateway/channel-file-workspace.js +130 -0
- package/dist/gateway/localhost-gateway.js +140 -0
- package/dist/plugin-sdk.js +72 -3
- package/dist/plugin-sdk.js.map +2 -2
- package/dist/policy/gateway-authorization.d.ts +1 -1
- package/dist/policy/gateway-authorization.js +22 -2
- package/dist/providers/claude/adapter.js +1 -0
- package/dist/providers/claude/cli-client.js +4 -0
- package/dist/providers/codex/adapter.js +1 -0
- package/dist/providers/codex/cli-client.js +5 -0
- package/dist/providers/copilot/adapter.js +4 -0
- package/dist/providers/copilot/cli-client.js +11 -1
- package/dist/providers/copilot/sdk-session.d.ts +12 -3
- package/dist/providers/copilot/sdk-session.js +222 -5
- package/dist/providers/create-provider.js +4 -0
- package/dist/providers/prompt-usage.d.ts +8 -0
- package/dist/providers/prompt-usage.js +52 -0
- package/dist/state-paths.d.ts +2 -0
- package/dist/state-paths.js +6 -0
- package/dist/types.d.ts +41 -0
- package/dist/typing-lease.d.ts +14 -0
- package/dist/typing-lease.js +31 -0
- package/package.json +2 -2
- package/skills/borgee-agent/SKILL.md +12 -4
- package/skills/borgee-agent/scripts/borgee-agent.mjs +89 -0
- package/skills/borgee-agent/scripts/borgee-agent.py +90 -0
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExecutionGrant } from './plugin-sdk.js';
|
|
1
2
|
export type ProviderKind = 'claude' | 'codex' | 'copilot';
|
|
2
3
|
export type ProjectionStrategy = 'session-brief' | 'turn-full' | 'message-only' | 'turn-thin' | 'file-brief';
|
|
3
4
|
export interface ProviderCommandConfig {
|
|
@@ -111,6 +112,8 @@ export interface ChannelMessageEvent {
|
|
|
111
112
|
content_type?: string;
|
|
112
113
|
attachments?: HostedMessageAttachment[];
|
|
113
114
|
created_at?: number;
|
|
115
|
+
/** Server-issued capability used only by the execution telemetry rail. */
|
|
116
|
+
execution_grant?: ExecutionGrant;
|
|
114
117
|
[key: string]: unknown;
|
|
115
118
|
}
|
|
116
119
|
export interface MeResponseUser {
|
|
@@ -135,6 +138,36 @@ export interface ChannelHistoryEntry {
|
|
|
135
138
|
createdAt: number;
|
|
136
139
|
editedAt?: number;
|
|
137
140
|
}
|
|
141
|
+
export interface ChannelFileEntry {
|
|
142
|
+
id: string;
|
|
143
|
+
ownerUserId: string;
|
|
144
|
+
channelId: string;
|
|
145
|
+
path: string;
|
|
146
|
+
parentPath: string;
|
|
147
|
+
name: string;
|
|
148
|
+
isDirectory: boolean;
|
|
149
|
+
mimeType: string;
|
|
150
|
+
sizeBytes: number;
|
|
151
|
+
source: string;
|
|
152
|
+
sourceMessageId: string;
|
|
153
|
+
createdAt: number;
|
|
154
|
+
updatedAt: number;
|
|
155
|
+
}
|
|
156
|
+
export interface ChannelFileContent {
|
|
157
|
+
path: string;
|
|
158
|
+
filename: string;
|
|
159
|
+
contentType: string;
|
|
160
|
+
sizeBytes: number;
|
|
161
|
+
base64?: string;
|
|
162
|
+
text?: string;
|
|
163
|
+
}
|
|
164
|
+
export interface PublishChannelFileInput {
|
|
165
|
+
channelId: string;
|
|
166
|
+
path: string;
|
|
167
|
+
contentType: string;
|
|
168
|
+
base64: string;
|
|
169
|
+
overwrite: boolean;
|
|
170
|
+
}
|
|
138
171
|
export interface HostedMessageAttachment {
|
|
139
172
|
url: string;
|
|
140
173
|
filename?: string;
|
|
@@ -454,6 +487,12 @@ export interface ProviderReply {
|
|
|
454
487
|
*/
|
|
455
488
|
sessionId?: string;
|
|
456
489
|
}
|
|
490
|
+
export interface ProviderTokenUsage {
|
|
491
|
+
inputTokens: number;
|
|
492
|
+
outputTokens: number;
|
|
493
|
+
cacheReadTokens: number;
|
|
494
|
+
cacheWriteTokens: number;
|
|
495
|
+
}
|
|
457
496
|
export interface PostedMessage {
|
|
458
497
|
messageId: string;
|
|
459
498
|
}
|
|
@@ -588,4 +627,6 @@ export type ProviderProgressUpdate = {
|
|
|
588
627
|
};
|
|
589
628
|
export interface ProviderGenerateOptions {
|
|
590
629
|
onProgress?: (update: ProviderProgressUpdate) => void;
|
|
630
|
+
onUsage?: (usage: ProviderTokenUsage) => void;
|
|
631
|
+
onExecutionContinuation?: (completion: Promise<boolean>) => void;
|
|
591
632
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TypingLease {
|
|
2
|
+
renew(): void;
|
|
3
|
+
stop(): void;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Keeps typing truthful without making provider completion its only release.
|
|
7
|
+
*
|
|
8
|
+
* A lease reports one pulse immediately. Visible answer updates may renew it
|
|
9
|
+
* at most once per interval; nothing is scheduled between updates. Browser
|
|
10
|
+
* expiry bounds every pulse, so a provider turn can keep running tools or
|
|
11
|
+
* background work while the activity rail says "working" without a hidden
|
|
12
|
+
* periodic heartbeat surviving indefinitely.
|
|
13
|
+
*/
|
|
14
|
+
export declare function startTypingLease(reportTyping: () => void, now?: () => number): TypingLease;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const TYPING_PULSE_INTERVAL_MS = 2_000;
|
|
2
|
+
/**
|
|
3
|
+
* Keeps typing truthful without making provider completion its only release.
|
|
4
|
+
*
|
|
5
|
+
* A lease reports one pulse immediately. Visible answer updates may renew it
|
|
6
|
+
* at most once per interval; nothing is scheduled between updates. Browser
|
|
7
|
+
* expiry bounds every pulse, so a provider turn can keep running tools or
|
|
8
|
+
* background work while the activity rail says "working" without a hidden
|
|
9
|
+
* periodic heartbeat surviving indefinitely.
|
|
10
|
+
*/
|
|
11
|
+
export function startTypingLease(reportTyping, now = () => performance.now()) {
|
|
12
|
+
let closed = false;
|
|
13
|
+
let lastPulseAt = now();
|
|
14
|
+
reportTyping();
|
|
15
|
+
return {
|
|
16
|
+
renew: () => {
|
|
17
|
+
if (closed) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const currentTime = now();
|
|
21
|
+
if (currentTime - lastPulseAt < TYPING_PULSE_INTERVAL_MS) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
lastPulseAt = currentTime;
|
|
25
|
+
reportTyping();
|
|
26
|
+
},
|
|
27
|
+
stop: () => {
|
|
28
|
+
closed = true;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@borgee/agents-host",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.101",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"tsx": "^4.20.5",
|
|
39
39
|
"typescript": "^5.9.3",
|
|
40
40
|
"vitest": "^4.1.5",
|
|
41
|
-
"@borgee/plugin-sdk": "0.15.
|
|
41
|
+
"@borgee/plugin-sdk": "0.15.3"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"predev": "pnpm --filter @borgee/agent-remote-protocol build && pnpm --filter @borgee/plugin-sdk build && pnpm run vendor:claude",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: borgee-agent
|
|
3
|
-
description: Read the Borgee channel this turn is running in and act on its tasks — channel history, visible participants, task list/create/get/update, task properties, and short auxiliary mentions — through the packaged local CLI. Use when you need to know what was said in this channel, who is here, or what the current task is, and when you need to record task state or ping another participant.
|
|
3
|
+
description: Read the Borgee channel this turn is running in and act on its tasks — channel history, channel files, visible participants, task list/create/get/update, task properties, and short auxiliary mentions — through the packaged local CLI. Use when you need to know what was said in this channel, which files it already holds, who is here, or what the current task is, and when you need to record task state or ping another participant.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Borgee channel agent
|
|
7
7
|
|
|
8
|
-
This turn is running as an agent inside a Borgee channel. The packaged CLI is the only way to see that channel and
|
|
8
|
+
This turn is running as an agent inside a Borgee channel. The packaged CLI is the only way to see that channel, list its files, and act on its tasks: it talks to a loopback-only gateway that is already authorized for this channel and this turn. This file is the whole manual — the invocation, every command, what is reachable when, and the limits.
|
|
9
9
|
|
|
10
10
|
## Authorization
|
|
11
11
|
|
|
@@ -52,6 +52,11 @@ Everything below writes that leading interpreter and script path as `borgee-agen
|
|
|
52
52
|
| `bootstrap` | The channel context the host published for this turn: the channel id, whether collaboration is live, and inside a task thread the current task id. The one command whose answer you cannot predict. |
|
|
53
53
|
| `whoami` | This agent's own id and display name. |
|
|
54
54
|
| `history [--limit <n>] [--before <n>] [--after <n>]` | Recent messages in this channel. At most 20 per call — a larger `--limit` is silently reduced to 20, so page with `--before` rather than asking for more. `--before` and `--after` are `createdAt` epoch-millisecond cursors copied from a message you already hold, not message ids and not counts. |
|
|
55
|
+
| `files` | Files visible in this channel, returned as a flattened list with stable root-relative paths and basic metadata. |
|
|
56
|
+
| `files cat <channel-path>` | Print one UTF-8 text file. The file must have a supported text MIME type and be at most 64 KiB. |
|
|
57
|
+
| `files download <channel-path> [--overwrite]` | Copy one channel file to `<execution.local_directory>/.borgee/channel-files/<channel-path>`. The default refuses an existing working copy. |
|
|
58
|
+
| `files sync [<channel-path>] [--overwrite]` | Copy every visible file, or one exact path/subtree, into the same fixed working-copy directory. File bytes never appear in command output. |
|
|
59
|
+
| `files publish <local-relative-path> --channel-path <channel-path> [--content-type <mime>] [--overwrite]` | Explicitly publish one regular file under `execution.local_directory`. The default is create-only; `--overwrite` is required to replace an existing channel file. |
|
|
55
60
|
| `users` | Every participant you share a channel, DM or thread with. That is a superset of this channel's members, not its roster. |
|
|
56
61
|
| `draft --turn-execution-id <id>` | This turn's host-private in-flight draft. It exists only once this turn has produced visible reply text, and answers `not_found` before that. |
|
|
57
62
|
| `send --body <text> --turn-execution-id <id> (--reply-to <message-id> \| --mention <user-id>)...` | Post a short auxiliary message. It must address someone. |
|
|
@@ -70,7 +75,7 @@ The keys `task set-property` and `task delete-property` accept are a closed set,
|
|
|
70
75
|
|
|
71
76
|
## What is available on a turn
|
|
72
77
|
|
|
73
|
-
`health`, `bootstrap`, `whoami`, `history`, `users` and the task commands are live on every turn whose prompt names a gateway credential file.
|
|
78
|
+
`health`, `bootstrap`, `whoami`, `history`, `files`, `users` and the task commands are live on every turn whose prompt names a gateway credential file. File listing and `files cat` work without a task execution directory. Download, sync and publish require a task execution target with an absolute `execution.local_directory`.
|
|
74
79
|
|
|
75
80
|
`draft`, `send` and `mention` are live only where collaboration is enabled; the prompt says when they are not, and the gateway answers `not_found` for all three. They additionally need the turn execution id the prompt carries: no command returns that id and the gateway credential file does not hold it.
|
|
76
81
|
|
|
@@ -137,7 +142,10 @@ $ borgee-agent --gateway /state/channels/channel-a/.borgee-agent-gateway.json \
|
|
|
137
142
|
## Constraints
|
|
138
143
|
|
|
139
144
|
- Local only. The CLI reaches the loopback gateway and nothing else.
|
|
140
|
-
-
|
|
145
|
+
- File transfer accepts only portable relative paths. Absolute paths, `..`, backslashes, symbolic-link traversal, non-regular publish sources and files larger than 10 MiB are rejected.
|
|
146
|
+
- Download and sync write only below `<execution.local_directory>/.borgee/channel-files`; publish reads only below `execution.local_directory`.
|
|
147
|
+
- Channel files are shared inputs and the fixed local directory is a working copy. Local changes are not shared until an explicit `files publish`.
|
|
148
|
+
- Published files are classified as explicit Agent publishes. Publish is create-only unless `--overwrite` is explicit; it cannot write arbitrary shared storage.
|
|
141
149
|
- Auxiliary sends are short notices — one line, at most twelve words counting the `<@id>` the CLI appends, addressed to someone or attached to a message. They are not a place for the answer.
|
|
142
150
|
- Exactly one auxiliary send per turn, whoever it addresses. A send the gateway rejects does not spend it.
|
|
143
151
|
- The draft is host-private. Read it to see what the host is about to post; never re-post it.
|
|
@@ -18,6 +18,16 @@ Commands:
|
|
|
18
18
|
This agent's identity in this channel.
|
|
19
19
|
history [--limit <n>] [--before <n>] [--after <n>]
|
|
20
20
|
Recent messages in this channel.
|
|
21
|
+
files
|
|
22
|
+
Files visible in this channel.
|
|
23
|
+
files cat <channel-path>
|
|
24
|
+
Print one UTF-8 text file up to 64 KiB.
|
|
25
|
+
files download <channel-path> [--overwrite]
|
|
26
|
+
Copy one file into execution.local_directory/.borgee/channel-files.
|
|
27
|
+
files sync [<channel-path>] [--overwrite]
|
|
28
|
+
Copy all files, or one subtree, into the fixed working-copy directory.
|
|
29
|
+
files publish <local-relative-path> --channel-path <channel-path> [--content-type <mime>] [--overwrite]
|
|
30
|
+
Explicitly publish one execution.local_directory file. Existing channel files are preserved unless --overwrite is present.
|
|
21
31
|
users
|
|
22
32
|
Participants visible in this channel.
|
|
23
33
|
draft --turn-execution-id <id>
|
|
@@ -75,6 +85,7 @@ const COMMANDS = new Set([
|
|
|
75
85
|
'bootstrap',
|
|
76
86
|
'whoami',
|
|
77
87
|
'history',
|
|
88
|
+
'files',
|
|
78
89
|
'users',
|
|
79
90
|
'draft',
|
|
80
91
|
'send',
|
|
@@ -91,6 +102,7 @@ const TASK_COMMANDS = new Set([
|
|
|
91
102
|
'set-property',
|
|
92
103
|
'delete-property',
|
|
93
104
|
]);
|
|
105
|
+
const FILE_COMMANDS = new Set(['cat', 'download', 'sync', 'publish']);
|
|
94
106
|
|
|
95
107
|
class UsageError extends Error {}
|
|
96
108
|
|
|
@@ -114,6 +126,9 @@ function parseArgs(argv) {
|
|
|
114
126
|
let status;
|
|
115
127
|
let propertyKey;
|
|
116
128
|
let propertyValue;
|
|
129
|
+
let channelPath;
|
|
130
|
+
let contentType;
|
|
131
|
+
let overwrite = false;
|
|
117
132
|
let index = 0;
|
|
118
133
|
|
|
119
134
|
const takeValue = (flag) => {
|
|
@@ -193,6 +208,15 @@ function parseArgs(argv) {
|
|
|
193
208
|
case '--value':
|
|
194
209
|
propertyValue = takeValue(arg);
|
|
195
210
|
break;
|
|
211
|
+
case '--channel-path':
|
|
212
|
+
channelPath = trimArgument(takeValue(arg));
|
|
213
|
+
break;
|
|
214
|
+
case '--content-type':
|
|
215
|
+
contentType = trimArgument(takeValue(arg));
|
|
216
|
+
break;
|
|
217
|
+
case '--overwrite':
|
|
218
|
+
overwrite = true;
|
|
219
|
+
break;
|
|
196
220
|
default:
|
|
197
221
|
throw new UsageError(`Unknown argument: ${arg}`);
|
|
198
222
|
}
|
|
@@ -209,6 +233,8 @@ function parseArgs(argv) {
|
|
|
209
233
|
let taskCommand = null;
|
|
210
234
|
let taskId = null;
|
|
211
235
|
let mentionTargetId = null;
|
|
236
|
+
let fileCommand = null;
|
|
237
|
+
let filePath = null;
|
|
212
238
|
let consumed = 1;
|
|
213
239
|
if (command === 'task') {
|
|
214
240
|
if (positionals.length < 2) {
|
|
@@ -232,6 +258,16 @@ function parseArgs(argv) {
|
|
|
232
258
|
}
|
|
233
259
|
consumed += 1;
|
|
234
260
|
}
|
|
261
|
+
} else if (command === 'files' && positionals.length > consumed) {
|
|
262
|
+
fileCommand = positionals[consumed];
|
|
263
|
+
if (!FILE_COMMANDS.has(fileCommand)) {
|
|
264
|
+
throw new UsageError(`Unknown files command: ${fileCommand}`);
|
|
265
|
+
}
|
|
266
|
+
consumed += 1;
|
|
267
|
+
if (positionals.length > consumed) {
|
|
268
|
+
filePath = trimArgument(positionals[consumed]);
|
|
269
|
+
consumed += 1;
|
|
270
|
+
}
|
|
235
271
|
} else if (command === 'mention' && positionals.length > consumed) {
|
|
236
272
|
mentionTargetId = trimArgument(positionals[consumed]);
|
|
237
273
|
consumed += 1;
|
|
@@ -267,6 +303,11 @@ function parseArgs(argv) {
|
|
|
267
303
|
status,
|
|
268
304
|
propertyKey,
|
|
269
305
|
propertyValue,
|
|
306
|
+
fileCommand,
|
|
307
|
+
filePath,
|
|
308
|
+
channelPath,
|
|
309
|
+
contentType,
|
|
310
|
+
overwrite,
|
|
270
311
|
};
|
|
271
312
|
}
|
|
272
313
|
|
|
@@ -303,6 +344,14 @@ function validateCommand(parsed) {
|
|
|
303
344
|
return;
|
|
304
345
|
}
|
|
305
346
|
if (parsed.command !== 'task') {
|
|
347
|
+
if (parsed.command === 'files' && parsed.fileCommand !== null) {
|
|
348
|
+
if (parsed.fileCommand !== 'sync' || parsed.filePath !== null) {
|
|
349
|
+
requireText(parsed.filePath, parsed.fileCommand === 'publish' ? 'local path' : 'channel path', label);
|
|
350
|
+
}
|
|
351
|
+
if (parsed.fileCommand === 'publish') {
|
|
352
|
+
requireText(parsed.channelPath, '--channel-path', label);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
306
355
|
return;
|
|
307
356
|
}
|
|
308
357
|
if (parsed.taskCommand === 'create') {
|
|
@@ -486,6 +535,39 @@ function resolveRequest(credential, parsed) {
|
|
|
486
535
|
body: null,
|
|
487
536
|
};
|
|
488
537
|
}
|
|
538
|
+
if (parsed.command === 'files' && parsed.fileCommand === null) {
|
|
539
|
+
return { url: `${baseUrl}/v1/channels/${channel}/files`, method: 'GET', body: null };
|
|
540
|
+
}
|
|
541
|
+
if (parsed.command === 'files' && parsed.fileCommand === 'cat') {
|
|
542
|
+
const query = buildQuery([['path', parsed.filePath]]);
|
|
543
|
+
return { url: `${baseUrl}/v1/channels/${channel}/files/content${query}`, method: 'GET', body: null };
|
|
544
|
+
}
|
|
545
|
+
if (parsed.command === 'files' && parsed.fileCommand === 'download') {
|
|
546
|
+
return {
|
|
547
|
+
url: `${baseUrl}/v1/channels/${channel}/files/download`,
|
|
548
|
+
method: 'POST',
|
|
549
|
+
body: { path: parsed.filePath, overwrite: parsed.overwrite },
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
if (parsed.command === 'files' && parsed.fileCommand === 'sync') {
|
|
553
|
+
return {
|
|
554
|
+
url: `${baseUrl}/v1/channels/${channel}/files/sync`,
|
|
555
|
+
method: 'POST',
|
|
556
|
+
body: { ...(parsed.filePath !== null ? { path: parsed.filePath } : {}), overwrite: parsed.overwrite },
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
if (parsed.command === 'files' && parsed.fileCommand === 'publish') {
|
|
560
|
+
return {
|
|
561
|
+
url: `${baseUrl}/v1/channels/${channel}/files/publish`,
|
|
562
|
+
method: 'POST',
|
|
563
|
+
body: {
|
|
564
|
+
sourcePath: parsed.filePath,
|
|
565
|
+
channelPath: parsed.channelPath,
|
|
566
|
+
...(parsed.contentType ? { contentType: parsed.contentType } : {}),
|
|
567
|
+
overwrite: parsed.overwrite,
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
}
|
|
489
571
|
if (parsed.command === 'users') {
|
|
490
572
|
return { url: `${baseUrl}/v1/channels/${channel}/users`, method: 'GET', body: null };
|
|
491
573
|
}
|
|
@@ -541,6 +623,13 @@ async function main() {
|
|
|
541
623
|
validateCommand(parsed);
|
|
542
624
|
const credential = await readGatewayCredential(parsed.gatewayCredentialPath);
|
|
543
625
|
const result = await callGateway(credential, resolveRequest(credential, parsed));
|
|
626
|
+
if (parsed.command === 'files' && parsed.fileCommand === 'cat') {
|
|
627
|
+
process.stdout.write(result.text);
|
|
628
|
+
if (!result.text.endsWith('\n')) {
|
|
629
|
+
process.stdout.write('\n');
|
|
630
|
+
}
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
544
633
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
545
634
|
return 0;
|
|
546
635
|
}
|
|
@@ -25,6 +25,16 @@ Commands:
|
|
|
25
25
|
This agent's identity in this channel.
|
|
26
26
|
history [--limit <n>] [--before <n>] [--after <n>]
|
|
27
27
|
Recent messages in this channel.
|
|
28
|
+
files
|
|
29
|
+
Files visible in this channel.
|
|
30
|
+
files cat <channel-path>
|
|
31
|
+
Print one UTF-8 text file up to 64 KiB.
|
|
32
|
+
files download <channel-path> [--overwrite]
|
|
33
|
+
Copy one file into execution.local_directory/.borgee/channel-files.
|
|
34
|
+
files sync [<channel-path>] [--overwrite]
|
|
35
|
+
Copy all files, or one subtree, into the fixed working-copy directory.
|
|
36
|
+
files publish <local-relative-path> --channel-path <channel-path> [--content-type <mime>] [--overwrite]
|
|
37
|
+
Explicitly publish one execution.local_directory file. Existing channel files are preserved unless --overwrite is present.
|
|
28
38
|
users
|
|
29
39
|
Participants visible in this channel.
|
|
30
40
|
draft --turn-execution-id <id>
|
|
@@ -72,6 +82,7 @@ COMMANDS = frozenset(
|
|
|
72
82
|
"bootstrap",
|
|
73
83
|
"whoami",
|
|
74
84
|
"history",
|
|
85
|
+
"files",
|
|
75
86
|
"users",
|
|
76
87
|
"draft",
|
|
77
88
|
"send",
|
|
@@ -91,6 +102,7 @@ TASK_COMMANDS = frozenset(
|
|
|
91
102
|
"delete-property",
|
|
92
103
|
}
|
|
93
104
|
)
|
|
105
|
+
FILE_COMMANDS = frozenset({"cat", "download", "sync", "publish"})
|
|
94
106
|
|
|
95
107
|
|
|
96
108
|
class UsageError(Exception):
|
|
@@ -121,6 +133,9 @@ def parse_args(argv: list[str]) -> dict[str, object]:
|
|
|
121
133
|
status: str | None = None
|
|
122
134
|
property_key: str | None = None
|
|
123
135
|
property_value: str | None = None
|
|
136
|
+
channel_path: str | None = None
|
|
137
|
+
content_type: str | None = None
|
|
138
|
+
overwrite = False
|
|
124
139
|
index = 0
|
|
125
140
|
|
|
126
141
|
def take_value(flag: str) -> str:
|
|
@@ -176,6 +191,12 @@ def parse_args(argv: list[str]) -> dict[str, object]:
|
|
|
176
191
|
property_key = trim_argument(take_value(arg))
|
|
177
192
|
elif arg == "--value":
|
|
178
193
|
property_value = take_value(arg)
|
|
194
|
+
elif arg == "--channel-path":
|
|
195
|
+
channel_path = trim_argument(take_value(arg))
|
|
196
|
+
elif arg == "--content-type":
|
|
197
|
+
content_type = trim_argument(take_value(arg))
|
|
198
|
+
elif arg == "--overwrite":
|
|
199
|
+
overwrite = True
|
|
179
200
|
else:
|
|
180
201
|
raise UsageError(f"Unknown argument: {arg}")
|
|
181
202
|
index += 1
|
|
@@ -189,6 +210,8 @@ def parse_args(argv: list[str]) -> dict[str, object]:
|
|
|
189
210
|
task_command: str | None = None
|
|
190
211
|
task_id: str | None = None
|
|
191
212
|
mention_target_id: str | None = None
|
|
213
|
+
file_command: str | None = None
|
|
214
|
+
file_path: str | None = None
|
|
192
215
|
consumed = 1
|
|
193
216
|
if command == "task":
|
|
194
217
|
if len(positionals) < 2:
|
|
@@ -209,6 +232,14 @@ def parse_args(argv: list[str]) -> dict[str, object]:
|
|
|
209
232
|
f'Empty task id for "task {task_command}"; pass a task id or omit the argument entirely'
|
|
210
233
|
)
|
|
211
234
|
consumed += 1
|
|
235
|
+
elif command == "files" and len(positionals) > consumed:
|
|
236
|
+
file_command = positionals[consumed]
|
|
237
|
+
if file_command not in FILE_COMMANDS:
|
|
238
|
+
raise UsageError(f"Unknown files command: {file_command}")
|
|
239
|
+
consumed += 1
|
|
240
|
+
if len(positionals) > consumed:
|
|
241
|
+
file_path = trim_argument(positionals[consumed])
|
|
242
|
+
consumed += 1
|
|
212
243
|
elif command == "mention" and len(positionals) > consumed:
|
|
213
244
|
mention_target_id = trim_argument(positionals[consumed])
|
|
214
245
|
consumed += 1
|
|
@@ -240,6 +271,11 @@ def parse_args(argv: list[str]) -> dict[str, object]:
|
|
|
240
271
|
"status": status,
|
|
241
272
|
"property_key": property_key,
|
|
242
273
|
"property_value": property_value,
|
|
274
|
+
"file_command": file_command,
|
|
275
|
+
"file_path": file_path,
|
|
276
|
+
"channel_path": channel_path,
|
|
277
|
+
"content_type": content_type,
|
|
278
|
+
"overwrite": overwrite,
|
|
243
279
|
}
|
|
244
280
|
|
|
245
281
|
|
|
@@ -272,6 +308,16 @@ def validate_command(parsed: dict[str, object]) -> None:
|
|
|
272
308
|
require_text(parsed["body"], "--body", label)
|
|
273
309
|
require_text(parsed["turn_execution_id"], "--turn-execution-id", label)
|
|
274
310
|
return
|
|
311
|
+
if command == "files" and parsed["file_command"] is not None:
|
|
312
|
+
if parsed["file_command"] != "sync" or parsed["file_path"] is not None:
|
|
313
|
+
require_text(
|
|
314
|
+
parsed["file_path"],
|
|
315
|
+
"local path" if parsed["file_command"] == "publish" else "channel path",
|
|
316
|
+
label,
|
|
317
|
+
)
|
|
318
|
+
if parsed["file_command"] == "publish":
|
|
319
|
+
require_text(parsed["channel_path"], "--channel-path", label)
|
|
320
|
+
return
|
|
275
321
|
if command != "task":
|
|
276
322
|
return
|
|
277
323
|
task_command = parsed["task_command"]
|
|
@@ -464,6 +510,44 @@ def resolve_request(credential: dict[str, str], parsed: dict[str, object]) -> di
|
|
|
464
510
|
"method": "GET",
|
|
465
511
|
"body": None,
|
|
466
512
|
}
|
|
513
|
+
if command == "files":
|
|
514
|
+
file_command = parsed["file_command"]
|
|
515
|
+
if file_command == "cat":
|
|
516
|
+
query = build_query([("path", parsed["file_path"])])
|
|
517
|
+
return {
|
|
518
|
+
"url": f"{base_url}/v1/channels/{channel}/files/content{query}",
|
|
519
|
+
"method": "GET",
|
|
520
|
+
"body": None,
|
|
521
|
+
}
|
|
522
|
+
if file_command == "download":
|
|
523
|
+
return {
|
|
524
|
+
"url": f"{base_url}/v1/channels/{channel}/files/download",
|
|
525
|
+
"method": "POST",
|
|
526
|
+
"body": {"path": parsed["file_path"], "overwrite": parsed["overwrite"]},
|
|
527
|
+
}
|
|
528
|
+
if file_command == "sync":
|
|
529
|
+
body = {"overwrite": parsed["overwrite"]}
|
|
530
|
+
if parsed["file_path"] is not None:
|
|
531
|
+
body["path"] = parsed["file_path"]
|
|
532
|
+
return {
|
|
533
|
+
"url": f"{base_url}/v1/channels/{channel}/files/sync",
|
|
534
|
+
"method": "POST",
|
|
535
|
+
"body": body,
|
|
536
|
+
}
|
|
537
|
+
if file_command == "publish":
|
|
538
|
+
body = {
|
|
539
|
+
"sourcePath": parsed["file_path"],
|
|
540
|
+
"channelPath": parsed["channel_path"],
|
|
541
|
+
"overwrite": parsed["overwrite"],
|
|
542
|
+
}
|
|
543
|
+
if parsed["content_type"] is not None:
|
|
544
|
+
body["contentType"] = parsed["content_type"]
|
|
545
|
+
return {
|
|
546
|
+
"url": f"{base_url}/v1/channels/{channel}/files/publish",
|
|
547
|
+
"method": "POST",
|
|
548
|
+
"body": body,
|
|
549
|
+
}
|
|
550
|
+
return {"url": f"{base_url}/v1/channels/{channel}/files", "method": "GET", "body": None}
|
|
467
551
|
if command == "users":
|
|
468
552
|
return {"url": f"{base_url}/v1/channels/{channel}/users", "method": "GET", "body": None}
|
|
469
553
|
if command == "draft":
|
|
@@ -532,6 +616,12 @@ def main() -> int:
|
|
|
532
616
|
validate_command(parsed)
|
|
533
617
|
credential = read_gateway_credential(str(parsed["gateway_credential_path"]))
|
|
534
618
|
result = call_gateway(credential, resolve_request(credential, parsed))
|
|
619
|
+
if parsed["command"] == "files" and parsed["file_command"] == "cat":
|
|
620
|
+
text = str(result["text"])
|
|
621
|
+
if not text.endswith("\n"):
|
|
622
|
+
text += "\n"
|
|
623
|
+
sys.stdout.buffer.write(text.encode("utf-8"))
|
|
624
|
+
return 0
|
|
535
625
|
sys.stdout.write(json.dumps(result, indent=2, ensure_ascii=False) + "\n")
|
|
536
626
|
return 0
|
|
537
627
|
|