@borgee/agents-host 0.2.94 → 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.
Files changed (40) hide show
  1. package/dist/agents-host.d.ts +8 -0
  2. package/dist/agents-host.js +266 -64
  3. package/dist/background-runs.d.ts +4 -0
  4. package/dist/background-runs.js +8 -4
  5. package/dist/chat/chat-control-plane.d.ts +14 -3
  6. package/dist/chat/sdk-chat-control-plane.d.ts +31 -5
  7. package/dist/chat/sdk-chat-control-plane.js +134 -3
  8. package/dist/context/skill-manual.js +1 -1
  9. package/dist/execution-telemetry.d.ts +96 -0
  10. package/dist/execution-telemetry.js +583 -0
  11. package/dist/gateway/channel-file-workspace.d.ts +15 -0
  12. package/dist/gateway/channel-file-workspace.js +130 -0
  13. package/dist/gateway/localhost-gateway.js +140 -0
  14. package/dist/plugin-sdk.js +85 -4
  15. package/dist/plugin-sdk.js.map +2 -2
  16. package/dist/policy/gateway-authorization.d.ts +1 -1
  17. package/dist/policy/gateway-authorization.js +22 -2
  18. package/dist/progress-to-activity.d.ts +3 -3
  19. package/dist/providers/claude/adapter.d.ts +2 -1
  20. package/dist/providers/claude/adapter.js +11 -0
  21. package/dist/providers/claude/cli-client.d.ts +3 -1
  22. package/dist/providers/claude/cli-client.js +267 -13
  23. package/dist/providers/codex/adapter.js +1 -0
  24. package/dist/providers/codex/cli-client.js +5 -0
  25. package/dist/providers/copilot/adapter.js +4 -0
  26. package/dist/providers/copilot/cli-client.js +11 -1
  27. package/dist/providers/copilot/sdk-session.d.ts +12 -3
  28. package/dist/providers/copilot/sdk-session.js +222 -5
  29. package/dist/providers/create-provider.js +4 -0
  30. package/dist/providers/prompt-usage.d.ts +8 -0
  31. package/dist/providers/prompt-usage.js +52 -0
  32. package/dist/state-paths.d.ts +2 -0
  33. package/dist/state-paths.js +6 -0
  34. package/dist/types.d.ts +41 -0
  35. package/dist/typing-lease.d.ts +14 -0
  36. package/dist/typing-lease.js +31 -0
  37. package/package.json +9 -9
  38. package/skills/borgee-agent/SKILL.md +12 -4
  39. package/skills/borgee-agent/scripts/borgee-agent.mjs +89 -0
  40. package/skills/borgee-agent/scripts/borgee-agent.py +90 -0
@@ -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 to 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.
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
- - It does not read or write files anywhere except the one gateway credential file it is handed.
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