@borgee/agents-host 0.2.35 → 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.
Files changed (42) hide show
  1. package/dist/agents-host.d.ts +2 -0
  2. package/dist/agents-host.js +111 -44
  3. package/dist/chat/sdk-chat-control-plane.js +3 -0
  4. package/dist/context/injection.d.ts +5 -3
  5. package/dist/context/injection.js +45 -29
  6. package/dist/context/prompt.js +23 -35
  7. package/dist/context/skill-manual.d.ts +13 -0
  8. package/dist/context/skill-manual.js +18 -0
  9. package/dist/context/turn-preparation.js +13 -4
  10. package/dist/gateway/localhost-gateway.js +16 -8
  11. package/dist/hosted-turn-content.d.ts +15 -0
  12. package/dist/hosted-turn-content.js +50 -0
  13. package/dist/managed-daemon.d.ts +3 -2
  14. package/dist/managed-daemon.js +77 -29
  15. package/dist/providers/claude/adapter.d.ts +3 -1
  16. package/dist/providers/claude/adapter.js +10 -0
  17. package/dist/providers/claude/cli-client.d.ts +11 -2
  18. package/dist/providers/claude/cli-client.js +98 -27
  19. package/dist/providers/codex/adapter.d.ts +3 -1
  20. package/dist/providers/codex/adapter.js +10 -0
  21. package/dist/providers/codex/cli-client.d.ts +10 -2
  22. package/dist/providers/codex/cli-client.js +85 -21
  23. package/dist/providers/codex/project-doc.js +13 -29
  24. package/dist/providers/copilot/adapter.d.ts +3 -1
  25. package/dist/providers/copilot/adapter.js +10 -0
  26. package/dist/providers/copilot/cli-client.d.ts +9 -1
  27. package/dist/providers/copilot/cli-client.js +71 -8
  28. package/dist/providers/create-provider.d.ts +1 -1
  29. package/dist/providers/create-provider.js +16 -5
  30. package/dist/providers/provider-adapter.d.ts +35 -0
  31. package/dist/providers/provider-adapter.js +44 -1
  32. package/dist/state-paths.d.ts +9 -1
  33. package/dist/state-paths.js +22 -3
  34. package/dist/types.d.ts +33 -2
  35. package/package.json +1 -1
  36. package/skills/borgee-agent/SKILL.md +119 -38
  37. package/skills/borgee-agent/references/errors.md +38 -0
  38. package/skills/borgee-agent/references/task-properties.md +30 -0
  39. package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
  40. package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
  41. package/skills/borgee-agent/borgee-agent.mjs +0 -562
  42. package/skills/borgee-agent/borgee-agent.py +0 -469
