@borgee/agents-host 0.1.0
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 +91 -0
- package/dist/agents-host.d.ts +46 -0
- package/dist/agents-host.js +117 -0
- package/dist/chat/chat-control-plane.d.ts +7 -0
- package/dist/chat/chat-control-plane.js +1 -0
- package/dist/chat/sdk-chat-control-plane.d.ts +23 -0
- package/dist/chat/sdk-chat-control-plane.js +71 -0
- package/dist/cli-args.d.ts +22 -0
- package/dist/cli-args.js +68 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +34 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +46 -0
- package/dist/context/prompt.d.ts +15 -0
- package/dist/context/prompt.js +24 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -0
- package/dist/providers/claude/adapter.d.ts +8 -0
- package/dist/providers/claude/adapter.js +18 -0
- package/dist/providers/claude/cli-client.d.ts +21 -0
- package/dist/providers/claude/cli-client.js +71 -0
- package/dist/providers/copilot/adapter.d.ts +8 -0
- package/dist/providers/copilot/adapter.js +18 -0
- package/dist/providers/copilot/cli-client.d.ts +24 -0
- package/dist/providers/copilot/cli-client.js +65 -0
- package/dist/providers/create-provider.d.ts +3 -0
- package/dist/providers/create-provider.js +18 -0
- package/dist/providers/provider-adapter.d.ts +4 -0
- package/dist/providers/provider-adapter.js +1 -0
- package/dist/run.d.ts +7 -0
- package/dist/run.js +22 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.js +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @borgee/agents-host
|
|
2
|
+
|
|
3
|
+
Minimal local runtime host: connects a local **Claude Code CLI** (`claude`) or
|
|
4
|
+
**GitHub Copilot CLI** (`copilot`) process to a single Borgee agent over
|
|
5
|
+
[`@borgee/plugin-sdk`](../../sdk/plugin-ts) — the same BPP (`/ws/plugin`)
|
|
6
|
+
protocol the official OpenClaw plugin uses.
|
|
7
|
+
|
|
8
|
+
## What this is (and isn't)
|
|
9
|
+
|
|
10
|
+
This package is intentionally the *smallest* slice that proves the full loop:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Borgee channel message
|
|
14
|
+
→ /ws/plugin (BPP)
|
|
15
|
+
→ @borgee/plugin-sdk
|
|
16
|
+
→ AgentsHost.handleMessage
|
|
17
|
+
→ local `claude`/`copilot` CLI subprocess
|
|
18
|
+
→ BPP reply
|
|
19
|
+
→ Borgee channel
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**In scope:** one agent per process, mention-only gating (with a DM bypass),
|
|
23
|
+
per-channel conversation memory via each CLI's own native session resume
|
|
24
|
+
(Claude `--resume`, Copilot `--session-id`), and dispatching to a local CLI
|
|
25
|
+
provider.
|
|
26
|
+
|
|
27
|
+
**Out of scope** (see the larger `feat/multi-agent-runtime-host` work for
|
|
28
|
+
these): remote command execution / shell dispatch, node provisioning,
|
|
29
|
+
multi-agent discovery/sync, systemd service install, and scheduled/periodic
|
|
30
|
+
prompts. To run several agents, run several processes with different
|
|
31
|
+
`BORGEE_AGENT_API_KEY` values.
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
1. In the Borgee web UI, create an Agent and reveal its API key.
|
|
36
|
+
2. Install the Claude Code CLI (`claude`) or GitHub Copilot CLI (`copilot`)
|
|
37
|
+
locally and make sure it works standalone.
|
|
38
|
+
3. Run this package with the environment variables below.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
BORGEE_BASE_URL=https://your-borgee-server \
|
|
42
|
+
BORGEE_AGENT_API_KEY=bgr_xxxxxxxx \
|
|
43
|
+
BORGEE_AGENT_NAME="My Agent" \
|
|
44
|
+
RUNTIME_PROVIDER=claude \
|
|
45
|
+
pnpm --filter @borgee/agents-host dev
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Environment variables
|
|
49
|
+
|
|
50
|
+
| Variable | Required | Default | Description |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| `BORGEE_BASE_URL` | yes | — | Borgee server base URL (e.g. `https://borgee.example.com`) |
|
|
53
|
+
| `BORGEE_AGENT_API_KEY` | yes | — | The agent's API key, revealed from the web UI |
|
|
54
|
+
| `BORGEE_AGENT_NAME` | no | `Assistant` | Display name used for mention matching and prompts |
|
|
55
|
+
| `RUNTIME_PROVIDER` | no | `claude` | `claude` or `copilot` |
|
|
56
|
+
| `CLAUDE_COMMAND` / `CLAUDE_ARGS` | no | `claude` / `--print` | Local Claude CLI command + args |
|
|
57
|
+
| `COPILOT_COMMAND` / `COPILOT_ARGS` | no | `copilot` / `-s --no-color --allow-all-tools --output-format text` | Local Copilot CLI command + args |
|
|
58
|
+
| `RESPOND_ON_MENTION_ONLY` | no | `true` | If `true`, only replies in non-DM channels when @mentioned; DMs always get a reply |
|
|
59
|
+
|
|
60
|
+
## Conversation memory
|
|
61
|
+
|
|
62
|
+
Each Borgee channel is mapped 1:1 to a native CLI session, keyed by a
|
|
63
|
+
generated UUID kept in memory for the process's lifetime:
|
|
64
|
+
|
|
65
|
+
- **Claude**: first turn for a channel uses `--session-id <uuid>`; every
|
|
66
|
+
turn after uses `-r/--resume <uuid>` (Claude treats these as distinct
|
|
67
|
+
operations).
|
|
68
|
+
- **Copilot**: every turn uses `--session-id=<uuid>` — Copilot's CLI treats
|
|
69
|
+
that flag as create-or-resume for the same UUID.
|
|
70
|
+
|
|
71
|
+
There is no separate history/context store on our side; if the runtime host
|
|
72
|
+
process restarts, each channel starts a fresh CLI session (no session-id
|
|
73
|
+
persistence across restarts in this minimal version).
|
|
74
|
+
|
|
75
|
+
## Running
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pnpm --filter @borgee/agents-host dev # tsx, no build step
|
|
79
|
+
pnpm --filter @borgee/agents-host build # tsc -> dist/
|
|
80
|
+
pnpm --filter @borgee/agents-host start # node dist/index.js
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
There is no systemd/service installer here — run it as a plain foreground
|
|
84
|
+
process (or wrap it with your own process manager) and stop it with
|
|
85
|
+
`Ctrl+C`/`SIGTERM`.
|
|
86
|
+
|
|
87
|
+
## Testing
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pnpm --filter @borgee/agents-host test
|
|
91
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ChatControlPlane } from './chat/chat-control-plane.js';
|
|
2
|
+
import type { ProviderAdapter } from './providers/provider-adapter.js';
|
|
3
|
+
import type { AgentsHostConfig } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Minimal single-agent agents host: connects one local Claude/Copilot CLI
|
|
6
|
+
* to exactly one Borgee agent over `@borgee/plugin-sdk` (BPP / `/ws/plugin`).
|
|
7
|
+
*
|
|
8
|
+
* Conversation memory is handled entirely by each provider's native
|
|
9
|
+
* per-channel CLI session (Claude `--resume`, Copilot `--session-id`) — see
|
|
10
|
+
* the cli-client.ts file under each provider's folder. This class does not
|
|
11
|
+
* keep any message history itself.
|
|
12
|
+
*
|
|
13
|
+
* Out of scope by design (see README): execution/remote-command dispatch,
|
|
14
|
+
* node provisioning, multi-agent discovery, systemd install, and scheduled
|
|
15
|
+
* (periodic) prompts. Running several agents means running several
|
|
16
|
+
* processes, each with its own `BORGEE_AGENT_API_KEY`.
|
|
17
|
+
*/
|
|
18
|
+
export declare class AgentsHost {
|
|
19
|
+
private readonly config;
|
|
20
|
+
private readonly provider;
|
|
21
|
+
private readonly borgee;
|
|
22
|
+
private selfAgentId;
|
|
23
|
+
private started;
|
|
24
|
+
constructor(config: AgentsHostConfig, deps?: {
|
|
25
|
+
borgee?: ChatControlPlane;
|
|
26
|
+
provider?: ProviderAdapter;
|
|
27
|
+
});
|
|
28
|
+
start(): Promise<void>;
|
|
29
|
+
stop(): Promise<void>;
|
|
30
|
+
private handleMessage;
|
|
31
|
+
/**
|
|
32
|
+
* Whether `msg` mentions this agent: either via the SDK's structured
|
|
33
|
+
* mention event kind, or a plain-text substring match on the configured
|
|
34
|
+
* agent name.
|
|
35
|
+
*/
|
|
36
|
+
isLikelyMention(message: {
|
|
37
|
+
type?: string;
|
|
38
|
+
}, content: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Channel-type check, based solely on the `channel_type` hint the SDK
|
|
41
|
+
* derives from the inbound BPP event. There is no REST fallback here (that
|
|
42
|
+
* would require the excluded control-plane SDK) — an absent hint is
|
|
43
|
+
* treated as "not a DM".
|
|
44
|
+
*/
|
|
45
|
+
private isDirectMessage;
|
|
46
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { SdkChatControlPlane } from './chat/sdk-chat-control-plane.js';
|
|
2
|
+
import { createProvider } from './providers/create-provider.js';
|
|
3
|
+
/**
|
|
4
|
+
* Minimal single-agent agents host: connects one local Claude/Copilot CLI
|
|
5
|
+
* to exactly one Borgee agent over `@borgee/plugin-sdk` (BPP / `/ws/plugin`).
|
|
6
|
+
*
|
|
7
|
+
* Conversation memory is handled entirely by each provider's native
|
|
8
|
+
* per-channel CLI session (Claude `--resume`, Copilot `--session-id`) — see
|
|
9
|
+
* the cli-client.ts file under each provider's folder. This class does not
|
|
10
|
+
* keep any message history itself.
|
|
11
|
+
*
|
|
12
|
+
* Out of scope by design (see README): execution/remote-command dispatch,
|
|
13
|
+
* node provisioning, multi-agent discovery, systemd install, and scheduled
|
|
14
|
+
* (periodic) prompts. Running several agents means running several
|
|
15
|
+
* processes, each with its own `BORGEE_AGENT_API_KEY`.
|
|
16
|
+
*/
|
|
17
|
+
export class AgentsHost {
|
|
18
|
+
config;
|
|
19
|
+
provider;
|
|
20
|
+
borgee;
|
|
21
|
+
selfAgentId = null;
|
|
22
|
+
started = false;
|
|
23
|
+
constructor(config, deps) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.provider = deps?.provider ?? createProvider({
|
|
26
|
+
provider: config.agent.provider,
|
|
27
|
+
claudeCommand: config.claudeCommand,
|
|
28
|
+
claudeArgs: config.claudeArgs,
|
|
29
|
+
copilotCommand: config.copilotCommand,
|
|
30
|
+
copilotArgs: config.copilotArgs,
|
|
31
|
+
});
|
|
32
|
+
this.borgee = deps?.borgee ?? new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined,
|
|
33
|
+
// Lets the server know which runtime/provider actually connected
|
|
34
|
+
// (internal/bpp ConnectHandler → users.last_connected_plugin_id), so
|
|
35
|
+
// the web UI can show real state instead of a client-side guess.
|
|
36
|
+
`agents-host:${config.agent.provider}`);
|
|
37
|
+
}
|
|
38
|
+
async start() {
|
|
39
|
+
if (this.started)
|
|
40
|
+
return;
|
|
41
|
+
this.started = true;
|
|
42
|
+
await this.borgee.connect((message) => {
|
|
43
|
+
void this.handleMessage(message);
|
|
44
|
+
});
|
|
45
|
+
const me = await this.borgee.getMe();
|
|
46
|
+
this.selfAgentId = me.id;
|
|
47
|
+
console.log('[agents-host] connected', {
|
|
48
|
+
agentId: me.id,
|
|
49
|
+
agentName: this.config.agent.agentName,
|
|
50
|
+
provider: this.config.agent.provider,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async stop() {
|
|
54
|
+
if (!this.started)
|
|
55
|
+
return;
|
|
56
|
+
this.started = false;
|
|
57
|
+
await this.borgee.close();
|
|
58
|
+
}
|
|
59
|
+
async handleMessage(msg) {
|
|
60
|
+
console.log('[agents-host] received message', {
|
|
61
|
+
eventType: msg.type,
|
|
62
|
+
provider: this.config.agent.provider,
|
|
63
|
+
agentName: this.config.agent.agentName,
|
|
64
|
+
channelId: msg.channel_id,
|
|
65
|
+
});
|
|
66
|
+
const authorId = String(msg.user_id ?? msg.sender_id ?? 'unknown');
|
|
67
|
+
if (this.selfAgentId && authorId === this.selfAgentId) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const content = String(msg.content ?? msg.body ?? '').trim();
|
|
71
|
+
if (!content)
|
|
72
|
+
return;
|
|
73
|
+
const isDm = this.isDirectMessage(msg.channel_type);
|
|
74
|
+
if (this.config.agent.respondOnMentionOnly && !isDm) {
|
|
75
|
+
if (!this.isLikelyMention(msg, content))
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const reply = await this.provider.generateReply({
|
|
80
|
+
agentName: this.config.agent.agentName,
|
|
81
|
+
provider: this.config.agent.provider,
|
|
82
|
+
channelId: msg.channel_id,
|
|
83
|
+
incomingAuthorId: authorId,
|
|
84
|
+
incomingContent: content,
|
|
85
|
+
});
|
|
86
|
+
await this.borgee.postMessage(msg.channel_id, reply.text);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error('[agents-host] failed to generate or send reply:', {
|
|
90
|
+
provider: this.config.agent.provider,
|
|
91
|
+
agentName: this.config.agent.agentName,
|
|
92
|
+
error,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether `msg` mentions this agent: either via the SDK's structured
|
|
98
|
+
* mention event kind, or a plain-text substring match on the configured
|
|
99
|
+
* agent name.
|
|
100
|
+
*/
|
|
101
|
+
isLikelyMention(message, content) {
|
|
102
|
+
if (message.type === 'mention') {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return this.config.agent.agentName.trim().length > 0
|
|
106
|
+
&& content.toLowerCase().includes(this.config.agent.agentName.toLowerCase());
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Channel-type check, based solely on the `channel_type` hint the SDK
|
|
110
|
+
* derives from the inbound BPP event. There is no REST fallback here (that
|
|
111
|
+
* would require the excluded control-plane SDK) — an absent hint is
|
|
112
|
+
* treated as "not a DM".
|
|
113
|
+
*/
|
|
114
|
+
isDirectMessage(hintedType) {
|
|
115
|
+
return hintedType === 'dm';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ChannelMessageEvent, MeResponseUser } from '../types.js';
|
|
2
|
+
export interface ChatControlPlane {
|
|
3
|
+
connect(onMessage: (message: ChannelMessageEvent) => void): Promise<void>;
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
postMessage(channelId: string, content: string): Promise<void>;
|
|
6
|
+
getMe(): Promise<MeResponseUser>;
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type BorgeePluginClient, type BorgeePluginOptions, type InboundMessageEvent } from '@borgee/plugin-sdk';
|
|
2
|
+
import type { ChannelMessageEvent, MeResponseUser } from '../types.js';
|
|
3
|
+
import type { ChatControlPlane } from './chat-control-plane.js';
|
|
4
|
+
type PluginClientLike = Pick<BorgeePluginClient, 'close' | 'connect' | 'getMe' | 'on' | 'sendMessage'>;
|
|
5
|
+
type PluginClientFactory = (options: BorgeePluginOptions) => PluginClientLike;
|
|
6
|
+
/**
|
|
7
|
+
* Thin adapter over `@borgee/plugin-sdk` (the same BPP/`/ws/plugin` SDK used by
|
|
8
|
+
* the OpenClaw plugin) that implements the minimal `ChatControlPlane` surface
|
|
9
|
+
* this runtime host needs: connect, receive messages, and post replies.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SdkChatControlPlane implements ChatControlPlane {
|
|
12
|
+
private readonly client;
|
|
13
|
+
private pendingMessages;
|
|
14
|
+
private unsubscribe;
|
|
15
|
+
private connected;
|
|
16
|
+
constructor(baseUrl: string, apiKey: string, createClient?: PluginClientFactory, pluginId?: string);
|
|
17
|
+
connect(onMessage: (message: ChannelMessageEvent) => void): Promise<void>;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
postMessage(channelId: string, content: string): Promise<void>;
|
|
20
|
+
getMe(): Promise<MeResponseUser>;
|
|
21
|
+
}
|
|
22
|
+
export declare function mapInboundToChannelMessage(event: InboundMessageEvent): ChannelMessageEvent;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createBorgeePlugin, } from '@borgee/plugin-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Thin adapter over `@borgee/plugin-sdk` (the same BPP/`/ws/plugin` SDK used by
|
|
4
|
+
* the OpenClaw plugin) that implements the minimal `ChatControlPlane` surface
|
|
5
|
+
* this runtime host needs: connect, receive messages, and post replies.
|
|
6
|
+
*/
|
|
7
|
+
export class SdkChatControlPlane {
|
|
8
|
+
client;
|
|
9
|
+
pendingMessages = [];
|
|
10
|
+
unsubscribe = null;
|
|
11
|
+
connected = false;
|
|
12
|
+
constructor(baseUrl, apiKey, createClient = createBorgeePlugin, pluginId) {
|
|
13
|
+
this.client = createClient({ baseUrl, apiKey, pluginId });
|
|
14
|
+
}
|
|
15
|
+
async connect(onMessage) {
|
|
16
|
+
this.unsubscribe = this.client.on('message', (event) => {
|
|
17
|
+
const message = mapInboundToChannelMessage(event);
|
|
18
|
+
if (!this.connected) {
|
|
19
|
+
this.pendingMessages.push(message);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
onMessage(message);
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
await this.client.connect();
|
|
26
|
+
this.connected = true;
|
|
27
|
+
for (const message of this.pendingMessages) {
|
|
28
|
+
onMessage(message);
|
|
29
|
+
}
|
|
30
|
+
this.pendingMessages = [];
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
this.unsubscribe?.();
|
|
34
|
+
this.unsubscribe = null;
|
|
35
|
+
this.pendingMessages = [];
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async close() {
|
|
40
|
+
this.connected = false;
|
|
41
|
+
this.pendingMessages = [];
|
|
42
|
+
this.unsubscribe?.();
|
|
43
|
+
this.unsubscribe = null;
|
|
44
|
+
await this.client.close();
|
|
45
|
+
}
|
|
46
|
+
async postMessage(channelId, content) {
|
|
47
|
+
await this.client.sendMessage({ channelId, body: content });
|
|
48
|
+
}
|
|
49
|
+
async getMe() {
|
|
50
|
+
const me = await this.client.getMe();
|
|
51
|
+
return {
|
|
52
|
+
id: me.id,
|
|
53
|
+
display_name: me.displayName,
|
|
54
|
+
role: me.kind,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function mapInboundToChannelMessage(event) {
|
|
59
|
+
return {
|
|
60
|
+
type: event.kind,
|
|
61
|
+
channel_id: event.channelId,
|
|
62
|
+
channel_type: event.channelType,
|
|
63
|
+
message_id: event.message?.id ?? event.messageId,
|
|
64
|
+
user_id: event.message?.authorId ?? event.reaction?.userId,
|
|
65
|
+
sender_id: event.message?.authorId ?? event.reaction?.userId,
|
|
66
|
+
content: event.message?.body,
|
|
67
|
+
body: event.message?.body,
|
|
68
|
+
content_type: event.message?.contentType,
|
|
69
|
+
created_at: event.createdAt,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps `agents-host start` CLI flags to the env vars `config.ts` reads.
|
|
3
|
+
* Keeping this as a plain lookup table (rather than duplicating
|
|
4
|
+
* `loadConfigFromEnv`'s parsing logic) means the CLI and the plain
|
|
5
|
+
* env-var entry point (`index.ts`) always agree on defaults/validation.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CLI_FLAG_TO_ENV: Record<string, string>;
|
|
8
|
+
export interface ParsedStartArgs {
|
|
9
|
+
serverUrl: string;
|
|
10
|
+
apiKey: string;
|
|
11
|
+
env: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export declare class CliUsageError extends Error {
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parses `start <serverUrl> <apiKey> [--flag value ...]` (the argv slice
|
|
17
|
+
* after the `start` command word). Throws `CliUsageError` with a
|
|
18
|
+
* human-readable message on any usage problem instead of exiting the
|
|
19
|
+
* process, so callers (and tests) can decide how to report it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseStartArgs(argv: string[]): ParsedStartArgs;
|
|
22
|
+
export declare const USAGE = "Usage: agents-host start <serverUrl> <apiKey> [options]\n\nOptions:\n --name <name> Display name (default: Assistant)\n --provider <claude|copilot> Runtime provider (default: claude)\n --claude-command <cmd> Local Claude CLI command (default: claude)\n --claude-args <args> Local Claude CLI args (default: --print)\n --copilot-command <cmd> Local Copilot CLI command (default: copilot)\n --copilot-args <args> Local Copilot CLI args\n --mention-only <true|false> Reply only when @mentioned outside DMs (default: true)\n\nExample:\n agents-host start https://borgee.example.com bgr_xxxxxxxx --provider copilot\n";
|
package/dist/cli-args.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps `agents-host start` CLI flags to the env vars `config.ts` reads.
|
|
3
|
+
* Keeping this as a plain lookup table (rather than duplicating
|
|
4
|
+
* `loadConfigFromEnv`'s parsing logic) means the CLI and the plain
|
|
5
|
+
* env-var entry point (`index.ts`) always agree on defaults/validation.
|
|
6
|
+
*/
|
|
7
|
+
export const CLI_FLAG_TO_ENV = {
|
|
8
|
+
name: 'BORGEE_AGENT_NAME',
|
|
9
|
+
provider: 'RUNTIME_PROVIDER',
|
|
10
|
+
'claude-command': 'CLAUDE_COMMAND',
|
|
11
|
+
'claude-args': 'CLAUDE_ARGS',
|
|
12
|
+
'copilot-command': 'COPILOT_COMMAND',
|
|
13
|
+
'copilot-args': 'COPILOT_ARGS',
|
|
14
|
+
'mention-only': 'RESPOND_ON_MENTION_ONLY',
|
|
15
|
+
};
|
|
16
|
+
export class CliUsageError extends Error {
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parses `start <serverUrl> <apiKey> [--flag value ...]` (the argv slice
|
|
20
|
+
* after the `start` command word). Throws `CliUsageError` with a
|
|
21
|
+
* human-readable message on any usage problem instead of exiting the
|
|
22
|
+
* process, so callers (and tests) can decide how to report it.
|
|
23
|
+
*/
|
|
24
|
+
export function parseStartArgs(argv) {
|
|
25
|
+
const positionals = [];
|
|
26
|
+
const env = {};
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const arg = argv[i];
|
|
29
|
+
if (arg.startsWith('--')) {
|
|
30
|
+
const flag = arg.slice(2);
|
|
31
|
+
const value = argv[i + 1];
|
|
32
|
+
if (value === undefined || value.startsWith('--')) {
|
|
33
|
+
throw new CliUsageError(`Missing value for --${flag}`);
|
|
34
|
+
}
|
|
35
|
+
const envKey = CLI_FLAG_TO_ENV[flag];
|
|
36
|
+
if (!envKey) {
|
|
37
|
+
throw new CliUsageError(`Unknown option: --${flag}`);
|
|
38
|
+
}
|
|
39
|
+
env[envKey] = value;
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
positionals.push(arg);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const [serverUrl, apiKey, ...extra] = positionals;
|
|
47
|
+
if (!serverUrl)
|
|
48
|
+
throw new CliUsageError('Missing <serverUrl>');
|
|
49
|
+
if (!apiKey)
|
|
50
|
+
throw new CliUsageError('Missing <apiKey>');
|
|
51
|
+
if (extra.length > 0)
|
|
52
|
+
throw new CliUsageError(`Unexpected argument: ${extra[0]}`);
|
|
53
|
+
return { serverUrl, apiKey, env };
|
|
54
|
+
}
|
|
55
|
+
export const USAGE = `Usage: agents-host start <serverUrl> <apiKey> [options]
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
--name <name> Display name (default: Assistant)
|
|
59
|
+
--provider <claude|copilot> Runtime provider (default: claude)
|
|
60
|
+
--claude-command <cmd> Local Claude CLI command (default: claude)
|
|
61
|
+
--claude-args <args> Local Claude CLI args (default: --print)
|
|
62
|
+
--copilot-command <cmd> Local Copilot CLI command (default: copilot)
|
|
63
|
+
--copilot-args <args> Local Copilot CLI args
|
|
64
|
+
--mention-only <true|false> Reply only when @mentioned outside DMs (default: true)
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
agents-host start https://borgee.example.com bgr_xxxxxxxx --provider copilot
|
|
68
|
+
`;
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CliUsageError, parseStartArgs, USAGE } from './cli-args.js';
|
|
3
|
+
import { runMain } from './run.js';
|
|
4
|
+
function fail(message) {
|
|
5
|
+
if (message)
|
|
6
|
+
console.error(`[agents-host] ${message}`);
|
|
7
|
+
console.error(USAGE);
|
|
8
|
+
process.exit(message ? 1 : 0);
|
|
9
|
+
}
|
|
10
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
11
|
+
if (command === undefined || command === '--help' || command === '-h') {
|
|
12
|
+
fail();
|
|
13
|
+
}
|
|
14
|
+
if (command !== 'start') {
|
|
15
|
+
fail(`Unknown command: ${command}`);
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const { serverUrl, apiKey, env } = parseStartArgs(rest);
|
|
19
|
+
process.env.BORGEE_BASE_URL = serverUrl;
|
|
20
|
+
process.env.BORGEE_AGENT_API_KEY = apiKey;
|
|
21
|
+
for (const [key, value] of Object.entries(env)) {
|
|
22
|
+
process.env[key] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof CliUsageError) {
|
|
27
|
+
fail(error.message);
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
runMain().catch((error) => {
|
|
32
|
+
console.error('[agents-host] fatal error:', error);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AgentsHostConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Loads agents-host configuration from environment variables. This process
|
|
4
|
+
* hosts exactly one Borgee agent (`BORGEE_AGENT_API_KEY`) — to run more
|
|
5
|
+
* agents, run more processes with different env vars.
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadConfigFromEnv(): AgentsHostConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function requireEnv(name) {
|
|
2
|
+
const value = process.env[name];
|
|
3
|
+
if (!value || value.trim().length === 0) {
|
|
4
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
5
|
+
}
|
|
6
|
+
return value.trim();
|
|
7
|
+
}
|
|
8
|
+
function envOr(name, fallback) {
|
|
9
|
+
const value = process.env[name];
|
|
10
|
+
return value && value.trim().length > 0 ? value.trim() : fallback;
|
|
11
|
+
}
|
|
12
|
+
function envBoolOr(name, fallback) {
|
|
13
|
+
const value = process.env[name];
|
|
14
|
+
if (value === undefined || value.trim().length === 0)
|
|
15
|
+
return fallback;
|
|
16
|
+
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
function parseArgs(value) {
|
|
19
|
+
return value.trim().length > 0 ? value.trim().split(/\s+/) : [];
|
|
20
|
+
}
|
|
21
|
+
function resolveProvider() {
|
|
22
|
+
const raw = envOr('RUNTIME_PROVIDER', 'claude').toLowerCase();
|
|
23
|
+
if (raw === 'claude' || raw === 'copilot')
|
|
24
|
+
return raw;
|
|
25
|
+
throw new Error(`Unsupported RUNTIME_PROVIDER: ${raw} (expected "claude" or "copilot")`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Loads agents-host configuration from environment variables. This process
|
|
29
|
+
* hosts exactly one Borgee agent (`BORGEE_AGENT_API_KEY`) — to run more
|
|
30
|
+
* agents, run more processes with different env vars.
|
|
31
|
+
*/
|
|
32
|
+
export function loadConfigFromEnv() {
|
|
33
|
+
return {
|
|
34
|
+
borgeeBaseUrl: requireEnv('BORGEE_BASE_URL'),
|
|
35
|
+
claudeCommand: envOr('CLAUDE_COMMAND', 'claude'),
|
|
36
|
+
claudeArgs: parseArgs(envOr('CLAUDE_ARGS', '--print')),
|
|
37
|
+
copilotCommand: envOr('COPILOT_COMMAND', 'copilot'),
|
|
38
|
+
copilotArgs: parseArgs(envOr('COPILOT_ARGS', '-s --no-color --allow-all-tools --output-format text')),
|
|
39
|
+
agent: {
|
|
40
|
+
agentApiKey: requireEnv('BORGEE_AGENT_API_KEY'),
|
|
41
|
+
agentName: envOr('BORGEE_AGENT_NAME', 'Assistant'),
|
|
42
|
+
respondOnMentionOnly: envBoolOr('RESPOND_ON_MENTION_ONLY', true),
|
|
43
|
+
provider: resolveProvider(),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProviderKind } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the per-turn prompt. Conversation memory is intentionally NOT
|
|
4
|
+
* assembled here: each provider's CLI client resumes a native per-channel
|
|
5
|
+
* session (Claude `--resume`, Copilot `--session-id`), so the CLI itself
|
|
6
|
+
* already has the prior turns. This only needs to carry the agent's
|
|
7
|
+
* identity/instructions plus the new incoming message.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildPrompt(params: {
|
|
10
|
+
agentName: string;
|
|
11
|
+
provider: ProviderKind;
|
|
12
|
+
channelId: string;
|
|
13
|
+
incomingAuthorId: string;
|
|
14
|
+
incomingContent: string;
|
|
15
|
+
}): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function providerLabel(provider) {
|
|
2
|
+
return provider === 'copilot' ? 'GitHub Copilot' : 'Claude';
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Builds the per-turn prompt. Conversation memory is intentionally NOT
|
|
6
|
+
* assembled here: each provider's CLI client resumes a native per-channel
|
|
7
|
+
* session (Claude `--resume`, Copilot `--session-id`), so the CLI itself
|
|
8
|
+
* already has the prior turns. This only needs to carry the agent's
|
|
9
|
+
* identity/instructions plus the new incoming message.
|
|
10
|
+
*/
|
|
11
|
+
export function buildPrompt(params) {
|
|
12
|
+
return [
|
|
13
|
+
`You are ${params.agentName}, a helpful AI teammate inside Borgee.`,
|
|
14
|
+
`Your current backend provider is ${providerLabel(params.provider)} (${params.provider}).`,
|
|
15
|
+
'You are replying inside a shared collaboration channel.',
|
|
16
|
+
'Be concise, helpful, and honest about uncertainty.',
|
|
17
|
+
'Do not claim to have performed actions you did not actually perform.',
|
|
18
|
+
`If the user asks who you are, what powers you, or which backend/provider you use, mention that you are currently running on ${providerLabel(params.provider)}.`,
|
|
19
|
+
`Channel: ${params.channelId}`,
|
|
20
|
+
'',
|
|
21
|
+
`New message from ${params.incomingAuthorId}:`,
|
|
22
|
+
params.incomingContent,
|
|
23
|
+
].join('\n');
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { runMain } from './run.js';
|
|
2
|
+
// Plain env-var entry point (`pnpm dev` / `pnpm start`). For the CLI entry
|
|
3
|
+
// point that accepts `agents-host start <serverUrl> <apiKey> ...`, see
|
|
4
|
+
// cli.ts, which sets the equivalent env vars from argv and delegates to the
|
|
5
|
+
// same runMain().
|
|
6
|
+
runMain().catch((error) => {
|
|
7
|
+
console.error('[agents-host] fatal error:', error);
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProviderAdapter } from '../provider-adapter.js';
|
|
2
|
+
import type { ProviderInput, ProviderReply } from '../../types.js';
|
|
3
|
+
import { ClaudeCliClient } from './cli-client.js';
|
|
4
|
+
export declare class ClaudeProviderAdapter implements ProviderAdapter {
|
|
5
|
+
private readonly cli;
|
|
6
|
+
constructor(cli: ClaudeCliClient);
|
|
7
|
+
generateReply(input: ProviderInput): Promise<ProviderReply>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { buildPrompt } from '../../context/prompt.js';
|
|
2
|
+
export class ClaudeProviderAdapter {
|
|
3
|
+
cli;
|
|
4
|
+
constructor(cli) {
|
|
5
|
+
this.cli = cli;
|
|
6
|
+
}
|
|
7
|
+
async generateReply(input) {
|
|
8
|
+
const prompt = buildPrompt({
|
|
9
|
+
agentName: input.agentName,
|
|
10
|
+
provider: input.provider,
|
|
11
|
+
channelId: input.channelId,
|
|
12
|
+
incomingAuthorId: input.incomingAuthorId,
|
|
13
|
+
incomingContent: input.incomingContent,
|
|
14
|
+
});
|
|
15
|
+
const text = await this.cli.generateReply(input.channelId, prompt);
|
|
16
|
+
return { text };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI client for the Claude Code CLI (`claude`) non-interactive mode, with
|
|
3
|
+
* native per-channel session continuity.
|
|
4
|
+
*
|
|
5
|
+
* Verified CLI behavior (`claude --help`):
|
|
6
|
+
* - `--session-id <uuid>` starts a *new* conversation pinned to that UUID.
|
|
7
|
+
* - `-r/--resume <uuid>` resumes an *existing* conversation by session ID.
|
|
8
|
+
* These are documented as distinct operations, so this client tracks which
|
|
9
|
+
* channels have already started a session and switches from `--session-id`
|
|
10
|
+
* (first turn) to `--resume` (every turn after) accordingly. No message
|
|
11
|
+
* history is kept on our side — the CLI's own session storage is the single
|
|
12
|
+
* source of truth for conversation memory.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ClaudeCliClient {
|
|
15
|
+
private readonly command;
|
|
16
|
+
private readonly args;
|
|
17
|
+
private readonly sessionIdsByChannel;
|
|
18
|
+
constructor(command: string, args: string[]);
|
|
19
|
+
generateReply(channelId: string, prompt: string): Promise<string>;
|
|
20
|
+
private run;
|
|
21
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
/**
|
|
4
|
+
* CLI client for the Claude Code CLI (`claude`) non-interactive mode, with
|
|
5
|
+
* native per-channel session continuity.
|
|
6
|
+
*
|
|
7
|
+
* Verified CLI behavior (`claude --help`):
|
|
8
|
+
* - `--session-id <uuid>` starts a *new* conversation pinned to that UUID.
|
|
9
|
+
* - `-r/--resume <uuid>` resumes an *existing* conversation by session ID.
|
|
10
|
+
* These are documented as distinct operations, so this client tracks which
|
|
11
|
+
* channels have already started a session and switches from `--session-id`
|
|
12
|
+
* (first turn) to `--resume` (every turn after) accordingly. No message
|
|
13
|
+
* history is kept on our side — the CLI's own session storage is the single
|
|
14
|
+
* source of truth for conversation memory.
|
|
15
|
+
*/
|
|
16
|
+
export class ClaudeCliClient {
|
|
17
|
+
command;
|
|
18
|
+
args;
|
|
19
|
+
sessionIdsByChannel = new Map();
|
|
20
|
+
constructor(command, args) {
|
|
21
|
+
this.command = command;
|
|
22
|
+
this.args = args;
|
|
23
|
+
}
|
|
24
|
+
async generateReply(channelId, prompt) {
|
|
25
|
+
const existingSessionId = this.sessionIdsByChannel.get(channelId);
|
|
26
|
+
const sessionId = existingSessionId ?? randomUUID();
|
|
27
|
+
const sessionArgs = existingSessionId
|
|
28
|
+
? ['--resume', sessionId]
|
|
29
|
+
: ['--session-id', sessionId];
|
|
30
|
+
const text = await this.run([...this.args, ...sessionArgs], prompt);
|
|
31
|
+
// Only remember the session once the CLI call actually succeeds, so a
|
|
32
|
+
// failed first turn doesn't leave us permanently trying to `--resume`
|
|
33
|
+
// a session that was never created.
|
|
34
|
+
this.sessionIdsByChannel.set(channelId, sessionId);
|
|
35
|
+
return text;
|
|
36
|
+
}
|
|
37
|
+
async run(args, prompt) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const child = spawn(this.command, args, {
|
|
40
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
41
|
+
});
|
|
42
|
+
let stdout = '';
|
|
43
|
+
let stderr = '';
|
|
44
|
+
child.stdout.setEncoding('utf8');
|
|
45
|
+
child.stderr.setEncoding('utf8');
|
|
46
|
+
child.stdout.on('data', (chunk) => {
|
|
47
|
+
stdout += chunk;
|
|
48
|
+
});
|
|
49
|
+
child.stderr.on('data', (chunk) => {
|
|
50
|
+
stderr += chunk;
|
|
51
|
+
});
|
|
52
|
+
child.on('error', (error) => {
|
|
53
|
+
reject(error);
|
|
54
|
+
});
|
|
55
|
+
child.on('close', (code) => {
|
|
56
|
+
if (code !== 0) {
|
|
57
|
+
reject(new Error(`Claude CLI failed with code ${code}: ${stderr.trim()}`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const text = stdout.trim();
|
|
61
|
+
if (!text) {
|
|
62
|
+
reject(new Error('Claude CLI returned empty output'));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
resolve(text);
|
|
66
|
+
});
|
|
67
|
+
child.stdin.write(prompt);
|
|
68
|
+
child.stdin.end();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProviderAdapter } from '../provider-adapter.js';
|
|
2
|
+
import type { ProviderInput, ProviderReply } from '../../types.js';
|
|
3
|
+
import { CopilotCliClient } from './cli-client.js';
|
|
4
|
+
export declare class CopilotProviderAdapter implements ProviderAdapter {
|
|
5
|
+
private readonly cli;
|
|
6
|
+
constructor(cli: CopilotCliClient);
|
|
7
|
+
generateReply(input: ProviderInput): Promise<ProviderReply>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { buildPrompt } from '../../context/prompt.js';
|
|
2
|
+
export class CopilotProviderAdapter {
|
|
3
|
+
cli;
|
|
4
|
+
constructor(cli) {
|
|
5
|
+
this.cli = cli;
|
|
6
|
+
}
|
|
7
|
+
async generateReply(input) {
|
|
8
|
+
const prompt = buildPrompt({
|
|
9
|
+
agentName: input.agentName,
|
|
10
|
+
provider: input.provider,
|
|
11
|
+
channelId: input.channelId,
|
|
12
|
+
incomingAuthorId: input.incomingAuthorId,
|
|
13
|
+
incomingContent: input.incomingContent,
|
|
14
|
+
});
|
|
15
|
+
const text = await this.cli.generateReply(input.channelId, prompt);
|
|
16
|
+
return { text };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI client for the GitHub Copilot CLI (`copilot`) non-interactive mode,
|
|
3
|
+
* with native per-channel session continuity.
|
|
4
|
+
*
|
|
5
|
+
* Unlike the Claude CLI, Copilot's non-interactive mode takes the prompt as
|
|
6
|
+
* a `-p/--prompt` argument rather than reading it from stdin, so the prompt
|
|
7
|
+
* is appended to the configured base args on every invocation instead of
|
|
8
|
+
* being written to the child process's stdin.
|
|
9
|
+
*
|
|
10
|
+
* Verified CLI behavior (`copilot --help` + empirical check): `--session-id
|
|
11
|
+
* <uuid>` both *creates* a new session pinned to that UUID (first call) and
|
|
12
|
+
* *resumes* it (subsequent calls with the same UUID) — one flag covers both
|
|
13
|
+
* cases, unlike Claude's `--session-id`/`--resume` split. No message history
|
|
14
|
+
* is kept on our side — the CLI's own session storage is the single source
|
|
15
|
+
* of truth for conversation memory.
|
|
16
|
+
*/
|
|
17
|
+
export declare class CopilotCliClient {
|
|
18
|
+
private readonly command;
|
|
19
|
+
private readonly args;
|
|
20
|
+
private readonly sessionIdsByChannel;
|
|
21
|
+
constructor(command: string, args: string[]);
|
|
22
|
+
generateReply(channelId: string, prompt: string): Promise<string>;
|
|
23
|
+
private run;
|
|
24
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
/**
|
|
4
|
+
* CLI client for the GitHub Copilot CLI (`copilot`) non-interactive mode,
|
|
5
|
+
* with native per-channel session continuity.
|
|
6
|
+
*
|
|
7
|
+
* Unlike the Claude CLI, Copilot's non-interactive mode takes the prompt as
|
|
8
|
+
* a `-p/--prompt` argument rather than reading it from stdin, so the prompt
|
|
9
|
+
* is appended to the configured base args on every invocation instead of
|
|
10
|
+
* being written to the child process's stdin.
|
|
11
|
+
*
|
|
12
|
+
* Verified CLI behavior (`copilot --help` + empirical check): `--session-id
|
|
13
|
+
* <uuid>` both *creates* a new session pinned to that UUID (first call) and
|
|
14
|
+
* *resumes* it (subsequent calls with the same UUID) — one flag covers both
|
|
15
|
+
* cases, unlike Claude's `--session-id`/`--resume` split. No message history
|
|
16
|
+
* is kept on our side — the CLI's own session storage is the single source
|
|
17
|
+
* of truth for conversation memory.
|
|
18
|
+
*/
|
|
19
|
+
export class CopilotCliClient {
|
|
20
|
+
command;
|
|
21
|
+
args;
|
|
22
|
+
sessionIdsByChannel = new Map();
|
|
23
|
+
constructor(command, args) {
|
|
24
|
+
this.command = command;
|
|
25
|
+
this.args = args;
|
|
26
|
+
}
|
|
27
|
+
async generateReply(channelId, prompt) {
|
|
28
|
+
const sessionId = this.sessionIdsByChannel.get(channelId) ?? randomUUID();
|
|
29
|
+
const text = await this.run([...this.args, `--session-id=${sessionId}`, '-p', prompt]);
|
|
30
|
+
this.sessionIdsByChannel.set(channelId, sessionId);
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
async run(args) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const child = spawn(this.command, args, {
|
|
36
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
37
|
+
});
|
|
38
|
+
let stdout = '';
|
|
39
|
+
let stderr = '';
|
|
40
|
+
child.stdout.setEncoding('utf8');
|
|
41
|
+
child.stderr.setEncoding('utf8');
|
|
42
|
+
child.stdout.on('data', (chunk) => {
|
|
43
|
+
stdout += chunk;
|
|
44
|
+
});
|
|
45
|
+
child.stderr.on('data', (chunk) => {
|
|
46
|
+
stderr += chunk;
|
|
47
|
+
});
|
|
48
|
+
child.on('error', (error) => {
|
|
49
|
+
reject(error);
|
|
50
|
+
});
|
|
51
|
+
child.on('close', (code) => {
|
|
52
|
+
if (code !== 0) {
|
|
53
|
+
reject(new Error(`Copilot CLI failed with code ${code}: ${stderr.trim()}`));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const text = stdout.trim();
|
|
57
|
+
if (!text) {
|
|
58
|
+
reject(new Error('Copilot CLI returned empty output'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
resolve(text);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ClaudeCliClient } from './claude/cli-client.js';
|
|
2
|
+
import { ClaudeProviderAdapter } from './claude/adapter.js';
|
|
3
|
+
import { CopilotCliClient } from './copilot/cli-client.js';
|
|
4
|
+
import { CopilotProviderAdapter } from './copilot/adapter.js';
|
|
5
|
+
export function createProvider(config) {
|
|
6
|
+
switch (config.provider) {
|
|
7
|
+
case 'claude': {
|
|
8
|
+
const cli = new ClaudeCliClient(config.claudeCommand, config.claudeArgs);
|
|
9
|
+
return new ClaudeProviderAdapter(cli);
|
|
10
|
+
}
|
|
11
|
+
case 'copilot': {
|
|
12
|
+
const cli = new CopilotCliClient(config.copilotCommand, config.copilotArgs);
|
|
13
|
+
return new CopilotProviderAdapter(cli);
|
|
14
|
+
}
|
|
15
|
+
default:
|
|
16
|
+
throw new Error(`Unsupported provider: ${String(config.provider)}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads config from the environment, starts an `AgentsHost`, and wires up
|
|
3
|
+
* graceful shutdown on SIGINT/SIGTERM. Shared by the plain env-var entry
|
|
4
|
+
* point (`index.ts`) and the CLI entry point (`cli.ts`), which sets the
|
|
5
|
+
* relevant env vars from argv before delegating here.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runMain(): Promise<void>;
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { loadConfigFromEnv } from './config.js';
|
|
2
|
+
import { AgentsHost } from './agents-host.js';
|
|
3
|
+
/**
|
|
4
|
+
* Loads config from the environment, starts an `AgentsHost`, and wires up
|
|
5
|
+
* graceful shutdown on SIGINT/SIGTERM. Shared by the plain env-var entry
|
|
6
|
+
* point (`index.ts`) and the CLI entry point (`cli.ts`), which sets the
|
|
7
|
+
* relevant env vars from argv before delegating here.
|
|
8
|
+
*/
|
|
9
|
+
export async function runMain() {
|
|
10
|
+
const config = loadConfigFromEnv();
|
|
11
|
+
const host = new AgentsHost(config);
|
|
12
|
+
const shutdown = (signal) => {
|
|
13
|
+
console.log(`[agents-host] received ${signal}, shutting down`);
|
|
14
|
+
host
|
|
15
|
+
.stop()
|
|
16
|
+
.catch((error) => console.error('[agents-host] error during shutdown:', error))
|
|
17
|
+
.finally(() => process.exit(0));
|
|
18
|
+
};
|
|
19
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
20
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
21
|
+
await host.start();
|
|
22
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type ProviderKind = 'claude' | 'copilot';
|
|
2
|
+
export interface ProviderCommandConfig {
|
|
3
|
+
claudeCommand: string;
|
|
4
|
+
claudeArgs: string[];
|
|
5
|
+
copilotCommand: string;
|
|
6
|
+
copilotArgs: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface ProviderRuntimeConfig extends ProviderCommandConfig {
|
|
9
|
+
provider: ProviderKind;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Single hosted-agent configuration. The minimal agents host supports exactly
|
|
13
|
+
* one Borgee agent per process — running multiple agents means running
|
|
14
|
+
* multiple processes with different env vars (see README).
|
|
15
|
+
*/
|
|
16
|
+
export interface HostedAgentConfig {
|
|
17
|
+
agentApiKey: string;
|
|
18
|
+
agentName: string;
|
|
19
|
+
respondOnMentionOnly: boolean;
|
|
20
|
+
provider: ProviderKind;
|
|
21
|
+
}
|
|
22
|
+
export interface AgentsHostConfig extends ProviderCommandConfig {
|
|
23
|
+
borgeeBaseUrl: string;
|
|
24
|
+
agent: HostedAgentConfig;
|
|
25
|
+
}
|
|
26
|
+
export interface ChannelMessageEvent {
|
|
27
|
+
type?: string;
|
|
28
|
+
channel_id: string;
|
|
29
|
+
channel_type?: string;
|
|
30
|
+
message_id?: string;
|
|
31
|
+
user_id?: string;
|
|
32
|
+
sender_id?: string;
|
|
33
|
+
content?: string;
|
|
34
|
+
body?: string;
|
|
35
|
+
content_type?: string;
|
|
36
|
+
created_at?: number;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
export interface MeResponseUser {
|
|
40
|
+
id: string;
|
|
41
|
+
display_name?: string;
|
|
42
|
+
role?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ProviderInput {
|
|
45
|
+
agentName: string;
|
|
46
|
+
provider: ProviderKind;
|
|
47
|
+
channelId: string;
|
|
48
|
+
incomingAuthorId: string;
|
|
49
|
+
incomingContent: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ProviderReply {
|
|
52
|
+
text: string;
|
|
53
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@borgee/agents-host",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"description": "Minimal local agents host: connects a local Claude/Copilot CLI to a Borgee agent over @borgee/plugin-sdk",
|
|
10
|
+
"bin": {
|
|
11
|
+
"agents-host": "./dist/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/codetreker/borgee.git",
|
|
20
|
+
"directory": "packages/runtimes/agents-host"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@borgee/plugin-sdk": "0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^24.3.0",
|
|
28
|
+
"tsx": "^4.20.5",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
|
+
"vitest": "^4.1.5"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"predev": "pnpm --filter @borgee/plugin-sdk build",
|
|
34
|
+
"dev": "tsx src/index.ts",
|
|
35
|
+
"precli": "pnpm --filter @borgee/plugin-sdk build",
|
|
36
|
+
"cli": "tsx src/cli.ts",
|
|
37
|
+
"prestart": "pnpm --filter @borgee/plugin-sdk build",
|
|
38
|
+
"start": "node dist/index.js",
|
|
39
|
+
"prebuild": "pnpm --filter @borgee/plugin-sdk build",
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"pretest": "pnpm --filter @borgee/plugin-sdk build",
|
|
43
|
+
"test": "vitest run"
|
|
44
|
+
}
|
|
45
|
+
}
|