@adecore/agents 0.29.3 → 0.31.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/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 +33 -17
- 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/provider.d.ts +1 -1
- 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/dist/usage/index-file.d.ts +1 -1
- package/dist/usage/index-file.js +3 -1
- package/dist/usage/index.d.ts +2 -1
- package/dist/usage/index.js +2 -0
- package/dist/usage/limits/monitor.d.ts +3 -1
- package/dist/usage/limits/monitor.js +19 -4
- package/dist/usage/limits/normalize.d.ts +1 -0
- package/dist/usage/limits/normalize.js +2 -1
- package/dist/usage/readers/host-log.d.ts +16 -0
- package/dist/usage/readers/host-log.js +49 -0
- package/dist/usage/roots.js +6 -2
- package/dist/usage/scanner.js +13 -2
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +476 -0
- package/src/chat/acp-protocol.ts +763 -0
- 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 +49 -18
- 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/provider.ts +2 -1
- package/src/providers/registry.ts +5 -0
- package/src/testing/fake-acp.ts +296 -0
- package/src/testing/index.ts +1 -0
- package/src/usage/index-file.ts +3 -2
- package/src/usage/index.ts +2 -1
- package/src/usage/limits/monitor.ts +24 -5
- package/src/usage/limits/normalize.ts +2 -1
- package/src/usage/readers/host-log.ts +72 -0
- package/src/usage/roots.ts +7 -3
- package/src/usage/scanner.ts +16 -5
|
@@ -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,17 +105,22 @@ export class ProviderAccountsService {
|
|
|
104
105
|
}
|
|
105
106
|
snapshot() {
|
|
106
107
|
const loginCommands = {};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
const chatLogins = [];
|
|
109
|
+
for (const kind of this.kinds()) {
|
|
110
|
+
const home = this.providers.get(kind).home;
|
|
111
|
+
if (home?.loginCommand !== undefined) {
|
|
112
|
+
loginCommands[kind] = home.loginCommand;
|
|
113
|
+
}
|
|
114
|
+
else if (home !== undefined) {
|
|
115
|
+
chatLogins.push(kind);
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
return {
|
|
114
119
|
accounts: this.accounts,
|
|
115
120
|
statuses: Object.entries(this.accounts).map(([id, account]) => this.statusOf(id, account)),
|
|
116
121
|
secretsAvailable: this.secrets !== null,
|
|
117
|
-
loginCommands
|
|
122
|
+
loginCommands,
|
|
123
|
+
...(chatLogins.length > 0 ? { chatLogins } : {})
|
|
118
124
|
};
|
|
119
125
|
}
|
|
120
126
|
/* The account under an id, for a launch; null for an id this machine does not have. */
|
|
@@ -145,7 +151,7 @@ export class ProviderAccountsService {
|
|
|
145
151
|
throw refuse('no account on it has that id');
|
|
146
152
|
}
|
|
147
153
|
if (account.kind !== kind) {
|
|
148
|
-
throw refuse(`it is an account of ${
|
|
154
|
+
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
155
|
}
|
|
150
156
|
if (account.enabled === false) {
|
|
151
157
|
throw refuse('it is turned off');
|
|
@@ -177,7 +183,7 @@ export class ProviderAccountsService {
|
|
|
177
183
|
if (fromAccount === undefined || toAccount === undefined || fromAccount.kind !== kind) {
|
|
178
184
|
return false;
|
|
179
185
|
}
|
|
180
|
-
return canContinue({ id: fromId, account: fromAccount }, { id: toId, account: toAccount },
|
|
186
|
+
return canContinue({ id: fromId, account: fromAccount }, { id: toId, account: toAccount }, this.accountProviders(), this.env);
|
|
181
187
|
}
|
|
182
188
|
labelOf(id) {
|
|
183
189
|
return this.accounts[id]?.label ?? id;
|
|
@@ -278,6 +284,12 @@ export class ProviderAccountsService {
|
|
|
278
284
|
}
|
|
279
285
|
}
|
|
280
286
|
}
|
|
287
|
+
kinds() {
|
|
288
|
+
return this.providers.kinds?.() ?? KNOWN_AGENT_KINDS;
|
|
289
|
+
}
|
|
290
|
+
accountProviders() {
|
|
291
|
+
return { kinds: this.kinds(), get: (kind) => this.providers.get(kind) };
|
|
292
|
+
}
|
|
281
293
|
/* One change of the file at a time: a create and a save both read what the other just wrote. */
|
|
282
294
|
serial(work) {
|
|
283
295
|
const next = this.writes.then(work);
|
|
@@ -285,7 +297,7 @@ export class ProviderAccountsService {
|
|
|
285
297
|
return next;
|
|
286
298
|
}
|
|
287
299
|
async commit(accounts, isDirectory) {
|
|
288
|
-
const stored = acceptAccounts(accounts, this.stored,
|
|
300
|
+
const stored = acceptAccounts(accounts, this.stored, this.accountProviders(), this.env, isDirectory, this.host);
|
|
289
301
|
const { writes, removals } = this.planSecrets(stored);
|
|
290
302
|
for (const [key, value] of writes) {
|
|
291
303
|
await this.requireSecrets().write(key, value);
|
|
@@ -293,7 +305,7 @@ export class ProviderAccountsService {
|
|
|
293
305
|
}
|
|
294
306
|
await writeAccounts(this.path, stored);
|
|
295
307
|
this.stored = stored;
|
|
296
|
-
this.accounts = wireAccounts(stored);
|
|
308
|
+
this.accounts = wireAccounts(stored, this.kinds());
|
|
297
309
|
for (const key of removals) {
|
|
298
310
|
this.secretValues.delete(key);
|
|
299
311
|
await this.secrets?.remove(key).catch((e) => console.error(`Could not remove ${key} from the keychain:`, errorText(e)));
|
|
@@ -318,7 +330,7 @@ export class ProviderAccountsService {
|
|
|
318
330
|
const kept = new Set();
|
|
319
331
|
for (const [id, entry] of Object.entries(stored)) {
|
|
320
332
|
const kind = entry.kind;
|
|
321
|
-
if (!
|
|
333
|
+
if (!isOfferedKind(kind, this.kinds())) {
|
|
322
334
|
for (const name of sensitiveNames(entry)) {
|
|
323
335
|
kept.add(secretKey(id, name));
|
|
324
336
|
}
|
|
@@ -416,7 +428,7 @@ export class ProviderAccountsService {
|
|
|
416
428
|
return this.status(id, account, 'checking', { checkedAt: 0 });
|
|
417
429
|
}
|
|
418
430
|
status(id, account, state, fields = {}) {
|
|
419
|
-
const provider =
|
|
431
|
+
const provider = isOfferedKind(account.kind, this.kinds()) ? this.providers.get(account.kind) : null;
|
|
420
432
|
const folder = provider === null ? '' : accountFolder(id, account, provider, this.env);
|
|
421
433
|
return {
|
|
422
434
|
id,
|
|
@@ -461,7 +473,7 @@ export class ProviderAccountsService {
|
|
|
461
473
|
}
|
|
462
474
|
async check(id, account, installed) {
|
|
463
475
|
const kind = account.kind;
|
|
464
|
-
if (!
|
|
476
|
+
if (!isOfferedKind(kind, this.kinds())) {
|
|
465
477
|
return this.status(id, account, 'unavailable', { message: `This version of ${this.host.name} does not know this CLI` });
|
|
466
478
|
}
|
|
467
479
|
const provider = this.providers.get(kind);
|
|
@@ -494,6 +506,10 @@ export class ProviderAccountsService {
|
|
|
494
506
|
if (missing !== null) {
|
|
495
507
|
throw new Error(`The keychain does not give the value of ${missing}`);
|
|
496
508
|
}
|
|
509
|
+
// A CLI that signs in from its chat has no command that says who is signed in; its chat asks when it must.
|
|
510
|
+
if (provider.home.loginCommand === undefined) {
|
|
511
|
+
return this.status(id, account, 'ready', { message });
|
|
512
|
+
}
|
|
497
513
|
const reading = await this.ask(kind, provider.command, accountEnv(id, account, provider, this.env, values));
|
|
498
514
|
return this.status(id, account, reading.signedIn ? 'ready' : 'signed-out', {
|
|
499
515
|
email: reading.email,
|
|
@@ -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';
|