@rien7/agent-center-host 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,115 @@
1
+ export const crewToolNames = new Set([
2
+ "agent_spawn",
3
+ "agent_send",
4
+ "agent_wait",
5
+ "agent_close",
6
+ ]);
7
+ const text = (value) => typeof value === "string" ? value.trim() : "";
8
+ const failure = (message) => ({
9
+ content: [{ type: "text", text: message }],
10
+ isError: true,
11
+ });
12
+ export function crewServedTools(ops, options) {
13
+ const guard = (handler) => async (input, { signal }) => {
14
+ try {
15
+ return await handler(input, signal);
16
+ }
17
+ catch (error) {
18
+ return failure(error.message);
19
+ }
20
+ };
21
+ return [
22
+ {
23
+ name: "agent_spawn",
24
+ description: `开一个子 agent 在后台做一件独立的事(比如整理一段时间的日程、把一类备忘归纳成总结)。它有自己的会话(用户在 App 里能看到、能直接和它聊)、自己的记忆和自己的 Center 权限:默认只读,writeScopes 可以给它写权限(只能是你自己有的:${options.writeScopes.join(", ") || "无"})。它做完后报告会自动送回给你;需要在这一轮里拿到结果就用 agent_wait。task 要写清楚:要做什么、范围、结果要什么格式。子 agent 不能再开子 agent。`,
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: {
28
+ name: {
29
+ type: "string",
30
+ description: "简短的名字(用户会看到),如「下周日程总结」",
31
+ },
32
+ task: { type: "string", description: "交给它的任务,写完整" },
33
+ writeScopes: {
34
+ type: "array",
35
+ items: { type: "string" },
36
+ description: '给它的写权限,如 ["memo:write"];省略为只读',
37
+ },
38
+ },
39
+ required: ["name", "task"],
40
+ },
41
+ handler: guard((input, signal) => {
42
+ const name = text(input.name);
43
+ const task = text(input.task);
44
+ if (!name || !task)
45
+ throw new Error("name 和 task 都要写。");
46
+ return ops.spawn({
47
+ name,
48
+ task,
49
+ writeScopes: Array.isArray(input.writeScopes)
50
+ ? input.writeScopes.map(String)
51
+ : null,
52
+ }, signal);
53
+ }),
54
+ },
55
+ {
56
+ name: "agent_send",
57
+ description: "给你开的一个子 agent 再发一条消息(补充要求、追问)。它正忙时会被拒绝:先 agent_wait 等它报告。",
58
+ inputSchema: {
59
+ type: "object",
60
+ properties: {
61
+ agent: { type: "string", description: "子 agent 的名字或 id" },
62
+ message: { type: "string" },
63
+ },
64
+ required: ["agent", "message"],
65
+ },
66
+ handler: guard((input, signal) => {
67
+ const agent = text(input.agent);
68
+ const message = text(input.message);
69
+ if (!agent || !message)
70
+ throw new Error("agent 和 message 都要写。");
71
+ return ops.send({ agent, message }, signal);
72
+ }),
73
+ },
74
+ {
75
+ name: "agent_wait",
76
+ description: "等子 agent 的报告(默认等任何一个,最多 timeoutSeconds 秒,默认 120)。返回已经到的报告;没等到就说明谁还在做。不等的话报告做完也会自动送回给你。",
77
+ inputSchema: {
78
+ type: "object",
79
+ properties: {
80
+ agents: {
81
+ type: "array",
82
+ items: { type: "string" },
83
+ description: "只等这些子 agent(名字或 id);省略为任何一个",
84
+ },
85
+ timeoutSeconds: { type: "number", description: "最多等多久,≤ 600" },
86
+ },
87
+ },
88
+ handler: guard((input, signal) => ops.wait({
89
+ agents: Array.isArray(input.agents)
90
+ ? input.agents.map(String)
91
+ : null,
92
+ timeoutSeconds: Math.min(600, Math.max(0, typeof input.timeoutSeconds === "number"
93
+ ? input.timeoutSeconds
94
+ : 120)),
95
+ }, signal)),
96
+ },
97
+ {
98
+ name: "agent_close",
99
+ description: "结束一个子 agent(它的会话和记录会保留,用户还能翻看;再给它发消息会重新打开)。",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ agent: { type: "string", description: "子 agent 的名字或 id" },
104
+ },
105
+ required: ["agent"],
106
+ },
107
+ handler: guard((input, signal) => {
108
+ const agent = text(input.agent);
109
+ if (!agent)
110
+ throw new Error("agent 要写。");
111
+ return ops.close({ agent }, signal);
112
+ }),
113
+ },
114
+ ];
115
+ }
package/dist/driver.js ADDED
@@ -0,0 +1,87 @@
1
+ const sleep = (ms, signal) => new Promise((resolve) => {
2
+ if (signal.aborted || ms <= 0)
3
+ return resolve();
4
+ const timer = setTimeout(done, ms);
5
+ function done() {
6
+ clearTimeout(timer);
7
+ signal.removeEventListener("abort", done);
8
+ resolve();
9
+ }
10
+ signal.addEventListener("abort", done, { once: true });
11
+ });
12
+ const clip = (text, max) => {
13
+ const chars = Array.from(text);
14
+ return chars.length > max ? `${chars.slice(0, max).join("")}…` : text;
15
+ };
16
+ /**
17
+ * Plays a fixed script that echoes the user's message: a thought, one tool call, the text
18
+ * in small deltas, a record card pointing at the user's own message, done. Deterministic,
19
+ * so a resumed turn continues exactly where Center left off.
20
+ */
21
+ export class EchoDriver {
22
+ name = "echo";
23
+ stepMs;
24
+ chunk;
25
+ constructor(options = {}) {
26
+ this.stepMs = options.stepMs ?? 60;
27
+ this.chunk = options.chunkChars ?? 6;
28
+ }
29
+ script(input) {
30
+ const text = input.message.text;
31
+ const chars = Array.from(text);
32
+ const callId = `echo-${input.turn.id}`;
33
+ const reply = `你说:${text}`;
34
+ const replyChars = Array.from(reply);
35
+ const deltas = [];
36
+ for (let i = 0; i < replyChars.length; i += this.chunk)
37
+ deltas.push({
38
+ type: "textDelta",
39
+ text: replyChars.slice(i, i + this.chunk).join(""),
40
+ });
41
+ return [
42
+ {
43
+ type: "thinking",
44
+ text: `收到「${clip(text, 40)}」。我是回声 agent,把它原样说回去。`,
45
+ },
46
+ {
47
+ type: "toolStarted",
48
+ id: callId,
49
+ name: "echo",
50
+ title: "复述消息",
51
+ detail: `${chars.length} 个字`,
52
+ input: { text },
53
+ },
54
+ {
55
+ type: "toolEnded",
56
+ id: callId,
57
+ resultSummary: `复述了 ${chars.length} 个字`,
58
+ },
59
+ ...deltas,
60
+ {
61
+ type: "card",
62
+ card: {
63
+ type: "record",
64
+ record: {
65
+ uri: `agent:message/${input.message.id}`,
66
+ preview: {
67
+ title: clip(text, 80),
68
+ summary: "你刚发的消息",
69
+ symbol: "text.bubble",
70
+ date: input.message.at,
71
+ },
72
+ },
73
+ },
74
+ },
75
+ { type: "done" },
76
+ ];
77
+ }
78
+ async run(input, sink, signal) {
79
+ const events = this.script(input);
80
+ for (const event of events.slice(input.appended)) {
81
+ await sleep(this.stepMs, signal);
82
+ if (signal.aborted)
83
+ return;
84
+ sink.emit(event);
85
+ }
86
+ }
87
+ }
package/dist/host.js ADDED
@@ -0,0 +1,435 @@
1
+ /**
2
+ * The host loop: register → listen on `events?hostId=` → claim → run each turn through a
3
+ * Driver, appending its events in ~100 ms batches. Reconnects with backoff when the
4
+ * stream drops (or goes silent past the heartbeat), re-claims on every (re)connect and on
5
+ * a slow poll, and resumes turns it was running before a restart. Says host.heartbeat
6
+ * every 20 s so Center keeps its agents online. A driver may add agents (sub-agents) and
7
+ * open turns itself (turn.open): they run through the same loop.
8
+ */
9
+ import { CoreError } from "./core-client.js";
10
+ const maxBatch = 500;
11
+ const sleep = (ms, signal) => new Promise((resolve) => {
12
+ if (signal?.aborted)
13
+ return resolve();
14
+ const timer = setTimeout(done, ms);
15
+ function done() {
16
+ clearTimeout(timer);
17
+ signal?.removeEventListener("abort", done);
18
+ resolve();
19
+ }
20
+ signal?.addEventListener("abort", done, { once: true });
21
+ });
22
+ const jitter = (ms) => Math.round(ms * (0.8 + Math.random() * 0.4));
23
+ /** A rejection that retrying cannot fix: the turn was stopped, taken or removed. */
24
+ const final = (error) => error instanceof CoreError && [403, 404, 409].includes(error.status);
25
+ /** Collects one turn's events and appends them in order, one request at a time. */
26
+ class Batcher {
27
+ queue = [];
28
+ timer = null;
29
+ chain = Promise.resolve();
30
+ seq;
31
+ rejected = null;
32
+ /** A batch was given up on; later ones must not land out of order. */
33
+ abandoned = false;
34
+ host;
35
+ turnId;
36
+ active;
37
+ constructor(host, turnId, appended, active) {
38
+ this.host = host;
39
+ this.turnId = turnId;
40
+ this.active = active;
41
+ this.seq = appended + 1;
42
+ }
43
+ push(event) {
44
+ this.queue.push(event);
45
+ if (event.type === "done" || event.type === "failed")
46
+ this.flush();
47
+ else
48
+ this.timer ??= setTimeout(() => {
49
+ this.flush();
50
+ }, this.host.batchMs);
51
+ }
52
+ flush() {
53
+ if (this.timer)
54
+ clearTimeout(this.timer);
55
+ this.timer = null;
56
+ while (this.queue.length) {
57
+ const batch = this.queue.splice(0, maxBatch);
58
+ const seq = this.seq;
59
+ this.seq += batch.length;
60
+ this.chain = this.chain.then(() => this.send(batch, seq));
61
+ }
62
+ return this.chain;
63
+ }
64
+ async send(batch, seq) {
65
+ let delay = this.host.backoffInitial;
66
+ for (;;) {
67
+ if (this.rejected || this.abandoned || this.active.stop.signal.aborted)
68
+ return;
69
+ try {
70
+ await this.host.client.append(this.turnId, batch, seq);
71
+ return;
72
+ }
73
+ catch (error) {
74
+ if (final(error)) {
75
+ this.rejected = error;
76
+ this.host.log(`turn ${this.turnId}: append rejected (${error.message}); stopping it`);
77
+ this.active.stop.abort();
78
+ return;
79
+ }
80
+ // Network or server error: retry the same batch at the same seq (idempotent).
81
+ // When shutting down, give up: a resume replays from Center's event count.
82
+ if (this.host.signal.aborted) {
83
+ this.abandoned = true;
84
+ return;
85
+ }
86
+ this.host.log(`turn ${this.turnId}: append failed (${error.message}); retrying`);
87
+ await sleep(jitter(delay));
88
+ delay = Math.min(this.host.backoffMax, delay * 2);
89
+ }
90
+ }
91
+ }
92
+ async close() {
93
+ if (this.active.stop.signal.aborted) {
94
+ if (this.timer)
95
+ clearTimeout(this.timer);
96
+ this.queue = [];
97
+ }
98
+ else
99
+ this.flush();
100
+ await this.chain;
101
+ }
102
+ }
103
+ export class AgentHost {
104
+ client;
105
+ driver;
106
+ batchMs;
107
+ backoffInitial;
108
+ backoffMax;
109
+ log;
110
+ hostId = null;
111
+ agents = [];
112
+ /** SSE connections opened so far. */
113
+ connections = 0;
114
+ options;
115
+ lifetime = new AbortController();
116
+ active = new Map();
117
+ /** Turns this process already finished, so a racing claim response never reruns one. */
118
+ ended = new Set();
119
+ claiming = null;
120
+ claimAgain = false;
121
+ loops = [];
122
+ constructor(options) {
123
+ this.options = options;
124
+ this.client = options.client;
125
+ this.driver = options.driver;
126
+ this.batchMs = options.batchMs ?? 100;
127
+ this.backoffInitial = options.backoff?.initialMs ?? 500;
128
+ this.backoffMax = options.backoff?.maxMs ?? 30_000;
129
+ this.log =
130
+ options.log ??
131
+ ((message) => console.error(`${new Date().toISOString()} agent-host: ${message}`));
132
+ }
133
+ get signal() {
134
+ return this.lifetime.signal;
135
+ }
136
+ /** Turns being answered now. */
137
+ get running() {
138
+ return [...this.active.keys()];
139
+ }
140
+ /** Register (retrying until Center answers), then run the loops in the background. */
141
+ async start() {
142
+ let delay = this.backoffInitial;
143
+ for (;;) {
144
+ try {
145
+ const registered = await this.client.register(this.options.name);
146
+ this.hostId = registered.hostId;
147
+ this.agents = registered.agents.filter((a) => a.hostId === registered.hostId);
148
+ break;
149
+ }
150
+ catch (error) {
151
+ if (error instanceof CoreError && [401, 403].includes(error.status))
152
+ throw error;
153
+ this.log(`register failed (${error.message}); retrying`);
154
+ await sleep(jitter(delay), this.signal);
155
+ if (this.signal.aborted)
156
+ return;
157
+ delay = Math.min(this.backoffMax, delay * 2);
158
+ }
159
+ }
160
+ const registered = this.agents;
161
+ if (this.options.agents?.length) {
162
+ this.agents = await this.ensureAgents();
163
+ // Sub-agents of these agents (made by the driver, not host.json) stay this host's.
164
+ const ids = new Set(this.agents.map((a) => a.id));
165
+ for (const a of registered)
166
+ if (a.parentAgentId && ids.has(a.parentAgentId) && !ids.has(a.id))
167
+ this.agents.push(a);
168
+ }
169
+ else if (!this.agents.length)
170
+ this.agents = [
171
+ await this.client.upsertAgent({
172
+ name: this.options.agentName ?? "回声",
173
+ role: "把你说的话原样说回来(echo host)",
174
+ hostId: this.hostId,
175
+ runtime: this.driver.name,
176
+ status: "idle",
177
+ }),
178
+ ];
179
+ await this.driver.start?.({
180
+ client: this.client,
181
+ hostId: this.hostId,
182
+ agents: this.agents,
183
+ log: this.log,
184
+ addAgent: (agent) => this.addAgent(agent),
185
+ openTurn: (input) => this.openTurn(input),
186
+ });
187
+ this.log(`registered as ${this.hostId}; agents: ${this.agents.map((a) => a.name).join(", ")}`);
188
+ this.loops = [this.listen(), this.poll(), this.beat()];
189
+ // Resume what this host was running before a restart, and pick up queued turns.
190
+ this.claimSoon();
191
+ }
192
+ /** Stop listening; running turns flush what they have and stay running for a resume. */
193
+ async stop() {
194
+ if (this.lifetime.signal.aborted)
195
+ return;
196
+ this.lifetime.abort();
197
+ for (const turn of this.active.values())
198
+ turn.shutdown = true;
199
+ await Promise.allSettled([
200
+ ...this.loops,
201
+ ...[...this.active.values()].map((t) => t.settled),
202
+ this.claiming,
203
+ ]);
204
+ await this.driver.stop?.().catch((error) => {
205
+ this.log(`driver stop failed (${error.message})`);
206
+ });
207
+ await Promise.allSettled(this.agents.map((a) => this.client.upsertAgent({ id: a.id, status: "offline" })));
208
+ }
209
+ /** Match host.json agents to the registered ones; create or update the rest. */
210
+ async ensureAgents() {
211
+ const result = [];
212
+ for (const spec of this.options.agents ?? []) {
213
+ const found = this.agents.find((a) => spec.id && a.id === spec.id) ??
214
+ this.agents.find((a) => !spec.id && a.name === spec.name);
215
+ const wanted = {
216
+ name: spec.name,
217
+ ...(spec.role !== undefined ? { role: spec.role } : {}),
218
+ runtime: spec.runtime ?? this.driver.name,
219
+ ...(spec.model !== undefined ? { model: spec.model } : {}),
220
+ };
221
+ const stale = !found ||
222
+ found.name !== wanted.name ||
223
+ found.runtime !== wanted.runtime ||
224
+ (wanted.role !== undefined && found.role !== wanted.role) ||
225
+ (wanted.model !== undefined && found.model !== wanted.model);
226
+ result.push(stale
227
+ ? await this.client.upsertAgent({
228
+ ...(found ? { id: found.id } : spec.id ? { id: spec.id } : {}),
229
+ ...wanted,
230
+ hostId: this.hostId,
231
+ ...(found ? {} : { status: "idle" }),
232
+ })
233
+ : found);
234
+ }
235
+ return result;
236
+ }
237
+ async listen() {
238
+ const hostId = this.hostId;
239
+ const staleMs = this.options.staleMs ?? 45_000;
240
+ let delay = this.backoffInitial;
241
+ while (!this.signal.aborted) {
242
+ const connection = new AbortController();
243
+ const abort = () => connection.abort();
244
+ this.signal.addEventListener("abort", abort, { once: true });
245
+ let watchdog = setTimeout(abort, staleMs);
246
+ try {
247
+ this.connections++;
248
+ for await (const frame of this.client.events(hostId, connection.signal)) {
249
+ clearTimeout(watchdog);
250
+ watchdog = setTimeout(() => {
251
+ this.log("events stream went silent; reconnecting");
252
+ abort();
253
+ }, staleMs);
254
+ if (frame.type === "ready") {
255
+ delay = this.backoffInitial;
256
+ // Anything queued while we were away.
257
+ this.claimSoon();
258
+ }
259
+ else if (frame.type === "turn")
260
+ this.onTurn(frame.turn);
261
+ }
262
+ if (!this.signal.aborted)
263
+ this.log("events stream ended; reconnecting");
264
+ }
265
+ catch (error) {
266
+ if (this.signal.aborted)
267
+ break;
268
+ if (!connection.signal.aborted)
269
+ this.log(`events stream failed (${error.message}); reconnecting`);
270
+ }
271
+ finally {
272
+ clearTimeout(watchdog);
273
+ this.signal.removeEventListener("abort", abort);
274
+ connection.abort();
275
+ }
276
+ await sleep(jitter(delay), this.signal);
277
+ delay = Math.min(this.backoffMax, delay * 2);
278
+ }
279
+ }
280
+ async poll() {
281
+ const every = this.options.pollMs ?? 30_000;
282
+ while (!this.signal.aborted) {
283
+ await sleep(every, this.signal);
284
+ if (!this.signal.aborted)
285
+ this.claimSoon();
286
+ }
287
+ }
288
+ async beat() {
289
+ if (!this.client.heartbeat)
290
+ return;
291
+ const every = this.options.heartbeatMs ?? 20_000;
292
+ let failing = false;
293
+ while (!this.signal.aborted) {
294
+ await sleep(every, this.signal);
295
+ if (this.signal.aborted)
296
+ return;
297
+ try {
298
+ const { online } = await this.client.heartbeat();
299
+ if (online)
300
+ this.log(`heartbeat: ${online} agent(s) back online`);
301
+ failing = false;
302
+ }
303
+ catch (error) {
304
+ // An older Center has no heartbeat: say so once, keep the loop (it may update).
305
+ if (!failing)
306
+ this.log(`heartbeat failed (${error.message})`);
307
+ failing = true;
308
+ }
309
+ }
310
+ }
311
+ /** Run (and claim turns of) one more agent, e.g. a sub-agent the driver created. */
312
+ addAgent(agent) {
313
+ const index = this.agents.findIndex((a) => a.id === agent.id);
314
+ if (index >= 0)
315
+ this.agents[index] = agent;
316
+ else
317
+ this.agents.push(agent);
318
+ }
319
+ /**
320
+ * Start a turn this host originates (turn.open) and answer it like a claimed one.
321
+ * Resolves once Center stored it; the answer runs in the background.
322
+ */
323
+ async openTurn(input) {
324
+ if (!this.client.openTurn)
325
+ throw new Error("this Center client cannot open turns");
326
+ const item = await this.client.openTurn(input);
327
+ if (!this.signal.aborted &&
328
+ !this.active.has(item.turn.id) &&
329
+ !this.ended.has(item.turn.id))
330
+ this.runTurn(item);
331
+ return item;
332
+ }
333
+ onTurn(turn) {
334
+ if (turn.state === "queued")
335
+ this.claimSoon();
336
+ else if (turn.state !== "running") {
337
+ const active = this.active.get(turn.id);
338
+ if (active && !active.stop.signal.aborted) {
339
+ this.log(`turn ${turn.id} ${turn.state}; aborting`);
340
+ active.stop.abort();
341
+ }
342
+ }
343
+ }
344
+ /** Claim now; calls while a claim is in flight fold into one more round. */
345
+ claimSoon() {
346
+ if (this.signal.aborted)
347
+ return;
348
+ if (this.claiming) {
349
+ this.claimAgain = true;
350
+ return;
351
+ }
352
+ this.claiming = (async () => {
353
+ do {
354
+ this.claimAgain = false;
355
+ try {
356
+ await this.claimOnce();
357
+ }
358
+ catch (error) {
359
+ this.log(`claim failed (${error.message})`);
360
+ }
361
+ } while (this.claimAgain && !this.signal.aborted);
362
+ })().finally(() => {
363
+ this.claiming = null;
364
+ });
365
+ }
366
+ async claimOnce() {
367
+ const items = await this.client.claim({
368
+ limit: this.options.claimLimit ?? 4,
369
+ resume: true,
370
+ agentIds: this.agents.map((a) => a.id),
371
+ });
372
+ for (const item of items)
373
+ if (!this.active.has(item.turn.id) && !this.ended.has(item.turn.id))
374
+ this.runTurn(item);
375
+ }
376
+ runTurn(item) {
377
+ const turnId = item.turn.id;
378
+ const appended = item.turn.eventCount;
379
+ const active = {
380
+ stop: new AbortController(),
381
+ shutdown: false,
382
+ settled: Promise.resolve(),
383
+ };
384
+ this.active.set(turnId, active);
385
+ this.log(appended
386
+ ? `turn ${turnId}: resuming after ${appended} events`
387
+ : `turn ${turnId}: answering`);
388
+ const batcher = new Batcher(this, turnId, appended, active);
389
+ // The driver sees one signal: owner stop or host shutdown.
390
+ const signal = AbortSignal.any([active.stop.signal, this.signal]);
391
+ let terminal = false;
392
+ const sink = {
393
+ emit: (event) => {
394
+ if (terminal || active.stop.signal.aborted)
395
+ return;
396
+ if (event.type === "done" || event.type === "failed")
397
+ terminal = true;
398
+ batcher.push(event);
399
+ },
400
+ flushed: () => batcher.flush(),
401
+ };
402
+ active.settled = (async () => {
403
+ try {
404
+ await this.driver.run({ ...item, appended }, sink, signal);
405
+ }
406
+ catch (error) {
407
+ if (!signal.aborted)
408
+ sink.emit({
409
+ type: "failed",
410
+ reason: error.message || "The driver failed",
411
+ });
412
+ }
413
+ await batcher.close();
414
+ if (!terminal &&
415
+ !batcher.rejected &&
416
+ !active.stop.signal.aborted &&
417
+ !this.signal.aborted)
418
+ await this.client.finish(turnId, "done").catch((error) => {
419
+ this.log(`turn ${turnId}: finish failed (${error.message})`);
420
+ });
421
+ })()
422
+ .catch((error) => this.log(`turn ${turnId}: ${error.message}`))
423
+ .finally(() => {
424
+ this.active.delete(turnId);
425
+ if (!active.shutdown) {
426
+ this.ended.add(turnId);
427
+ if (this.ended.size > 1000)
428
+ this.ended.delete(this.ended.values().next().value);
429
+ this.log(`turn ${turnId}: ${active.stop.signal.aborted ? "stopped" : "finished"}`);
430
+ // The agent is free again: its next queued turn (no push announces that).
431
+ this.claimSoon();
432
+ }
433
+ });
434
+ }
435
+ }
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { CenterApi, CenterTools, deniedReason, recordCards, } from "./center-tools.js";
2
+ export { loadHostConfig, parseHostConfig, } from "./config.js";
3
+ export { CoreError, HttpCoreClient, parseSse, } from "./core-client.js";
4
+ export { configPath, loadCredentials, saveCredentials, TokenSource, } from "./credentials.js";
5
+ export { EchoDriver, } from "./driver.js";
6
+ export { AgentHost } from "./host.js";
7
+ export { deviceLogin, normalizeServer } from "./login.js";
8
+ export { MemorySync } from "./memory.js";
9
+ export { OarDriver } from "./oar-driver.js";
10
+ export { bareToolName, coreToolTitle, toolTitle } from "./tool-titles.js";