@@ -1,469 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import re
7
- import sys
8
- from pathlib import Path
9
- from urllib.error import HTTPError
10
- from urllib.parse import quote, urlencode
11
- from urllib.request import Request, urlopen
12
-
13
- TASK_THREAD_COLLECTION_COMMAND_ERROR = (
14
- "Task assignment threads only support --get-task, --update-task, --set-property "
15
- "and --delete-property. Create/list tasks belong to the parent channel."
16
- )
17
- TASK_THREAD_MISSING_TASK_ID_ERROR = (
18
- "Task assignment thread could not resolve the current task from persisted context "
19
- "or local fallback. Pass --task-id explicitly."
20
- )
21
- # Python ints are arbitrary precision, so nothing can be rounded on the way into the query
22
- # string; the bound is here only so both packaged CLIs accept and reject exactly the same
23
- # inputs. It is the Node CLI that needs it: there an out-of-range magnitude re-stringifies as
24
- # `1e+21`, which the gateway's `Number.parseInt(value, 10)` reads back as `1`.
25
- INTEGER_ARGUMENT_PATTERN = re.compile(r"[+-]?[0-9]+")
26
- MAX_SAFE_INTEGER = 2**53 - 1
27
-
28
-
29
- def parse_int(flag: str, value: str | None) -> int:
30
- if value is None:
31
- raise ValueError(f"Missing value after {flag}")
32
- if not INTEGER_ARGUMENT_PATTERN.fullmatch(value):
33
- raise ValueError(f"Invalid integer for {flag}: {value}")
34
- parsed = int(value)
35
- if abs(parsed) > MAX_SAFE_INTEGER:
36
- raise ValueError(f"Integer out of range for {flag}: {value}")
37
- return parsed
38
-
39
-
40
- def parse_args(argv: list[str]) -> dict[str, object]:
41
- context_path: Path | None = None
42
- auth_path: Path | None = None
43
- turn_execution_id: str | None = None
44
- action = "print-bootstrap"
45
- limit: int | None = None
46
- before: int | None = None
47
- after: int | None = None
48
- body: str | None = None
49
- reply_to_id: str | None = None
50
- mention_target_id: str | None = None
51
- mentions: list[str] = []
52
- title: str | None = None
53
- description: str | None = None
54
- assignee_id: str | None = None
55
- task_id: str | None = None
56
- status: str | None = None
57
- property_key: str | None = None
58
- property_value: str | None = None
59
- index = 0
60
-
61
- def set_action(next_action: str) -> None:
62
- nonlocal action
63
- if action != "print-bootstrap":
64
- raise ValueError("Only one action flag may be used per invocation")
65
- action = next_action
66
-
67
- while index < len(argv):
68
- arg = argv[index]
69
- if arg == "--context":
70
- index += 1
71
- if index >= len(argv):
72
- raise ValueError("Missing path after --context")
73
- context_path = Path(argv[index]).resolve()
74
- elif arg == "--auth-path":
75
- index += 1
76
- if index >= len(argv):
77
- raise ValueError("Missing path after --auth-path")
78
- auth_path = Path(argv[index]).resolve()
79
- elif arg == "--turn-execution-id":
80
- index += 1
81
- if index >= len(argv):
82
- raise ValueError("Missing value after --turn-execution-id")
83
- turn_execution_id = argv[index].strip()
84
- elif arg == "--print-bootstrap":
85
- action = "print-bootstrap"
86
- elif arg == "--health":
87
- set_action("health")
88
- elif arg == "--read-bootstrap":
89
- set_action("read-bootstrap")
90
- elif arg == "--get-me":
91
- set_action("get-me")
92
- elif arg == "--read-history":
93
- set_action("read-history")
94
- elif arg == "--read-task-history":
95
- set_action("read-task-history")
96
- elif arg == "--read-draft":
97
- set_action("read-draft")
98
- elif arg == "--list-users":
99
- set_action("list-users")
100
- elif arg == "--send-message":
101
- set_action("send-message")
102
- elif arg == "--send-mention":
103
- set_action("send-mention")
104
- index += 1
105
- if index >= len(argv):
106
- raise ValueError("Missing value after --send-mention")
107
- mention_target_id = argv[index].strip()
108
- if not mention_target_id:
109
- raise ValueError("Missing value after --send-mention")
110
- elif arg == "--create-task":
111
- set_action("create-task")
112
- elif arg == "--list-tasks":
113
- set_action("list-tasks")
114
- elif arg == "--get-task":
115
- set_action("get-task")
116
- elif arg == "--update-task":
117
- set_action("update-task")
118
- elif arg == "--limit":
119
- index += 1
120
- limit = parse_int(arg, argv[index] if index < len(argv) else None)
121
- elif arg == "--before":
122
- index += 1
123
- before = parse_int(arg, argv[index] if index < len(argv) else None)
124
- elif arg == "--after":
125
- index += 1
126
- after = parse_int(arg, argv[index] if index < len(argv) else None)
127
- elif arg == "--body":
128
- index += 1
129
- if index >= len(argv):
130
- raise ValueError("Missing value after --body")
131
- body = argv[index]
132
- elif arg == "--reply-to":
133
- index += 1
134
- if index >= len(argv):
135
- raise ValueError("Missing value after --reply-to")
136
- reply_to_id = argv[index]
137
- elif arg == "--mention":
138
- index += 1
139
- if index >= len(argv):
140
- raise ValueError("Missing value after --mention")
141
- mention = argv[index].strip()
142
- if not mention:
143
- raise ValueError("Missing value after --mention")
144
- mentions.append(mention)
145
- elif arg == "--title":
146
- index += 1
147
- title = argv[index] if index < len(argv) else None
148
- elif arg == "--description":
149
- index += 1
150
- description = argv[index] if index < len(argv) else None
151
- elif arg == "--assignee-id":
152
- index += 1
153
- assignee_id = argv[index] if index < len(argv) else None
154
- elif arg == "--task-id":
155
- index += 1
156
- task_id = argv[index] if index < len(argv) else None
157
- elif arg == "--status":
158
- index += 1
159
- status = argv[index] if index < len(argv) else None
160
- elif arg == "--set-property":
161
- set_action("set-property")
162
- index += 1
163
- # key=value in one token so the pair cannot be split across flags
164
- # and land half-applied.
165
- pair = argv[index] if index < len(argv) else None
166
- if pair is None or "=" not in pair:
167
- raise ValueError("--set-property requires <key>=<value>")
168
- property_key, property_value = pair.split("=", 1)
169
- if not property_key.strip():
170
- raise ValueError("--set-property requires a non-empty key")
171
- elif arg == "--delete-property":
172
- set_action("delete-property")
173
- index += 1
174
- property_key = argv[index] if index < len(argv) else None
175
- if property_key is None or not property_key.strip():
176
- raise ValueError("--delete-property requires a key")
177
- else:
178
- raise ValueError(f"Unknown argument: {arg}")
179
- index += 1
180
-
181
- if context_path is None:
182
- raise ValueError("Missing required --context <path> argument")
183
- return {
184
- "context_path": context_path,
185
- "auth_path": auth_path,
186
- "turn_execution_id": turn_execution_id,
187
- "action": action,
188
- "limit": limit,
189
- "before": before,
190
- "after": after,
191
- "body": body,
192
- "reply_to_id": reply_to_id,
193
- "mention_target_id": mention_target_id,
194
- "mentions": mentions,
195
- "title": title,
196
- "description": description,
197
- "assignee_id": assignee_id,
198
- "task_id": task_id,
199
- "status": status,
200
- "property_key": property_key.strip() if isinstance(property_key, str) else None,
201
- "property_value": property_value,
202
- }
203
-
204
-
205
- def ensure_visible_mention(body: str, participant_id: str) -> str:
206
- token = f"<@{participant_id}>"
207
- if token in body:
208
- return body
209
- separator = "" if body.endswith((" ", "\n", "\t")) else " "
210
- return f"{body}{separator}{token}"
211
-
212
-
213
- def ensure_visible_mentions(body: str, participant_ids: list[str]) -> str:
214
- next_body = body
215
- for participant_id in participant_ids:
216
- next_body = ensure_visible_mention(next_body, participant_id)
217
- return next_body
218
-
219
-
220
- def explicit_task_id(options: dict[str, object]) -> str | None:
221
- task_id = options.get("task_id")
222
- trimmed = task_id.strip() if isinstance(task_id, str) else ""
223
- return trimmed if trimmed else None
224
-
225
-
226
- def missing_task_id_error(action: str) -> ValueError:
227
- return ValueError(f"Missing required --task-id <value> for --{action}")
228
-
229
-
230
- def require_explicit_task_id(action: str, options: dict[str, object]) -> str:
231
- task_id = explicit_task_id(options)
232
- if task_id is None:
233
- raise missing_task_id_error(action)
234
- return task_id
235
-
236
-
237
- def history_query_suffix(options: dict[str, object]) -> str:
238
- query: dict[str, str] = {}
239
- for key in ("limit", "before", "after"):
240
- if options.get(key) is not None:
241
- query[key] = str(options[key])
242
- return f"?{urlencode(query)}" if query else ""
243
-
244
-
245
- def resolve_task_id(payload: dict[str, object], action: str, options: dict[str, object]) -> str | None:
246
- task_id = explicit_task_id(options)
247
- if task_id is not None:
248
- return task_id
249
- task_assignment_context = payload.get("taskAssignmentContext")
250
- if isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True:
251
- current_task_id = task_assignment_context.get("currentTaskId")
252
- if isinstance(current_task_id, str) and current_task_id.strip():
253
- return current_task_id.strip()
254
- return None
255
- raise missing_task_id_error(action)
256
-
257
-
258
- def resolve_gateway_request(
259
- payload: dict[str, object],
260
- action: str,
261
- options: dict[str, object],
262
- ) -> tuple[str, str, object | None, bool]:
263
- gateway = payload.get("localhostGateway")
264
- if not isinstance(gateway, dict):
265
- raise ValueError("Missing localhostGateway bootstrap metadata in the context payload")
266
- base_url = gateway.get("baseUrl")
267
- if not isinstance(base_url, str):
268
- raise ValueError("Missing localhostGateway bootstrap metadata in the context payload")
269
- task_assignment_context = payload.get("taskAssignmentContext")
270
- if action in ("create-task", "list-tasks") and isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True:
271
- raise ValueError(TASK_THREAD_COLLECTION_COMMAND_ERROR)
272
- channel_id = quote(str(payload.get("channelId", "")), safe="")
273
- if action == "health":
274
- return f"{base_url}/health", "GET", None, False
275
- if action == "read-bootstrap":
276
- return f"{base_url}/v1/channels/{channel_id}/bootstrap", "GET", None, False
277
- if action == "get-me":
278
- return f"{base_url}/v1/channels/{channel_id}/me", "GET", None, False
279
- if action == "read-history":
280
- return f"{base_url}/v1/channels/{channel_id}/history{history_query_suffix(options)}", "GET", None, False
281
- if action == "read-task-history":
282
- task_id = quote(require_explicit_task_id(action, options), safe="")
283
- return f"{base_url}/v1/tasks/{task_id}/history{history_query_suffix(options)}", "GET", None, False
284
- if action == "read-draft":
285
- turn_execution_id = options.get("turn_execution_id")
286
- if not isinstance(turn_execution_id, str) or not turn_execution_id.strip():
287
- raise ValueError("Missing required --turn-execution-id value for --read-draft")
288
- suffix = urlencode({"turnExecutionId": turn_execution_id.strip()})
289
- return f"{base_url}/v1/channels/{channel_id}/draft?{suffix}", "GET", None, False
290
- if action == "list-users":
291
- return f"{base_url}/v1/channels/{channel_id}/users", "GET", None, False
292
- if action in ("send-message", "send-mention"):
293
- return f"{base_url}/v1/channels/{channel_id}/messages", "POST", None, False
294
- if action == "create-task":
295
- title = options.get("title")
296
- if not isinstance(title, str) or title == "":
297
- raise ValueError("Missing required --title <value> for --create-task")
298
- request_body: dict[str, object] = {"title": title}
299
- if options.get("description") is not None:
300
- request_body["description"] = options["description"]
301
- if options.get("assignee_id") is not None:
302
- request_body["assigneeId"] = options["assignee_id"]
303
- return f"{base_url}/v1/channels/{channel_id}/tasks", "POST", request_body, False
304
- if action == "list-tasks":
305
- return f"{base_url}/v1/channels/{channel_id}/tasks", "GET", None, False
306
- if action == "get-task":
307
- task_id = resolve_task_id(payload, action, options)
308
- if isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True and explicit_task_id(options) is None:
309
- return f"{base_url}/v1/channels/{channel_id}/current-task", "GET", None, True
310
- return f"{base_url}/v1/tasks/{quote(str(task_id), safe='')}", "GET", None, False
311
- if action == "update-task":
312
- task_id = resolve_task_id(payload, action, options)
313
- request_body: dict[str, object] = {}
314
- if options.get("status") is not None:
315
- request_body["status"] = options["status"]
316
- if options.get("assignee_id") is not None:
317
- request_body["assigneeId"] = options["assignee_id"]
318
- if options.get("title") is not None:
319
- request_body["title"] = options["title"]
320
- if options.get("description") is not None:
321
- request_body["description"] = options["description"]
322
- if not request_body:
323
- raise ValueError("At least one of --status, --assignee-id, --title, or --description is required for --update-task")
324
- if isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True and explicit_task_id(options) is None:
325
- return f"{base_url}/v1/channels/{channel_id}/current-task", "PATCH", request_body, True
326
- return f"{base_url}/v1/tasks/{quote(str(task_id), safe='')}", "PATCH", request_body, False
327
- if action in ("set-property", "delete-property"):
328
- task_id = resolve_task_id(payload, action, options)
329
- method = "PUT" if action == "set-property" else "DELETE"
330
- request_body = {"value": options.get("property_value")} if action == "set-property" else None
331
- encoded_key = quote(str(options.get("property_key") or ""), safe="")
332
- if isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True and explicit_task_id(options) is None:
333
- return f"{base_url}/v1/channels/{channel_id}/current-task/properties/{encoded_key}", method, request_body, True
334
- return f"{base_url}/v1/tasks/{quote(str(task_id), safe='')}/properties/{encoded_key}", method, request_body, False
335
- raise ValueError(f"Unsupported gateway action: {action}")
336
-
337
-
338
- def read_gateway_auth(auth_path: Path | None, expected_channel_id: object) -> dict[str, str | None]:
339
- if auth_path is None:
340
- raise ValueError("Missing required --auth-path <path> argument for gateway access")
341
- auth_payload = json.loads(auth_path.read_text(encoding="utf-8"))
342
- token = None
343
- localhost_gateway = auth_payload.get("localhostGateway")
344
- if isinstance(localhost_gateway, dict):
345
- raw_token = localhost_gateway.get("token")
346
- if isinstance(raw_token, str):
347
- token = raw_token
348
- if auth_payload.get("channelId") != expected_channel_id or not token:
349
- raise ValueError("Missing localhost gateway auth payload for this channel")
350
- return {"token": token}
351
-
352
-
353
- def call_gateway(payload: dict[str, object], action: str, options: dict[str, object]) -> object:
354
- url, method, request_body, uses_current_thread_fallback = resolve_gateway_request(payload, action, options)
355
- auth = read_gateway_auth(
356
- options.get("auth_path") if isinstance(options.get("auth_path"), Path) else None,
357
- payload.get("channelId"),
358
- )
359
-
360
- data = None
361
- headers = {"Authorization": f"Bearer {auth['token']}"}
362
- if action in ("send-message", "send-mention"):
363
- body = options.get("body")
364
- if not isinstance(body, str) or not body.strip():
365
- raise ValueError(f"Missing required --body value for --{action}")
366
- turn_execution_id = options.get("turn_execution_id")
367
- if not isinstance(turn_execution_id, str) or not turn_execution_id.strip():
368
- raise ValueError(f"Missing required --turn-execution-id value for --{action}")
369
- request_mentions = options.get("mentions")
370
- if action == "send-mention":
371
- mention_target_id = options.get("mention_target_id")
372
- if not isinstance(mention_target_id, str) or not mention_target_id.strip():
373
- raise ValueError("Missing required target user id after --send-mention")
374
- extra_mentions = options.get("mentions")
375
- if isinstance(extra_mentions, list) and len(extra_mentions) > 0:
376
- raise ValueError("Do not combine --send-mention with additional --mention flags")
377
- request_mentions = [mention_target_id.strip()]
378
- if isinstance(request_mentions, list) and len(request_mentions) > 0:
379
- body = ensure_visible_mentions(body, request_mentions)
380
- headers["Content-Type"] = "application/json; charset=utf-8"
381
- request_body = {
382
- "body": body,
383
- "turnExecutionId": turn_execution_id,
384
- }
385
- reply_to_id = options.get("reply_to_id")
386
- if isinstance(reply_to_id, str) and reply_to_id:
387
- request_body["replyToId"] = reply_to_id
388
- data = json.dumps(request_body).encode("utf-8")
389
- elif request_body is not None:
390
- headers["Content-Type"] = "application/json; charset=utf-8"
391
- data = json.dumps(request_body).encode("utf-8")
392
-
393
- request = Request(url, headers=headers, method=method, data=data)
394
- try:
395
- with urlopen(request) as response:
396
- response_body = response.read().decode("utf-8")
397
- return json.loads(response_body) if response_body else None
398
- except HTTPError as exc:
399
- error_body = exc.read().decode("utf-8")
400
- error_payload = json.loads(error_body) if error_body else None
401
- if uses_current_thread_fallback and isinstance(error_payload, dict) and error_payload.get("error") == "not_found":
402
- raise RuntimeError(TASK_THREAD_MISSING_TASK_ID_ERROR) from exc
403
- reported_body = json.dumps(error_payload, separators=(",", ":"), ensure_ascii=False)
404
- raise RuntimeError(f"Gateway request failed with {exc.code}: {reported_body}") from exc
405
-
406
-
407
- def redact_bootstrap(payload: dict[str, object], context_path: Path) -> dict[str, object]:
408
- localhost_gateway = payload.get("localhostGateway")
409
- base_url = None
410
- collaboration = None
411
- if isinstance(localhost_gateway, dict):
412
- raw_base_url = localhost_gateway.get("baseUrl")
413
- if isinstance(raw_base_url, str):
414
- base_url = raw_base_url
415
- raw_collaboration = localhost_gateway.get("collaboration")
416
- if isinstance(raw_collaboration, dict):
417
- collaboration = raw_collaboration
418
- return {
419
- "kind": "borgee-agent-skill-bootstrap",
420
- "contextPath": str(context_path),
421
- "channelId": payload.get("channelId"),
422
- "skillRuntime": payload.get("skillRuntime"),
423
- "taskAssignmentContext": payload.get("taskAssignmentContext"),
424
- "localhostGateway": (
425
- {
426
- "baseUrl": base_url,
427
- **({"collaboration": collaboration} if collaboration is not None else {}),
428
- }
429
- if base_url
430
- else None
431
- ),
432
- }
433
-
434
-
435
- parsed = parse_args(sys.argv[1:])
436
- context_path = parsed["context_path"]
437
- payload = json.loads(context_path.read_text(encoding="utf-8"))
438
-
439
- if parsed["action"] == "print-bootstrap":
440
- print(json.dumps(redact_bootstrap(payload, context_path), indent=2))
441
- else:
442
- print(
443
- json.dumps(
444
- call_gateway(
445
- payload,
446
- str(parsed["action"]),
447
- {
448
- "context_path": context_path,
449
- "auth_path": parsed["auth_path"],
450
- "turn_execution_id": parsed["turn_execution_id"],
451
- "limit": parsed["limit"],
452
- "before": parsed["before"],
453
- "after": parsed["after"],
454
- "body": parsed["body"],
455
- "reply_to_id": parsed["reply_to_id"],
456
- "mention_target_id": parsed["mention_target_id"],
457
- "mentions": parsed["mentions"],
458
- "title": parsed["title"],
459
- "description": parsed["description"],
460
- "assignee_id": parsed["assignee_id"],
461
- "task_id": parsed["task_id"],
462
- "status": parsed["status"],
463
- "property_key": parsed["property_key"],
464
- "property_value": parsed["property_value"],
465
- },
466
- ),
467
- indent=2,
468
- )
469
- )