@shardflux/cli 0.20.0 → 0.22.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/CHANGELOG.md +214 -1
- package/README.md +215 -75
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +19 -15
- package/dist/account-pool.js +47 -96
- package/dist/account-scope.d.ts +65 -0
- package/dist/account-scope.js +246 -0
- package/dist/acp-login.d.ts +4 -3
- package/dist/acp-login.js +7 -5
- package/dist/acp-models.js +20 -15
- package/dist/acp.js +66 -13
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +86 -32
- package/dist/agent-config-return.d.ts +46 -2
- package/dist/agent-config-return.js +110 -4
- package/dist/agent-credentials.js +7 -3
- package/dist/agent-repository.d.ts +16 -3
- package/dist/agent-repository.js +149 -74
- package/dist/agent-setup.d.ts +14 -3
- package/dist/agent-setup.js +78 -36
- package/dist/agent-target-commands.js +24 -13
- package/dist/args.d.ts +1 -1
- package/dist/args.js +94 -54
- package/dist/bin.js +5 -1
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -14
- package/dist/claude-cloud-login.d.ts +24 -2
- package/dist/claude-cloud-login.js +151 -11
- package/dist/cli-proxy-forward.d.ts +2 -0
- package/dist/cli-proxy-forward.js +9 -2
- package/dist/cli-proxy-hub.d.ts +69 -25
- package/dist/cli-proxy-hub.js +231 -54
- package/dist/cli-proxy.d.ts +70 -1
- package/dist/cli-proxy.js +298 -32
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +5 -0
- package/dist/codex-cloud.js +88 -17
- package/dist/command-center-actions.js +5 -1
- package/dist/commands.js +171 -22
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +8 -5
- package/dist/conversation-terminal.d.ts +10 -2
- package/dist/conversation-terminal.js +77 -13
- package/dist/conversations.d.ts +73 -1
- package/dist/conversations.js +257 -23
- package/dist/device-bridge.d.ts +1 -1
- package/dist/device-bridge.js +6 -4
- package/dist/device-helper.js +43 -22
- package/dist/errors.d.ts +27 -1
- package/dist/errors.js +45 -5
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +108 -54
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +3 -1
- package/dist/initial-prompt.d.ts +8 -0
- package/dist/initial-prompt.js +43 -1
- package/dist/large-media.d.ts +14 -0
- package/dist/large-media.js +100 -0
- package/dist/linux-terminal.d.ts +5 -3
- package/dist/linux-terminal.js +6 -20
- package/dist/local-sessions.d.ts +26 -2
- package/dist/local-sessions.js +113 -24
- package/dist/machine-setup.d.ts +11 -1
- package/dist/machine-setup.js +69 -22
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +11 -13
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +63 -0
- package/dist/move-worktrees.js +201 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +116 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +59 -27
- package/dist/prepared-repository.d.ts +202 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1306 -0
- package/dist/project-preparation.d.ts +48 -0
- package/dist/project-preparation.js +304 -0
- package/dist/project-sync-manifest.d.ts +4 -0
- package/dist/project-sync-manifest.js +5 -1
- package/dist/project-sync-transfer.js +1 -1
- package/dist/project-sync.d.ts +19 -4
- package/dist/project-sync.js +31 -8
- package/dist/project-template-setup.d.ts +33 -1
- package/dist/project-template-setup.js +59 -2
- package/dist/secret-ask.d.ts +72 -0
- package/dist/secret-ask.js +351 -0
- package/dist/seed-phases.d.ts +8 -1
- package/dist/seed-phases.js +17 -2
- package/dist/setup-cache.d.ts +7 -0
- package/dist/setup-cache.js +32 -0
- package/dist/t3-launch.js +1 -4
- package/dist/t3-mode.d.ts +8 -2
- package/dist/t3-mode.js +19 -4
- package/dist/t3-phone.d.ts +4 -2
- package/dist/t3-phone.js +12 -4
- package/dist/terminal-apps.d.ts +157 -0
- package/dist/terminal-apps.js +561 -0
- package/dist/terminal-onboarding.js +6 -1
- package/dist/this-computer.d.ts +11 -0
- package/dist/this-computer.js +36 -0
- package/dist/thread-on-computer.js +4 -1
- package/dist/update.js +11 -3
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AgentCredentialMetadata, CellClient, Workspace } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx } from './commands.js';
|
|
3
|
+
/** Someone types into this terminal: not --json, a TTY with raw mode, and not a coding agent's shell. */
|
|
4
|
+
export declare function claudeTerminal(ctx: Ctx): boolean;
|
|
3
5
|
export declare function requireClaudeTerminal(ctx: Ctx): void;
|
|
4
6
|
export declare function cloudClaudeLogin(ctx: Ctx): Promise<AgentCredentialMetadata | undefined>;
|
|
5
7
|
/** Parse only complete provider authorize links, including the native OSC-8 link. */
|
|
@@ -21,6 +23,26 @@ export declare function runWorkspaceLogin(ctx: Ctx, ws: Workspace, cell: CellCli
|
|
|
21
23
|
cwd: string;
|
|
22
24
|
urls: (output: string) => string[];
|
|
23
25
|
}): Promise<boolean>;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The same sign-in without an attached terminal (0.20.1+: a coding agent's shell, --json, no TTY). The workspace PTY
|
|
28
|
+
* runs the login exactly as runWorkspaceLogin does; shard reads its output, opens the claude.ai link it prints, and
|
|
29
|
+
* opens a page on this computer where the person pastes the code claude.ai shows (the agent never sees it). The code
|
|
30
|
+
* and Enter go into the PTY; the page says "Signed in" once the login exited 0. True when it did.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runWorkspaceLoginHeadless(ctx: Ctx, cell: CellClient, login: {
|
|
33
|
+
session: string;
|
|
34
|
+
argv: string[];
|
|
35
|
+
env: Record<string, string>;
|
|
36
|
+
cwd: string;
|
|
37
|
+
urls: (output: string) => string[];
|
|
38
|
+
}, timeoutMs: number): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* A transient Linux profile holds the initial pair; it is removed before an agent starts. `headless` (0.20.1+): the
|
|
41
|
+
* sign-in runs through the pages shard opens (runWorkspaceLoginHeadless) instead of an attached terminal.
|
|
42
|
+
*/
|
|
43
|
+
export declare function loginClaudeInWorkspace(ctx: Ctx, ws: Workspace, opts?: {
|
|
44
|
+
headless?: {
|
|
45
|
+
timeoutMs: number;
|
|
46
|
+
};
|
|
47
|
+
}): Promise<AgentCredentialMetadata>;
|
|
26
48
|
export declare function ensureCloudClaude(ctx: Ctx, ws: Workspace): Promise<void>;
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
1
2
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
3
|
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
3
|
-
import { UsageError } from "./errors.js";
|
|
4
|
+
import { CodedError, UsageError } from "./errors.js";
|
|
4
5
|
import { openUrl } from "./browser.js";
|
|
5
|
-
import { attachTerminal, cellConnector, PtyOutputCursor, workspaceAsleep } from "./agent-terminal.js";
|
|
6
|
+
import { attachTerminal, cellConnector, PtyOutputCursor, reattachDelayMs, retryableAttach, workspaceAsleep } from "./agent-terminal.js";
|
|
6
7
|
import { codingAgentShell } from "./agent-setup.js";
|
|
7
|
-
|
|
8
|
-
const
|
|
8
|
+
import { announce, askInBrowser, openInBrowser } from "./secret-ask.js";
|
|
9
|
+
const NEED_TERMINAL = 'Claude sign-in needed: run shard agents credentials login claude, once for the account (it opens the claude.ai sign-in and a page for its code).';
|
|
10
|
+
const NEED_TERMINAL_HINT = 'a coding agent runs shard agents credentials login claude --json itself; its user signs in and pastes the code into the page it opens';
|
|
9
11
|
const VERSION = '2.1.289';
|
|
10
|
-
|
|
12
|
+
/** Someone types into this terminal: not --json, a TTY with raw mode, and not a coding agent's shell. */
|
|
13
|
+
export function claudeTerminal(ctx) {
|
|
11
14
|
// A coding agent's shell (0.17.1+) may be a PTY, but the person who reads the code is not typing into it.
|
|
12
|
-
|
|
15
|
+
return !ctx.json && ctx.io.stdin.isTTY === true && typeof ctx.io.stdin.setRawMode === 'function' && typeof ctx.io.stdin.on === 'function' && !codingAgentShell(ctx.io.env);
|
|
16
|
+
}
|
|
17
|
+
export function requireClaudeTerminal(ctx) {
|
|
18
|
+
if (!claudeTerminal(ctx))
|
|
13
19
|
throw new UsageError(NEED_TERMINAL, NEED_TERMINAL_HINT);
|
|
14
20
|
}
|
|
15
21
|
export async function cloudClaudeLogin(ctx) {
|
|
@@ -109,7 +115,7 @@ export async function runWorkspaceLogin(ctx, ws, cell, login) {
|
|
|
109
115
|
for (const url of login.urls(output))
|
|
110
116
|
if (!opened.has(url)) {
|
|
111
117
|
opened.add(url);
|
|
112
|
-
openUrl(url, ctx.io.env);
|
|
118
|
+
void openUrl(url, ctx.io.env);
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
yield message;
|
|
@@ -121,9 +127,141 @@ export async function runWorkspaceLogin(ctx, ws, cell, login) {
|
|
|
121
127
|
const ended = await cell.pty.get(login.session);
|
|
122
128
|
return code === 0 && ended.state === 'exited' && ended.exit_code === 0;
|
|
123
129
|
}
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
/** How long Claude may take to exchange the pasted code and exit. */
|
|
131
|
+
const CODE_EXCHANGE_MS = 60_000;
|
|
132
|
+
/**
|
|
133
|
+
* The same sign-in without an attached terminal (0.20.1+: a coding agent's shell, --json, no TTY). The workspace PTY
|
|
134
|
+
* runs the login exactly as runWorkspaceLogin does; shard reads its output, opens the claude.ai link it prints, and
|
|
135
|
+
* opens a page on this computer where the person pastes the code claude.ai shows (the agent never sees it). The code
|
|
136
|
+
* and Enter go into the PTY; the page says "Signed in" once the login exited 0. True when it did.
|
|
137
|
+
*/
|
|
138
|
+
export async function runWorkspaceLoginHeadless(ctx, cell, login, timeoutMs) {
|
|
139
|
+
await cell.pty.open({ session_id: login.session, argv: login.argv, env: login.env, cwd: login.cwd, rows: 24, cols: 100 });
|
|
140
|
+
const deadline = Date.now() + timeoutMs;
|
|
141
|
+
const stop = new AbortController();
|
|
142
|
+
const signal = AbortSignal.any([ctx.signal, stop.signal]);
|
|
143
|
+
// The PTY ended (or could not be read) before the code went in: the page closes with that.
|
|
144
|
+
const ended = new AbortController();
|
|
145
|
+
const connect = cellConnector(cell, login.session);
|
|
146
|
+
const cursor = new PtyOutputCursor();
|
|
147
|
+
const decoder = new StringDecoder('utf8');
|
|
148
|
+
const waiters = new Set();
|
|
149
|
+
const notify = () => { for (const wake of waiters)
|
|
150
|
+
wake(); waiters.clear(); };
|
|
151
|
+
let output = '';
|
|
152
|
+
let exit;
|
|
153
|
+
let failure;
|
|
154
|
+
let reading = true;
|
|
155
|
+
let codeSent = false;
|
|
156
|
+
const early = () => new CodedError('claude_sign_in_ended', 'The Claude sign-in ended before its code arrived.', 'run shard agents credentials login claude again');
|
|
157
|
+
const reader = (async () => {
|
|
158
|
+
let failures = 0;
|
|
159
|
+
while (!signal.aborted && exit === undefined) {
|
|
160
|
+
let connection;
|
|
161
|
+
try {
|
|
162
|
+
connection = await connect(cursor.offset, signal);
|
|
163
|
+
for await (const message of connection.messages()) {
|
|
164
|
+
failures = 0;
|
|
165
|
+
const bytes = cursor.consume(message);
|
|
166
|
+
if (bytes?.length)
|
|
167
|
+
output = (output + decoder.write(bytes)).slice(-16384);
|
|
168
|
+
if (message.type === 'exit') {
|
|
169
|
+
exit = message.session?.exit_code ?? null;
|
|
170
|
+
if (!codeSent)
|
|
171
|
+
ended.abort(early());
|
|
172
|
+
}
|
|
173
|
+
notify();
|
|
174
|
+
if (exit !== undefined || signal.aborted)
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (signal.aborted)
|
|
180
|
+
return;
|
|
181
|
+
if (!retryableAttach(error))
|
|
182
|
+
throw error;
|
|
183
|
+
failures += 1;
|
|
184
|
+
await new Promise((resolve) => {
|
|
185
|
+
const done = () => { clearTimeout(timer); signal.removeEventListener('abort', done); resolve(); };
|
|
186
|
+
const timer = setTimeout(done, reattachDelayMs(error, failures));
|
|
187
|
+
signal.addEventListener('abort', done, { once: true });
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
await connection?.close().catch(() => undefined);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
})().catch((error) => {
|
|
195
|
+
failure = error;
|
|
196
|
+
if (!codeSent)
|
|
197
|
+
ended.abort(error instanceof Error ? error : early());
|
|
198
|
+
}).finally(() => { reading = false; notify(); });
|
|
199
|
+
/** Until `ready()`, at most `ms`: false when it did not happen (time out, or the reader stopped). */
|
|
200
|
+
const until = async (ready, ms) => {
|
|
201
|
+
const end = Date.now() + ms;
|
|
202
|
+
for (;;) {
|
|
203
|
+
if (ready())
|
|
204
|
+
return true;
|
|
205
|
+
if (failure !== undefined)
|
|
206
|
+
throw failure instanceof Error ? failure : new Error('The Claude sign-in terminal failed.');
|
|
207
|
+
if (!reading || Date.now() >= end)
|
|
208
|
+
return false;
|
|
209
|
+
await new Promise((resolve) => {
|
|
210
|
+
const timer = setTimeout(wake, end - Date.now());
|
|
211
|
+
function wake() { clearTimeout(timer); waiters.delete(wake); resolve(); }
|
|
212
|
+
waiters.add(wake);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
try {
|
|
217
|
+
await until(() => login.urls(output).length > 0 || exit !== undefined, deadline - Date.now());
|
|
218
|
+
const url = login.urls(output)[0];
|
|
219
|
+
if (url === undefined)
|
|
220
|
+
return false;
|
|
221
|
+
const opened = await openInBrowser(ctx, url);
|
|
222
|
+
announce(ctx, { event: 'open', url, opened }, opened ? `Opened the Claude sign-in in your browser (or open: ${url})` : `Open the Claude sign-in: ${url}`);
|
|
223
|
+
await askInBrowser(ctx, {
|
|
224
|
+
kind: 'code',
|
|
225
|
+
title: 'Sign in to Claude',
|
|
226
|
+
purpose: 'Paste the code claude.ai shows after you sign in. Your coding agent never sees it.',
|
|
227
|
+
label: 'Code',
|
|
228
|
+
submit: 'Sign in',
|
|
229
|
+
done: 'Signed in to Claude',
|
|
230
|
+
failed: 'Not signed in',
|
|
231
|
+
link: { href: url, text: 'Open the Claude sign-in again' },
|
|
232
|
+
doing: 'to paste the Claude code',
|
|
233
|
+
event: 'ask',
|
|
234
|
+
maxBytes: 4096,
|
|
235
|
+
timeoutMs: Math.max(1_000, deadline - Date.now()),
|
|
236
|
+
signal: ended.signal,
|
|
237
|
+
// Once the code went in, the login either finishes or ends: there is nothing to retry on the page.
|
|
238
|
+
retry: () => false,
|
|
239
|
+
use: async (code) => {
|
|
240
|
+
if (exit !== undefined)
|
|
241
|
+
throw early();
|
|
242
|
+
codeSent = true;
|
|
243
|
+
await cell.pty.input(login.session, `${code}\r`);
|
|
244
|
+
if (!await until(() => exit !== undefined, CODE_EXCHANGE_MS))
|
|
245
|
+
throw new CodedError('claude_sign_in_timeout', 'Claude did not finish the sign-in within a minute of the code.', 'run shard agents credentials login claude again');
|
|
246
|
+
if (exit !== 0)
|
|
247
|
+
throw new CodedError('claude_code_refused', `Claude did not accept the code (exit ${exit ?? 'by signal'}).`, 'run shard agents credentials login claude again for a new code');
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
stop.abort();
|
|
253
|
+
await reader;
|
|
254
|
+
}
|
|
255
|
+
const session = await cell.pty.get(login.session);
|
|
256
|
+
return session.state === 'exited' && session.exit_code === 0;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* A transient Linux profile holds the initial pair; it is removed before an agent starts. `headless` (0.20.1+): the
|
|
260
|
+
* sign-in runs through the pages shard opens (runWorkspaceLoginHeadless) instead of an attached terminal.
|
|
261
|
+
*/
|
|
262
|
+
export async function loginClaudeInWorkspace(ctx, ws, opts = {}) {
|
|
263
|
+
if (!opts.headless)
|
|
264
|
+
requireClaudeTerminal(ctx);
|
|
127
265
|
const api = ctx.cloud().agentCredentials;
|
|
128
266
|
let claim;
|
|
129
267
|
try {
|
|
@@ -147,7 +285,9 @@ export async function loginClaudeInWorkspace(ctx, ws) {
|
|
|
147
285
|
const version = await cell.exec.run(['claude', '--version'], { env, cwd: root, signal: ctx.signal, timeoutMs: 10_000 });
|
|
148
286
|
if (version.exitCode !== 0 || version.stdout.trim() !== `${VERSION} (Claude Code)`)
|
|
149
287
|
throw new UsageError(`Cloud Claude sign-in requires Claude Code ${VERSION} in the workspace image.`);
|
|
150
|
-
|
|
288
|
+
const login = { session, argv: ['claude', 'auth', 'login', '--claudeai'], env, cwd: root, urls: claudeLoginUrls };
|
|
289
|
+
const signedIn = opts.headless ? await runWorkspaceLoginHeadless(ctx, cell, login, opts.headless.timeoutMs) : await runWorkspaceLogin(ctx, ws, cell, login);
|
|
290
|
+
if (!signedIn)
|
|
151
291
|
throw new UsageError('Claude cloud sign-in was interrupted. Run shard agents credentials login claude again.');
|
|
152
292
|
const credential = JSON.parse(await cell.files.readText(`${root}/.claude/.credentials.json`));
|
|
153
293
|
const config = JSON.parse(await cell.files.readText(`${root}/.claude/.claude.json`));
|
|
@@ -13,6 +13,8 @@ export interface ForwarderOptions {
|
|
|
13
13
|
* for once, so a URL that stays invalid is answered as is.
|
|
14
14
|
*/
|
|
15
15
|
renew?: () => Promise<void>;
|
|
16
|
+
/** Called as each request (or upgrade) arrives, before it is forwarded: the hub is about to be awake. */
|
|
17
|
+
onRequest?: () => void;
|
|
16
18
|
host?: string;
|
|
17
19
|
port?: number;
|
|
18
20
|
/** How long one request may be retried for (default 60 s, plus the time a repair takes). */
|
|
@@ -97,8 +97,8 @@ export class CliProxyForwarder {
|
|
|
97
97
|
this.dropIdleConnections();
|
|
98
98
|
tick = now;
|
|
99
99
|
}, 5_000).unref();
|
|
100
|
-
this.#server = createServer((req, res) => void this.#handle(req, res));
|
|
101
|
-
this.#server.on('upgrade', (req, socket, head) => void this.#upgrade(req, socket, head));
|
|
100
|
+
this.#server = createServer((req, res) => { this.#arrived(); void this.#handle(req, res); });
|
|
101
|
+
this.#server.on('upgrade', (req, socket, head) => { this.#arrived(); void this.#upgrade(req, socket, head); });
|
|
102
102
|
// Long streams (SSE, Responses) must not be cut by the server's own timeouts.
|
|
103
103
|
this.#server.requestTimeout = 0;
|
|
104
104
|
this.#server.headersTimeout = 60_000;
|
|
@@ -327,6 +327,13 @@ export class CliProxyForwarder {
|
|
|
327
327
|
delay = Math.min(delay * 2, 8000);
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
|
+
/** onRequest never fails or delays a request. */
|
|
331
|
+
#arrived() {
|
|
332
|
+
try {
|
|
333
|
+
this.#o.onRequest?.();
|
|
334
|
+
}
|
|
335
|
+
catch { /* the hook is advisory */ }
|
|
336
|
+
}
|
|
330
337
|
async #handle(req, res) {
|
|
331
338
|
if (pathOnly(req.url) === IDENTITY_PATH) {
|
|
332
339
|
const info = this.#o.hub();
|
package/dist/cli-proxy-hub.d.ts
CHANGED
|
@@ -32,24 +32,20 @@ export declare const HUB_TEMPLATE = "codex-connector";
|
|
|
32
32
|
/**
|
|
33
33
|
* The codex-connector version a hub runs at least: the first with the hub's boot service and an sf-agent that serves
|
|
34
34
|
* the VM port (cli-proxy-filter). New hubs are created on it; an older hub is upgraded in place (its home, and so its
|
|
35
|
-
* logins and settings, stay) before VMs get the hub's address.
|
|
35
|
+
* logins and settings, stay) before VMs get the hub's address. A hub on a newer version keeps it (never downgraded).
|
|
36
36
|
*/
|
|
37
37
|
export declare const HUB_TEMPLATE_VERSION = 4;
|
|
38
|
-
/**
|
|
39
|
-
* The codex-connector version whose sf-agent runs the account pool router: a hub set up for pooled accounts is created
|
|
40
|
-
* on it, or upgraded to it. HUB_TEMPLATE_VERSION moves to it once it is registered in every environment.
|
|
41
|
-
*/
|
|
42
|
-
export declare const POOL_HUB_TEMPLATE_VERSION = 5;
|
|
43
38
|
export declare const HUB_CAPS: Caps;
|
|
44
39
|
export declare const HUB_INFO_FILE = "cli-proxy.json";
|
|
45
40
|
/** Paths in the hub workspace. */
|
|
46
41
|
export declare const HUB_DIR = "/home/user/.cli-proxy-hub";
|
|
47
42
|
export declare const HUB_AUTH_DIR = "/home/user/.cli-proxy-api";
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Each provider's active login: `{ "<provider type>": "<auth file, relative to the auth directory>" }` (0600). The one
|
|
45
|
+
* login of each provider that stays enabled in the hub; every other login of that provider is kept there with
|
|
46
|
+
* `"disabled": true`. Written by the CLI and by the hub's sf-agent, both under the hub's `logins.lock`.
|
|
47
|
+
*/
|
|
48
|
+
export declare const HUB_ACTIVE_FILE = "/home/user/.cli-proxy-hub/active.json";
|
|
53
49
|
export interface HubInfo {
|
|
54
50
|
version: 1;
|
|
55
51
|
api_url: string;
|
|
@@ -112,7 +108,10 @@ export interface HubWorkspace {
|
|
|
112
108
|
write(path: string, data: string, mode: '0600' | '0700'): Promise<void>;
|
|
113
109
|
/** Names in a directory, or null when it does not exist. */
|
|
114
110
|
list(dir: string): Promise<string[] | null>;
|
|
115
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* The hub's auth files, as paths relative to its auth directory (subdirectories included, as CLIProxyAPI reads them),
|
|
113
|
+
* oldest first: by modification time, then by path.
|
|
114
|
+
*/
|
|
116
115
|
authFiles(): Promise<string[]>;
|
|
117
116
|
/** Exposes the port (idempotent): when its callback URL was created, null without one. */
|
|
118
117
|
exposePort(port: number): Promise<{
|
|
@@ -167,6 +166,8 @@ export declare function sdkHubCloud(cloud: Shardflux, opts?: {
|
|
|
167
166
|
agentLabel?: string;
|
|
168
167
|
homeProject?: (organizationId: string) => Promise<HomeProject | null>;
|
|
169
168
|
}): HubCloud;
|
|
169
|
+
/** The hub's auth files, oldest first (modification time, then path in byte order), one relative path per line. */
|
|
170
|
+
export declare const AUTH_FILES_SCRIPT = "set -euo pipefail\n[ -d /home/user/.cli-proxy-api ] || exit 0; cd /home/user/.cli-proxy-api\nfind . -type f -name '*.json' -printf '%T@ %P\\n' | LC_ALL=C sort -t ' ' -k1,1n -k2 | cut -d ' ' -f 2-\n";
|
|
170
171
|
/** Installs the pinned release once: download, verify its SHA-256, then move the binary into place. */
|
|
171
172
|
export declare const INSTALL_SCRIPT: string;
|
|
172
173
|
/**
|
|
@@ -228,15 +229,6 @@ export interface EnsureOptions {
|
|
|
228
229
|
progress?: (message: string) => void;
|
|
229
230
|
/** Random management key (tests). */
|
|
230
231
|
randomKey?: () => string;
|
|
231
|
-
/**
|
|
232
|
-
* Pool mode (account pool): the hub's router gets the API URL and a hub token, and CLIProxyAPI passes the providers'
|
|
233
|
-
* rate-limit headers through so the router reads usage on every answer. `hubToken` mints a token for the hub
|
|
234
|
-
* workspace (`POST /v1/agent-pool/hub-token`); it is called when the hub has none, or always with `renew`.
|
|
235
|
-
*/
|
|
236
|
-
pool?: {
|
|
237
|
-
hubToken: (workspaceId: string) => Promise<string>;
|
|
238
|
-
renew?: boolean;
|
|
239
|
-
};
|
|
240
232
|
}
|
|
241
233
|
export interface EnsureResult {
|
|
242
234
|
info: HubInfo;
|
|
@@ -246,6 +238,8 @@ export interface EnsureResult {
|
|
|
246
238
|
callbackReplaced: boolean;
|
|
247
239
|
/** The config written now; null when the hub kept its own. */
|
|
248
240
|
config: HubConfig | null;
|
|
241
|
+
/** The hub's logins, one active per provider (the others kept disabled). */
|
|
242
|
+
logins: HubLogin[];
|
|
249
243
|
}
|
|
250
244
|
/** The hub could not be made ready (exit 1, code `cli_proxy_hub`). */
|
|
251
245
|
export declare class HubError extends CodedError {
|
|
@@ -258,11 +252,61 @@ export declare class HubError extends CodedError {
|
|
|
258
252
|
*/
|
|
259
253
|
export declare function ensureCliProxyHub(cloud: Shardflux | HubCloud, opts: EnsureOptions): Promise<EnsureResult>;
|
|
260
254
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
255
|
+
* A login in the hub: one auth file. Only its provider, email and state are ever shown, never its tokens.
|
|
256
|
+
*
|
|
257
|
+
* CLIProxyAPI spreads requests across every enabled login of a provider (round-robin, fill-first) and moves to another
|
|
258
|
+
* one on a 429, so the hub keeps exactly one enabled per provider: the user's active login. Every other login of that
|
|
259
|
+
* provider stays in the hub with `"disabled": true` (CLIProxyAPI 8.0.23 never uses a disabled auth file) until the
|
|
260
|
+
* user makes it active (`shard cli-proxy use`).
|
|
261
|
+
*/
|
|
262
|
+
export interface HubLogin {
|
|
263
|
+
/** Path relative to the auth directory. */
|
|
264
|
+
file: string;
|
|
265
|
+
/** The auth file's `type` (claude, codex, gemini…), lowercase; null when it has none. */
|
|
266
|
+
provider: string | null;
|
|
267
|
+
email: string | null;
|
|
268
|
+
/** The file carries `"disabled": true`. */
|
|
269
|
+
disabled: boolean;
|
|
270
|
+
/** The one login of its provider that serves requests. */
|
|
271
|
+
active: boolean;
|
|
272
|
+
}
|
|
273
|
+
/** Parses active.json: provider type → auth file. Anything else (missing, not JSON, other values) counts as empty. */
|
|
274
|
+
export declare function parseActive(text: string | null): Record<string, string>;
|
|
275
|
+
/**
|
|
276
|
+
* The hub's logins, oldest first, as they are (nothing changes): `active` marks the one active.json names while it is
|
|
277
|
+
* enabled. Files the retired account pool router left are not listed (they stay disabled).
|
|
278
|
+
*/
|
|
279
|
+
export declare function listHubLogins(ws: HubWorkspace): Promise<HubLogin[]>;
|
|
280
|
+
/** The auth file's text with `disabled` set (true) or removed (false), every other field kept; null when unchanged. */
|
|
281
|
+
export declare function withDisabled(text: string, disabled: boolean): string | null;
|
|
282
|
+
/**
|
|
283
|
+
* Replaces one auth file atomically (0600, a temporary file in its folder then rename), only while it still holds what
|
|
284
|
+
* the device read ($2, its SHA-256): CLIProxyAPI rewrites a file when it refreshes the login, and a newer token must
|
|
285
|
+
* never be overwritten. The modification time stays, so the order of the hub's logins (oldest first) does not change.
|
|
286
|
+
* $1 is the path relative to the auth directory, the new content comes on standard input. Prints `written`, `changed`
|
|
287
|
+
* or `gone`.
|
|
288
|
+
*/
|
|
289
|
+
export declare const SET_AUTH_SCRIPT = "set -euo pipefail\na=/home/user/.cli-proxy-api; rel=$1; want=$2\ncase \"$rel\" in ''|/*|*..*) echo \"not a path inside the auth folder\" >&2; exit 2 ;; esac\nf=\"$a/$rel\"\n[ -f \"$f\" ] || { echo gone; exit 0; }\ntmp=$(mktemp \"$(dirname \"$f\")/.writing.XXXXXX\"); trap 'rm -f \"$tmp\"' EXIT\nchmod 0600 \"$tmp\"; cat > \"$tmp\"; touch -r \"$f\" \"$tmp\"\nif [ \"$(sha256sum < \"$f\" | cut -d' ' -f1)\" != \"$want\" ]; then echo changed; exit 0; fi\nmv -f \"$tmp\" \"$f\"; trap - EXIT\necho written\n";
|
|
290
|
+
/** Writes active.json atomically (0600) from standard input. */
|
|
291
|
+
export declare const ACTIVE_SCRIPT = "set -euo pipefail\ndir=/home/user/.cli-proxy-hub; mkdir -p \"$dir\"; umask 077\ntmp=$(mktemp \"$dir/.active.XXXXXX\"); trap 'rm -f \"$tmp\"' EXIT\ncat > \"$tmp\"; mv -f \"$tmp\" \"$dir/active.json\"; trap - EXIT\necho written\n";
|
|
292
|
+
/**
|
|
293
|
+
* Keeps exactly one login enabled per provider (call it under the hub's `logins` lock, so two computers do not race):
|
|
294
|
+
* `prefer[provider]` when given, else the one active.json names while it is present and enabled, else the oldest.
|
|
295
|
+
* active.json records the choice first, then every other login of that provider is disabled (all other fields kept), then
|
|
296
|
+
* the chosen one is enabled. Files without a `type` are left alone; the retired pool router's files are
|
|
297
|
+
* disabled and never chosen. Returns the logins as they are now, oldest first (without the pool router's).
|
|
298
|
+
*/
|
|
299
|
+
export declare function enforceActiveLogins(ws: HubWorkspace, prefer?: Record<string, string>): Promise<HubLogin[]>;
|
|
300
|
+
/**
|
|
301
|
+
* One login per provider on a hub that may predate it, without waking the hub: a hub that is not running is left
|
|
302
|
+
* alone (it is checked again later). The forwarder on every connected computer runs this at start and every 15
|
|
303
|
+
* minutes, so a hub set up before this rule stops spreading requests across logins without anyone running a command.
|
|
264
304
|
*/
|
|
265
|
-
export declare function
|
|
305
|
+
export declare function enforceHubLoginsIfRunning(hub: HubCloud, workspaceId: string): Promise<'enforced' | 'asleep' | 'missing'>;
|
|
306
|
+
/** Removes the router's files; prints `retired` when its hub token was there (its sf-agent then starts again without it). */
|
|
307
|
+
export declare const RETIRE_POOL_SCRIPT: string;
|
|
308
|
+
/** The config without the router's own client key (`api-keys`, or v8 `access.api-keys`); the text itself when absent. */
|
|
309
|
+
export declare function withoutClientKey(text: string, key: string): string;
|
|
266
310
|
/** The callback URL as the hub stores it (`callback.json`, 0600): its secret is shown only when it is created. */
|
|
267
311
|
export interface StoredCallback {
|
|
268
312
|
callback_url: string;
|