@adecore/agents 0.29.2 → 0.30.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/dist/chat/acp-backend.d.ts +59 -0
- package/dist/chat/acp-backend.js +407 -0
- package/dist/chat/acp-protocol.d.ts +131 -0
- package/dist/chat/acp-protocol.js +605 -0
- package/dist/chat/chat-session.d.ts +2 -0
- package/dist/chat/chat-session.js +14 -10
- package/dist/chat/codex-transport.d.ts +4 -27
- package/dist/chat/codex-transport.js +4 -185
- package/dist/chat/fixtures/acp-v1-turn.json +240 -0
- package/dist/chat/index.d.ts +2 -0
- package/dist/chat/index.js +1 -0
- package/dist/chat/json-rpc-transport.d.ts +50 -0
- package/dist/chat/json-rpc-transport.js +203 -0
- package/dist/providers/accounts/accounts.d.ts +10 -6
- package/dist/providers/accounts/accounts.js +19 -19
- package/dist/providers/accounts/service.d.ts +3 -0
- package/dist/providers/accounts/service.js +20 -13
- package/dist/providers/accounts/store.d.ts +4 -5
- package/dist/providers/accounts/store.js +12 -12
- package/dist/providers/acp-provider.d.ts +22 -0
- package/dist/providers/acp-provider.js +45 -0
- package/dist/providers/catalog.js +1 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/registry.d.ts +1 -0
- package/dist/providers/registry.js +4 -0
- package/dist/testing/fake-acp.d.ts +10 -0
- package/dist/testing/fake-acp.js +262 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +476 -0
- package/src/chat/acp-protocol.ts +763 -0
- package/src/chat/chat-session.ts +17 -8
- package/src/chat/codex-transport.ts +6 -207
- package/src/chat/fixtures/acp-v1-turn.json +240 -0
- package/src/chat/index.ts +2 -0
- package/src/chat/json-rpc-transport.ts +253 -0
- package/src/providers/accounts/accounts.ts +28 -25
- package/src/providers/accounts/service.ts +37 -14
- package/src/providers/accounts/store.ts +14 -17
- package/src/providers/acp-provider.ts +73 -0
- package/src/providers/catalog.ts +1 -0
- package/src/providers/index.ts +1 -0
- package/src/providers/registry.ts +5 -0
- package/src/testing/fake-acp.ts +296 -0
- package/src/testing/index.ts +1 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { errorText } from '../error-text.js';
|
|
2
|
+
import { ChatChild, MAX_LINE_LENGTH, readStreamLines } from './chat-process.js';
|
|
3
|
+
/* An error the other side answered a request with. */
|
|
4
|
+
export class JsonRpcError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
data;
|
|
7
|
+
constructor(name, method, error) {
|
|
8
|
+
const detail = typeof error === 'object' && error !== null ? error : {};
|
|
9
|
+
super(`${method} failed: ${typeof detail.message === 'string' ? detail.message : JSON.stringify(error)}`);
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.code = typeof detail.code === 'number' ? detail.code : null;
|
|
12
|
+
this.data = detail.data;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/*
|
|
16
|
+
* One child process and JSON-RPC on its stdio: newline-delimited JSON, a request carries an id and
|
|
17
|
+
* gets a result or an error back under the same id, a notification has no id. The child also sends
|
|
18
|
+
* requests of its own, answered with `respond` or `respondError`.
|
|
19
|
+
*/
|
|
20
|
+
export class JsonRpcTransport {
|
|
21
|
+
child;
|
|
22
|
+
options;
|
|
23
|
+
pending = new Map();
|
|
24
|
+
nextId = 1;
|
|
25
|
+
closed = false;
|
|
26
|
+
// Set once stdin ended or broke; a write after an EPIPE only raises the next one.
|
|
27
|
+
inputClosed = false;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.child = new ChatChild({
|
|
31
|
+
command: options.command,
|
|
32
|
+
cwd: options.cwd,
|
|
33
|
+
env: options.env,
|
|
34
|
+
...(options.spawn ? { spawn: options.spawn } : {}),
|
|
35
|
+
onError: (error) => this.broken(error),
|
|
36
|
+
onExit: (exitCode, stderr) => this.handleExit(exitCode, stderr)
|
|
37
|
+
});
|
|
38
|
+
void this.readLines();
|
|
39
|
+
}
|
|
40
|
+
get pid() {
|
|
41
|
+
return this.child.pid;
|
|
42
|
+
}
|
|
43
|
+
get alive() {
|
|
44
|
+
return !this.closed;
|
|
45
|
+
}
|
|
46
|
+
request(method, params, options = {}) {
|
|
47
|
+
const label = this.options.label;
|
|
48
|
+
const timeoutMs = options.timeoutMs === undefined ? this.options.requestTimeoutMs : options.timeoutMs;
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
if (this.closed) {
|
|
51
|
+
reject(new Error(`${method} failed: ${label} is not running`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const id = this.nextId++;
|
|
55
|
+
let timer = null;
|
|
56
|
+
if (timeoutMs !== null) {
|
|
57
|
+
timer = setTimeout(() => {
|
|
58
|
+
this.pending.delete(id);
|
|
59
|
+
reject(new Error(`${method} failed: ${label} did not answer in ${timeoutMs / 1000} seconds`));
|
|
60
|
+
}, timeoutMs);
|
|
61
|
+
timer.unref?.();
|
|
62
|
+
}
|
|
63
|
+
this.pending.set(id, { resolve, reject, method, timer });
|
|
64
|
+
try {
|
|
65
|
+
this.write({ id, method, params });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (timer !== null) {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
}
|
|
71
|
+
this.pending.delete(id);
|
|
72
|
+
reject(error);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
notify(method, params) {
|
|
77
|
+
this.write({ method, params });
|
|
78
|
+
}
|
|
79
|
+
respond(id, result) {
|
|
80
|
+
this.write({ id, result });
|
|
81
|
+
}
|
|
82
|
+
respondError(id, code, message) {
|
|
83
|
+
this.write({ id, error: { code, message } });
|
|
84
|
+
}
|
|
85
|
+
/* Closes stdin; the child ends on its own once it has nothing more to say. */
|
|
86
|
+
end() {
|
|
87
|
+
this.inputClosed = true;
|
|
88
|
+
try {
|
|
89
|
+
this.child.process.stdin.end();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Already closed by the other side.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
kill(signal = 'SIGTERM') {
|
|
96
|
+
this.child.process.kill(signal);
|
|
97
|
+
}
|
|
98
|
+
stop() {
|
|
99
|
+
this.end();
|
|
100
|
+
this.child.endAfterGrace();
|
|
101
|
+
}
|
|
102
|
+
dispose() {
|
|
103
|
+
return this.child.end();
|
|
104
|
+
}
|
|
105
|
+
write(frame) {
|
|
106
|
+
if (this.closed || this.inputClosed) {
|
|
107
|
+
throw new Error(`${this.options.label} stdin is closed`);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const framed = this.options.versionField ? { jsonrpc: '2.0', ...frame } : frame;
|
|
111
|
+
this.child.process.stdin.write(`${JSON.stringify(framed)}\n`);
|
|
112
|
+
this.child.process.stdin.flush();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
this.inputClosed = true;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/* A pipe that failed takes no more writes; a frame a consumer choked on leaves it as it was. */
|
|
120
|
+
broken(error) {
|
|
121
|
+
this.inputClosed = true;
|
|
122
|
+
this.failed(error);
|
|
123
|
+
}
|
|
124
|
+
failed(error) {
|
|
125
|
+
const failure = new Error(`${this.options.label} transport failed: ${errorText(error)}`);
|
|
126
|
+
this.rejectPending(() => failure);
|
|
127
|
+
try {
|
|
128
|
+
this.options.onError?.(failure);
|
|
129
|
+
}
|
|
130
|
+
catch (reportError) {
|
|
131
|
+
console.error(`Reporting a ${this.options.label} transport failure failed:`, errorText(reportError));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
readLine(line) {
|
|
135
|
+
try {
|
|
136
|
+
this.handleLine(line);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
this.failed(error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async readLines() {
|
|
143
|
+
try {
|
|
144
|
+
await readStreamLines(this.child.process.stdout, (line) => this.readLine(line), () => this.failed(new Error(`${this.options.label} wrote a frame of more than ${MAX_LINE_LENGTH / 1_000_000} million characters, which was dropped`)));
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (!this.closed) {
|
|
148
|
+
this.broken(error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
handleLine(line) {
|
|
153
|
+
if (this.closed || line.trim() === '') {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
let frame;
|
|
157
|
+
try {
|
|
158
|
+
frame = JSON.parse(line);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (typeof frame !== 'object' || frame === null || Array.isArray(frame)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const message = frame;
|
|
167
|
+
if (typeof message.method === 'string') {
|
|
168
|
+
this.options.onFrame(message);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const settle = typeof message.id === 'number' ? this.pending.get(message.id) : undefined;
|
|
172
|
+
if (!settle) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.pending.delete(message.id);
|
|
176
|
+
if (settle.timer !== null) {
|
|
177
|
+
clearTimeout(settle.timer);
|
|
178
|
+
}
|
|
179
|
+
if (message.error !== undefined) {
|
|
180
|
+
settle.reject(new JsonRpcError(this.options.errorName ?? 'JsonRpcError', settle.method, message.error));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
settle.resolve(message.result);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
handleExit(exitCode, stderr) {
|
|
187
|
+
if (this.closed) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.closed = true;
|
|
191
|
+
this.rejectPending((method) => new Error(`${method} failed: ${this.options.label} exited`));
|
|
192
|
+
this.options.onExit(exitCode, stderr);
|
|
193
|
+
}
|
|
194
|
+
rejectPending(reason) {
|
|
195
|
+
for (const settle of this.pending.values()) {
|
|
196
|
+
if (settle.timer !== null) {
|
|
197
|
+
clearTimeout(settle.timer);
|
|
198
|
+
}
|
|
199
|
+
settle.reject(reason(settle.method));
|
|
200
|
+
}
|
|
201
|
+
this.pending.clear();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -2,12 +2,16 @@ import { type AgentKind, type ProviderAccount, type ProviderAccountMap } from '@
|
|
|
2
2
|
import type { ChatProvider } from '../provider.ts';
|
|
3
3
|
import { type AccountsHost, type ReservedVariables } from './variables.ts';
|
|
4
4
|
export type Env = Record<string, string | undefined>;
|
|
5
|
-
export
|
|
5
|
+
export interface AccountProviders {
|
|
6
|
+
kinds: readonly AgentKind[];
|
|
7
|
+
get(kind: AgentKind): ChatProvider;
|
|
8
|
+
}
|
|
9
|
+
export declare function isOfferedKind(kind: string, kinds: readonly AgentKind[]): boolean;
|
|
6
10
|
export declare function isDefaultAccount(id: string, account: Pick<ProviderAccount, 'kind'>): boolean;
|
|
7
11
|
export declare function expandHome(path: string, env: Env): string;
|
|
8
|
-
export declare function accountProblem(id: string, account: ProviderAccount, provider: ChatProvider, env: Env, reserved?: ReservedVariables): string | null;
|
|
9
|
-
export declare function folderVariablesOf(
|
|
10
|
-
export declare function withDefaults(accounts: ProviderAccountMap): ProviderAccountMap;
|
|
12
|
+
export declare function accountProblem(id: string, account: ProviderAccount, provider: ChatProvider, env: Env, reserved?: ReservedVariables, kinds?: readonly AgentKind[]): string | null;
|
|
13
|
+
export declare function folderVariablesOf(providers: AccountProviders): string[];
|
|
14
|
+
export declare function withDefaults(accounts: ProviderAccountMap, kinds: readonly AgentKind[]): ProviderAccountMap;
|
|
11
15
|
export declare function defaultFolder(provider: ChatProvider, env: Env): string;
|
|
12
16
|
export declare function accountFolder(id: string, account: ProviderAccount, provider: ChatProvider, env: Env): string;
|
|
13
17
|
export declare function transcriptFolder(id: string, account: ProviderAccount, provider: ChatProvider, env: Env): string;
|
|
@@ -16,5 +20,5 @@ export interface NamedAccount {
|
|
|
16
20
|
id: string;
|
|
17
21
|
account: ProviderAccount;
|
|
18
22
|
}
|
|
19
|
-
export declare function canContinue(from: NamedAccount, to: NamedAccount,
|
|
20
|
-
export declare function readAccount(id: string, raw: unknown,
|
|
23
|
+
export declare function canContinue(from: NamedAccount, to: NamedAccount, providers: AccountProviders, env: Env): boolean;
|
|
24
|
+
export declare function readAccount(id: string, raw: unknown, providers: AccountProviders, env: Env, host?: AccountsHost): unknown;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { KNOWN_AGENT_KINDS, ProviderAccountSchema } from '@adecore/agent-contracts';
|
|
3
3
|
import { DEFAULT_ACCOUNTS_HOST, variablesProblem } from './variables.js';
|
|
4
4
|
import { homeFolder } from '../../home-folder.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return KNOWN_KINDS.includes(kind);
|
|
5
|
+
export function isOfferedKind(kind, kinds) {
|
|
6
|
+
return kinds.includes(kind);
|
|
8
7
|
}
|
|
9
8
|
/* The default account of a CLI is the folder the CLI uses without being told, and its id is the CLI's kind. */
|
|
10
9
|
export function isDefaultAccount(id, account) {
|
|
@@ -28,8 +27,8 @@ function isRootedPath(path) {
|
|
|
28
27
|
* What is wrong with an account of a CLI this version knows, or null when nothing is. An account of
|
|
29
28
|
* an unknown kind is never judged: it is kept the way it was written, for a version that knows it.
|
|
30
29
|
*/
|
|
31
|
-
export function accountProblem(id, account, provider, env, reserved = { host: DEFAULT_ACCOUNTS_HOST, folderVariables: [] }) {
|
|
32
|
-
if (
|
|
30
|
+
export function accountProblem(id, account, provider, env, reserved = { host: DEFAULT_ACCOUNTS_HOST, folderVariables: [] }, kinds = KNOWN_AGENT_KINDS) {
|
|
31
|
+
if (isOfferedKind(id, kinds) && id !== account.kind) {
|
|
33
32
|
return `"${id}" is the id of the default ${id} account`;
|
|
34
33
|
}
|
|
35
34
|
const variables = variablesProblem(account.env ?? [], reserved);
|
|
@@ -63,16 +62,16 @@ export function accountProblem(id, account, provider, env, reserved = { host: DE
|
|
|
63
62
|
return null;
|
|
64
63
|
}
|
|
65
64
|
/* The variables that point a CLI at its folder, which no account may set by hand. */
|
|
66
|
-
export function folderVariablesOf(
|
|
67
|
-
return
|
|
68
|
-
const home =
|
|
65
|
+
export function folderVariablesOf(providers) {
|
|
66
|
+
return providers.kinds.flatMap((kind) => {
|
|
67
|
+
const home = providers.get(kind).home;
|
|
69
68
|
return home === undefined ? [] : [home.env];
|
|
70
69
|
});
|
|
71
70
|
}
|
|
72
|
-
/* The map with a default account for every CLI
|
|
73
|
-
export function withDefaults(accounts) {
|
|
71
|
+
/* The map with a default account for every CLI the host offers, those first and in catalog order. */
|
|
72
|
+
export function withDefaults(accounts, kinds) {
|
|
74
73
|
const complete = {};
|
|
75
|
-
for (const kind of
|
|
74
|
+
for (const kind of kinds) {
|
|
76
75
|
complete[kind] = accounts[kind] ?? { kind };
|
|
77
76
|
}
|
|
78
77
|
for (const [id, account] of Object.entries(accounts)) {
|
|
@@ -124,22 +123,22 @@ export function accountEnv(id, account, provider, baseEnv, variables = {}) {
|
|
|
124
123
|
* Whether a conversation of one account can go on under the other: the same CLI, reading the same
|
|
125
124
|
* transcripts. Two Codex accounts over one `home` can, shadow home or not; two Claude folders never.
|
|
126
125
|
*/
|
|
127
|
-
export function canContinue(from, to,
|
|
126
|
+
export function canContinue(from, to, providers, env) {
|
|
128
127
|
if (from.id === to.id) {
|
|
129
128
|
return true;
|
|
130
129
|
}
|
|
131
130
|
const kind = from.account.kind;
|
|
132
|
-
if (kind !== to.account.kind || !
|
|
131
|
+
if (kind !== to.account.kind || !isOfferedKind(kind, providers.kinds)) {
|
|
133
132
|
return false;
|
|
134
133
|
}
|
|
135
|
-
const provider =
|
|
134
|
+
const provider = providers.get(kind);
|
|
136
135
|
return transcriptFolder(from.id, from.account, provider, env) === transcriptFolder(to.id, to.account, provider, env);
|
|
137
136
|
}
|
|
138
137
|
/*
|
|
139
138
|
* An account as the file holds it, or null for one to drop. An unknown kind comes back as it was
|
|
140
139
|
* read, extra fields included; a known kind has to parse and make sense.
|
|
141
140
|
*/
|
|
142
|
-
export function readAccount(id, raw,
|
|
141
|
+
export function readAccount(id, raw, providers, env, host = DEFAULT_ACCOUNTS_HOST) {
|
|
143
142
|
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
144
143
|
return null;
|
|
145
144
|
}
|
|
@@ -147,11 +146,12 @@ export function readAccount(id, raw, providerOf, env, host = DEFAULT_ACCOUNTS_HO
|
|
|
147
146
|
if (typeof kind !== 'string' || kind === '') {
|
|
148
147
|
return null;
|
|
149
148
|
}
|
|
150
|
-
if (!
|
|
151
|
-
return
|
|
149
|
+
if (!isOfferedKind(kind, providers.kinds)) {
|
|
150
|
+
return isOfferedKind(id, providers.kinds) ? null : raw;
|
|
152
151
|
}
|
|
153
152
|
const parsed = ProviderAccountSchema.safeParse(raw);
|
|
154
|
-
|
|
153
|
+
const reserved = { host, folderVariables: folderVariablesOf(providers) };
|
|
154
|
+
if (!parsed.success || accountProblem(id, parsed.data, providers.get(kind), env, reserved, providers.kinds) !== null) {
|
|
155
155
|
return null;
|
|
156
156
|
}
|
|
157
157
|
return parsed.data;
|
|
@@ -15,6 +15,7 @@ export interface ProviderAccountsOptions {
|
|
|
15
15
|
list(): Promise<ProviderInfo[]>;
|
|
16
16
|
get(kind: AgentKind): ChatProvider;
|
|
17
17
|
enabled?(kind: AgentKind): boolean;
|
|
18
|
+
kinds?(): readonly AgentKind[];
|
|
18
19
|
};
|
|
19
20
|
env?: Env;
|
|
20
21
|
ask?: AskAccount;
|
|
@@ -70,6 +71,8 @@ export declare class ProviderAccountsService implements AccountLaunches {
|
|
|
70
71
|
refresh(): Promise<ProviderAccounts>;
|
|
71
72
|
private folderOf;
|
|
72
73
|
private runWatch;
|
|
74
|
+
private kinds;
|
|
75
|
+
private accountProviders;
|
|
73
76
|
private serial;
|
|
74
77
|
private commit;
|
|
75
78
|
private planSecrets;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { KNOWN_AGENT_KINDS } from '@adecore/agent-contracts';
|
|
5
5
|
import { DEFAULT_CODEX_CLIENT } from '../../chat/codex-transport.js';
|
|
6
6
|
import { ClientSinks } from '../../client-sinks.js';
|
|
7
7
|
import { errorText } from '../../error-text.js';
|
|
8
|
-
import { accountEnv, accountFolder, canContinue, defaultFolder, expandHome, isDefaultAccount,
|
|
8
|
+
import { accountEnv, accountFolder, canContinue, defaultFolder, expandHome, isDefaultAccount, isOfferedKind, transcriptFolder } from './accounts.js';
|
|
9
9
|
import { AccountError, isDefaultAccountOf } from './launch.js';
|
|
10
10
|
import { prepareShadowHome } from './shadow-home.js';
|
|
11
11
|
import { askAccountAs } from './status.js';
|
|
@@ -77,7 +77,7 @@ export class ProviderAccountsService {
|
|
|
77
77
|
launches = new Map();
|
|
78
78
|
states = new Map();
|
|
79
79
|
stored = {};
|
|
80
|
-
accounts
|
|
80
|
+
accounts;
|
|
81
81
|
passes = Promise.resolve();
|
|
82
82
|
timer = null;
|
|
83
83
|
constructor(options) {
|
|
@@ -93,10 +93,11 @@ export class ProviderAccountsService {
|
|
|
93
93
|
this.secrets = options.secrets === undefined ? platformSecrets(this.host, options.home) : options.secrets;
|
|
94
94
|
this.now = options.now ?? Date.now;
|
|
95
95
|
this.sleep = options.sleep ?? sleep;
|
|
96
|
+
this.accounts = wireAccounts({}, this.kinds());
|
|
96
97
|
}
|
|
97
98
|
async load() {
|
|
98
|
-
this.stored = await readAccounts(this.path,
|
|
99
|
-
this.accounts = wireAccounts(this.stored);
|
|
99
|
+
this.stored = await readAccounts(this.path, this.accountProviders(), this.env, this.host);
|
|
100
|
+
this.accounts = wireAccounts(this.stored, this.kinds());
|
|
100
101
|
await this.readSecrets();
|
|
101
102
|
}
|
|
102
103
|
subscribe(clientId, sink) {
|
|
@@ -104,7 +105,7 @@ export class ProviderAccountsService {
|
|
|
104
105
|
}
|
|
105
106
|
snapshot() {
|
|
106
107
|
const loginCommands = {};
|
|
107
|
-
for (const kind of
|
|
108
|
+
for (const kind of this.kinds()) {
|
|
108
109
|
const command = this.providers.get(kind).home?.loginCommand;
|
|
109
110
|
if (command !== undefined) {
|
|
110
111
|
loginCommands[kind] = command;
|
|
@@ -145,7 +146,7 @@ export class ProviderAccountsService {
|
|
|
145
146
|
throw refuse('no account on it has that id');
|
|
146
147
|
}
|
|
147
148
|
if (account.kind !== kind) {
|
|
148
|
-
throw refuse(`it is an account of ${
|
|
149
|
+
throw refuse(`it is an account of ${isOfferedKind(account.kind, this.kinds()) ? this.providers.get(account.kind).name : account.kind}, not of ${provider.name}`);
|
|
149
150
|
}
|
|
150
151
|
if (account.enabled === false) {
|
|
151
152
|
throw refuse('it is turned off');
|
|
@@ -177,7 +178,7 @@ export class ProviderAccountsService {
|
|
|
177
178
|
if (fromAccount === undefined || toAccount === undefined || fromAccount.kind !== kind) {
|
|
178
179
|
return false;
|
|
179
180
|
}
|
|
180
|
-
return canContinue({ id: fromId, account: fromAccount }, { id: toId, account: toAccount },
|
|
181
|
+
return canContinue({ id: fromId, account: fromAccount }, { id: toId, account: toAccount }, this.accountProviders(), this.env);
|
|
181
182
|
}
|
|
182
183
|
labelOf(id) {
|
|
183
184
|
return this.accounts[id]?.label ?? id;
|
|
@@ -278,6 +279,12 @@ export class ProviderAccountsService {
|
|
|
278
279
|
}
|
|
279
280
|
}
|
|
280
281
|
}
|
|
282
|
+
kinds() {
|
|
283
|
+
return this.providers.kinds?.() ?? KNOWN_AGENT_KINDS;
|
|
284
|
+
}
|
|
285
|
+
accountProviders() {
|
|
286
|
+
return { kinds: this.kinds(), get: (kind) => this.providers.get(kind) };
|
|
287
|
+
}
|
|
281
288
|
/* One change of the file at a time: a create and a save both read what the other just wrote. */
|
|
282
289
|
serial(work) {
|
|
283
290
|
const next = this.writes.then(work);
|
|
@@ -285,7 +292,7 @@ export class ProviderAccountsService {
|
|
|
285
292
|
return next;
|
|
286
293
|
}
|
|
287
294
|
async commit(accounts, isDirectory) {
|
|
288
|
-
const stored = acceptAccounts(accounts, this.stored,
|
|
295
|
+
const stored = acceptAccounts(accounts, this.stored, this.accountProviders(), this.env, isDirectory, this.host);
|
|
289
296
|
const { writes, removals } = this.planSecrets(stored);
|
|
290
297
|
for (const [key, value] of writes) {
|
|
291
298
|
await this.requireSecrets().write(key, value);
|
|
@@ -293,7 +300,7 @@ export class ProviderAccountsService {
|
|
|
293
300
|
}
|
|
294
301
|
await writeAccounts(this.path, stored);
|
|
295
302
|
this.stored = stored;
|
|
296
|
-
this.accounts = wireAccounts(stored);
|
|
303
|
+
this.accounts = wireAccounts(stored, this.kinds());
|
|
297
304
|
for (const key of removals) {
|
|
298
305
|
this.secretValues.delete(key);
|
|
299
306
|
await this.secrets?.remove(key).catch((e) => console.error(`Could not remove ${key} from the keychain:`, errorText(e)));
|
|
@@ -318,7 +325,7 @@ export class ProviderAccountsService {
|
|
|
318
325
|
const kept = new Set();
|
|
319
326
|
for (const [id, entry] of Object.entries(stored)) {
|
|
320
327
|
const kind = entry.kind;
|
|
321
|
-
if (!
|
|
328
|
+
if (!isOfferedKind(kind, this.kinds())) {
|
|
322
329
|
for (const name of sensitiveNames(entry)) {
|
|
323
330
|
kept.add(secretKey(id, name));
|
|
324
331
|
}
|
|
@@ -416,7 +423,7 @@ export class ProviderAccountsService {
|
|
|
416
423
|
return this.status(id, account, 'checking', { checkedAt: 0 });
|
|
417
424
|
}
|
|
418
425
|
status(id, account, state, fields = {}) {
|
|
419
|
-
const provider =
|
|
426
|
+
const provider = isOfferedKind(account.kind, this.kinds()) ? this.providers.get(account.kind) : null;
|
|
420
427
|
const folder = provider === null ? '' : accountFolder(id, account, provider, this.env);
|
|
421
428
|
return {
|
|
422
429
|
id,
|
|
@@ -461,7 +468,7 @@ export class ProviderAccountsService {
|
|
|
461
468
|
}
|
|
462
469
|
async check(id, account, installed) {
|
|
463
470
|
const kind = account.kind;
|
|
464
|
-
if (!
|
|
471
|
+
if (!isOfferedKind(kind, this.kinds())) {
|
|
465
472
|
return this.status(id, account, 'unavailable', { message: `This version of ${this.host.name} does not know this CLI` });
|
|
466
473
|
}
|
|
467
474
|
const provider = this.providers.get(kind);
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { type AgentKind, type ProviderAccountMap } from '@adecore/agent-contracts';
|
|
2
2
|
import { CodedError } from '../../coded-error.ts';
|
|
3
|
-
import type
|
|
4
|
-
import { type Env } from './accounts.ts';
|
|
3
|
+
import { type AccountProviders, type Env } from './accounts.ts';
|
|
5
4
|
import { type AccountsHost } from './variables.ts';
|
|
6
5
|
export declare function accountsPath(home: string): string;
|
|
7
6
|
export type StoredAccounts = Record<string, unknown>;
|
|
8
|
-
export declare function readAccounts(path: string,
|
|
7
|
+
export declare function readAccounts(path: string, providers: AccountProviders, env: Env, host?: AccountsHost): Promise<StoredAccounts>;
|
|
9
8
|
export declare function writeAccounts(path: string, stored: StoredAccounts): Promise<void>;
|
|
10
|
-
export declare function wireAccounts(stored: StoredAccounts): ProviderAccountMap;
|
|
9
|
+
export declare function wireAccounts(stored: StoredAccounts, kinds: readonly AgentKind[]): ProviderAccountMap;
|
|
11
10
|
export declare class InvalidAccountError extends CodedError<'invalid-account'> {
|
|
12
11
|
constructor(message: string);
|
|
13
12
|
}
|
|
14
|
-
export declare function acceptAccounts(saved: ProviderAccountMap, before: StoredAccounts,
|
|
13
|
+
export declare function acceptAccounts(saved: ProviderAccountMap, before: StoredAccounts, providers: AccountProviders, env: Env, isDirectory: (path: string) => boolean, host?: AccountsHost): StoredAccounts;
|
|
@@ -3,14 +3,14 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { ProviderAccountIdSchema, ProviderAccountSchema } from '@adecore/agent-contracts';
|
|
4
4
|
import { CodedError } from '../../coded-error.js';
|
|
5
5
|
import { isNotFound, writeAtomic } from '../../fs.js';
|
|
6
|
-
import { accountFolder, accountProblem, defaultFolder, expandHome, folderVariablesOf, isDefaultAccount,
|
|
6
|
+
import { accountFolder, accountProblem, defaultFolder, expandHome, folderVariablesOf, isDefaultAccount, isOfferedKind, readAccount, withDefaults } from './accounts.js';
|
|
7
7
|
import { DEFAULT_ACCOUNTS_HOST, redactVariables } from './variables.js';
|
|
8
8
|
const FILE_VERSION = 1;
|
|
9
9
|
export function accountsPath(home) {
|
|
10
10
|
return join(home, 'providers.json');
|
|
11
11
|
}
|
|
12
12
|
/* Every entry that does not parse is dropped on its own; a file that is not JSON at all reads as none. */
|
|
13
|
-
export async function readAccounts(path,
|
|
13
|
+
export async function readAccounts(path, providers, env, host = DEFAULT_ACCOUNTS_HOST) {
|
|
14
14
|
let parsed;
|
|
15
15
|
try {
|
|
16
16
|
parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
@@ -30,7 +30,7 @@ export async function readAccounts(path, providerOf, env, host = DEFAULT_ACCOUNT
|
|
|
30
30
|
if (!ProviderAccountIdSchema.safeParse(id).success) {
|
|
31
31
|
continue;
|
|
32
32
|
}
|
|
33
|
-
const account = readAccount(id, raw,
|
|
33
|
+
const account = readAccount(id, raw, providers, env, host);
|
|
34
34
|
if (account !== null) {
|
|
35
35
|
stored[id] = account;
|
|
36
36
|
}
|
|
@@ -48,8 +48,8 @@ function wireAccount(entry) {
|
|
|
48
48
|
}
|
|
49
49
|
return parsed.data.env === undefined ? parsed.data : { ...parsed.data, env: redactVariables(parsed.data.env) };
|
|
50
50
|
}
|
|
51
|
-
export function wireAccounts(stored) {
|
|
52
|
-
return withDefaults(Object.fromEntries(Object.entries(stored).map(([id, entry]) => [id, wireAccount(entry)])));
|
|
51
|
+
export function wireAccounts(stored, kinds) {
|
|
52
|
+
return withDefaults(Object.fromEntries(Object.entries(stored).map(([id, entry]) => [id, wireAccount(entry)])), kinds);
|
|
53
53
|
}
|
|
54
54
|
export class InvalidAccountError extends CodedError {
|
|
55
55
|
constructor(message) {
|
|
@@ -88,12 +88,12 @@ function linkProblem(id, account, before, provider, env, isDirectory) {
|
|
|
88
88
|
* client that knows the kind no better than this host could only have cut short. Sensitive values
|
|
89
89
|
* are still in it: the caller puts them in the keychain before the file is written.
|
|
90
90
|
*/
|
|
91
|
-
export function acceptAccounts(saved, before,
|
|
91
|
+
export function acceptAccounts(saved, before, providers, env, isDirectory, host = DEFAULT_ACCOUNTS_HOST) {
|
|
92
92
|
const stored = {};
|
|
93
|
-
const reserved = { host, folderVariables: folderVariablesOf(
|
|
94
|
-
for (const [id, account] of Object.entries(withDefaults(saved))) {
|
|
95
|
-
if (!
|
|
96
|
-
if (
|
|
93
|
+
const reserved = { host, folderVariables: folderVariablesOf(providers) };
|
|
94
|
+
for (const [id, account] of Object.entries(withDefaults(saved, providers.kinds))) {
|
|
95
|
+
if (!isOfferedKind(account.kind, providers.kinds)) {
|
|
96
|
+
if (isOfferedKind(id, providers.kinds)) {
|
|
97
97
|
throw new InvalidAccountError(`"${id}" is the id of the default ${id} account`);
|
|
98
98
|
}
|
|
99
99
|
const kept = before[id];
|
|
@@ -101,8 +101,8 @@ export function acceptAccounts(saved, before, providerOf, env, isDirectory, host
|
|
|
101
101
|
stored[id] = keptKind === account.kind ? kept : account;
|
|
102
102
|
continue;
|
|
103
103
|
}
|
|
104
|
-
const provider =
|
|
105
|
-
const problem = accountProblem(id, account, provider, env, reserved) ?? linkProblem(id, account, before[id], provider, env, isDirectory);
|
|
104
|
+
const provider = providers.get(account.kind);
|
|
105
|
+
const problem = accountProblem(id, account, provider, env, reserved, providers.kinds) ?? linkProblem(id, account, before[id], provider, env, isDirectory);
|
|
106
106
|
if (problem !== null) {
|
|
107
107
|
throw new InvalidAccountError(`${id}: ${problem}`);
|
|
108
108
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgentKind, ProviderCapabilities } from '@adecore/agent-contracts';
|
|
2
|
+
import { type AcpAgentSpec } from '../chat/acp-backend.ts';
|
|
3
|
+
import type { ModelCatalog } from './catalog.ts';
|
|
4
|
+
import { type CliDetection } from './detect.ts';
|
|
5
|
+
import type { ChatProvider, ProviderHome } from './provider.ts';
|
|
6
|
+
export declare const ACP_CAPABILITIES: ProviderCapabilities;
|
|
7
|
+
export interface AcpProviderSpec extends AcpAgentSpec {
|
|
8
|
+
kind: AgentKind;
|
|
9
|
+
command: string;
|
|
10
|
+
args?: readonly string[];
|
|
11
|
+
catalog: ModelCatalog;
|
|
12
|
+
capabilities?: Partial<ProviderCapabilities>;
|
|
13
|
+
resumeCommand?: string;
|
|
14
|
+
home?: ProviderHome;
|
|
15
|
+
ignoresProjectConfig?: boolean;
|
|
16
|
+
oneShotArgs?(prompt: string): string[];
|
|
17
|
+
oneShotStdinArgs?: readonly string[];
|
|
18
|
+
firstPromptArgs?(prompt: string): string[];
|
|
19
|
+
detect?(command: string, env: Record<string, string | undefined>): Promise<CliDetection>;
|
|
20
|
+
enabled?(): boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function createAcpProvider(spec: AcpProviderSpec): ChatProvider;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createAcpBackend } from '../chat/acp-backend.js';
|
|
2
|
+
import { detectCli } from './detect.js';
|
|
3
|
+
/*
|
|
4
|
+
* What every ACP agent can do through this backend. A file change comes as the text before and after;
|
|
5
|
+
* a message names its mentions and attachments by path, as text the agent opens with its own tools.
|
|
6
|
+
*/
|
|
7
|
+
export const ACP_CAPABILITIES = {
|
|
8
|
+
chat: true,
|
|
9
|
+
terminal: false,
|
|
10
|
+
hooks: false,
|
|
11
|
+
streamsToolOutput: false,
|
|
12
|
+
diffs: 'before-after',
|
|
13
|
+
attachments: true,
|
|
14
|
+
mentions: true,
|
|
15
|
+
denyReason: false,
|
|
16
|
+
allowAlways: true,
|
|
17
|
+
asyncQuestions: false,
|
|
18
|
+
compaction: 'none',
|
|
19
|
+
reportsCost: false,
|
|
20
|
+
reportsContextWindow: false,
|
|
21
|
+
reportsThinking: true,
|
|
22
|
+
slashCommands: true
|
|
23
|
+
};
|
|
24
|
+
export function createAcpProvider(spec) {
|
|
25
|
+
const capabilities = { ...ACP_CAPABILITIES, ...spec.capabilities };
|
|
26
|
+
return {
|
|
27
|
+
kind: spec.kind,
|
|
28
|
+
name: spec.name,
|
|
29
|
+
catalog: spec.catalog,
|
|
30
|
+
capabilities,
|
|
31
|
+
command: [spec.command, ...(spec.args ?? [])],
|
|
32
|
+
resumeCommand: spec.resumeCommand ?? '',
|
|
33
|
+
...(spec.ignoresProjectConfig === undefined ? {} : { ignoresProjectConfig: spec.ignoresProjectConfig }),
|
|
34
|
+
...(spec.home ? { home: spec.home } : {}),
|
|
35
|
+
...(spec.oneShotArgs ? { oneShotArgs: spec.oneShotArgs } : {}),
|
|
36
|
+
...(spec.oneShotStdinArgs ? { oneShotStdinArgs: spec.oneShotStdinArgs } : {}),
|
|
37
|
+
...(spec.enabled ? { enabled: spec.enabled } : {}),
|
|
38
|
+
firstPromptArgs: spec.firstPromptArgs ??
|
|
39
|
+
(() => {
|
|
40
|
+
throw new Error(`${spec.name} is only available as a chat.`);
|
|
41
|
+
}),
|
|
42
|
+
detect: spec.detect ?? ((command, env) => detectCli(command, env)),
|
|
43
|
+
createBackend: createAcpBackend(spec)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -31,6 +31,7 @@ export class ModelCatalog {
|
|
|
31
31
|
slug: model.slug,
|
|
32
32
|
name: model.name,
|
|
33
33
|
badge: model.badge,
|
|
34
|
+
...(model.vendor === undefined ? {} : { vendor: model.vendor }),
|
|
34
35
|
legacy: model.legacy === true,
|
|
35
36
|
isDefault: model.slug === this.data.defaultModel,
|
|
36
37
|
options: this.profileOf(model.slug)?.options ?? [],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { ACP_CAPABILITIES, type AcpProviderSpec, createAcpProvider } from './acp-provider.ts';
|
|
1
2
|
export { AccountError, type AccountLaunches, storedAccount, launchEnv } from './accounts/launch.ts';
|
|
2
3
|
export { ProviderAccountsService, type ProviderAccountsOptions } from './accounts/service.ts';
|
|
3
4
|
export type { ShadowHomeReport } from './accounts/shadow-home.ts';
|
package/dist/providers/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
|
|
2
|
+
export { ACP_CAPABILITIES, createAcpProvider } from './acp-provider.js';
|
|
2
3
|
export { AccountError, storedAccount, launchEnv } from './accounts/launch.js';
|
|
3
4
|
export { ProviderAccountsService } from './accounts/service.js';
|
|
4
5
|
export { variablesProblem, keychainService } from './accounts/variables.js';
|
|
@@ -18,6 +18,7 @@ export declare class ProviderRegistry {
|
|
|
18
18
|
private readonly cache;
|
|
19
19
|
constructor(options?: ProviderRegistryOptions);
|
|
20
20
|
list(): Promise<ProviderInfo[]>;
|
|
21
|
+
kinds(): AgentKind[];
|
|
21
22
|
get(kind: AgentKind): ChatProvider;
|
|
22
23
|
oneShotProvider(preferred?: AgentKind): Promise<ChatProvider | null>;
|
|
23
24
|
catalogFor(kind: AgentKind): ModelCatalog;
|
|
@@ -68,6 +68,10 @@ export class ProviderRegistry {
|
|
|
68
68
|
resumeCommand: provider.resumeCommand
|
|
69
69
|
})));
|
|
70
70
|
}
|
|
71
|
+
/* The kinds this host offers, in the order every menu lists them. */
|
|
72
|
+
kinds() {
|
|
73
|
+
return this.providers.map((provider) => provider.kind);
|
|
74
|
+
}
|
|
71
75
|
/* The provider of a kind, one this host does not offer included. */
|
|
72
76
|
get(kind) {
|
|
73
77
|
return this.providers.find((provider) => provider.kind === kind) ?? this.fallback(kind);
|