@shardflux/cli 0.22.0 → 0.23.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 +98 -0
- package/README.md +37 -10
- package/dist/acp-journal.js +58 -6
- package/dist/acp-login.d.ts +10 -3
- package/dist/acp-login.js +31 -4
- package/dist/acp-models.d.ts +6 -1
- package/dist/acp-models.js +21 -16
- package/dist/acp.js +60 -7
- package/dist/agent-commands.js +29 -15
- package/dist/agent-target-commands.d.ts +20 -1
- package/dist/agent-target-commands.js +49 -18
- package/dist/agent-targets.js +6 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +8 -7
- package/dist/cli-proxy-forward.d.ts +0 -2
- package/dist/cli-proxy-forward.js +2 -9
- package/dist/cli-proxy-hub.d.ts +22 -30
- package/dist/cli-proxy-hub.js +65 -94
- package/dist/cli-proxy.d.ts +12 -38
- package/dist/cli-proxy.js +51 -168
- package/dist/codex-cloud.d.ts +9 -4
- package/dist/codex-cloud.js +47 -14
- package/dist/codex-linux.d.ts +14 -0
- package/dist/codex-linux.js +69 -0
- package/dist/command-center-actions.js +13 -4
- package/dist/command-center-model.d.ts +2 -0
- package/dist/command-center-model.js +6 -1
- package/dist/command-center.js +3 -3
- package/dist/commands.js +2 -2
- package/dist/conversation-terminal.js +3 -2
- package/dist/conversations.d.ts +14 -1
- package/dist/conversations.js +55 -6
- package/dist/desktop-linux.js +16 -3
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +5 -2
- package/dist/filesystem-commands.js +25 -3
- package/dist/menubar.d.ts +6 -5
- package/dist/menubar.js +38 -28
- package/dist/move-worktrees.d.ts +13 -7
- package/dist/move-worktrees.js +68 -17
- package/dist/move.js +20 -25
- package/dist/onboarding.js +4 -2
- package/dist/prepared-repository.d.ts +2 -0
- package/dist/prepared-repository.js +1 -1
- package/dist/prepared-repository.py +3 -2
- package/dist/project-preparation.d.ts +14 -0
- package/dist/project-preparation.js +16 -4
- package/dist/project-sync.d.ts +3 -0
- package/dist/project-sync.js +4 -1
- package/dist/t3-launch.d.ts +2 -1
- package/dist/t3-launch.js +11 -2
- package/dist/t3-proxy.d.ts +81 -0
- package/dist/t3-proxy.js +325 -0
- package/dist/terminal-apps.d.ts +11 -0
- package/dist/terminal-apps.js +18 -5
- package/dist/update.d.ts +2 -0
- package/dist/update.js +32 -6
- package/package.json +5 -5
package/dist/acp.js
CHANGED
|
@@ -6,7 +6,8 @@ import { AcpRpc, RpcError, objectValue } from "./acp-rpc.js";
|
|
|
6
6
|
import { ACP_PROBE_CATALOG, nativeCurrentModel, nativeModelOptions, withoutNativeModel } from "./acp-catalog.js";
|
|
7
7
|
import { ACP_LIFECYCLE, AcpModelCache, acpScope } from "./acp-models.js";
|
|
8
8
|
import { AcpJournal } from "./acp-journal.js";
|
|
9
|
-
import { getConversation, conversationWorkspace, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
|
|
9
|
+
import { getConversation, conversationWorkspace, preparedInFlight, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
|
|
10
|
+
import { seedEvent } from "./seed-phases.js";
|
|
10
11
|
import { projectIdentity } from "./project-identity.js";
|
|
11
12
|
import { projectSyncBoundary } from "./project-sync.js";
|
|
12
13
|
import { localManifest } from "./project-sync-manifest.js";
|
|
@@ -18,6 +19,7 @@ import { ACP_LOGIN_METHOD, acpSignedIn } from "./acp-login.js";
|
|
|
18
19
|
import { ensureMenubar, MenubarManager } from "./menubar.js";
|
|
19
20
|
import { syncAgentConfigAfterStart } from "./agent-config-sync.js";
|
|
20
21
|
import { savedMachine } from "./filesystem-state.js";
|
|
22
|
+
import { accountVmProxy, prepareVmProxy } from "./t3-proxy.js";
|
|
21
23
|
const PREFIX = 'shardflux:';
|
|
22
24
|
export function acpModel(app, id) { return `${PREFIX}${app}:${id}`; }
|
|
23
25
|
export function parseAcpModel(value) {
|
|
@@ -82,6 +84,7 @@ export class ShardfluxAcp {
|
|
|
82
84
|
#sessions = new Map();
|
|
83
85
|
#lifecycle;
|
|
84
86
|
#cache;
|
|
87
|
+
#proxied = new Map();
|
|
85
88
|
#initialized = false;
|
|
86
89
|
#probe = false;
|
|
87
90
|
constructor(ctx, client, errors = acpError, lifecycle = ACP_LIFECYCLE) { this.#ctx = ctx; this.#client = client; this.#store = new AgentStateStore(ctx.store.dir); this.#cache = new AcpModelCache(ctx.store.dir); this.#errors = errors; this.#lifecycle = lifecycle; }
|
|
@@ -253,6 +256,7 @@ export class ShardfluxAcp {
|
|
|
253
256
|
}
|
|
254
257
|
session.preparing = (async () => {
|
|
255
258
|
await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
|
|
259
|
+
const proxy = this.#proxy();
|
|
256
260
|
const catalog = await this.#lifecycle.prepare(ctx, record.folder, `acp-${record.id.slice(3).replaceAll('-', '')}`, {
|
|
257
261
|
allocated: (c, view) => {
|
|
258
262
|
session.conversation = c;
|
|
@@ -272,6 +276,7 @@ export class ShardfluxAcp {
|
|
|
272
276
|
},
|
|
273
277
|
ready: ws => { session.ws = ws; record.scope = acpScope(ctx.apiUrl, ws.data); this.#cache.remember(ctx, record.folder, record.scope); },
|
|
274
278
|
prepared: () => { session.seedReady = true; prepared(); },
|
|
279
|
+
proxy: { env: { ...proxy.env.claude, ...proxy.env.codex }, ready: ws => this.#proxyReady(ws, proxy) },
|
|
275
280
|
});
|
|
276
281
|
ctx.signal.throwIfAborted();
|
|
277
282
|
if (!record.scope)
|
|
@@ -371,13 +376,20 @@ export class ShardfluxAcp {
|
|
|
371
376
|
}
|
|
372
377
|
async #start(session) {
|
|
373
378
|
const ctx = this.#ctx, { record } = session, app = record.app ?? parseAcpModel(record.model).app;
|
|
379
|
+
// SHARDFLUX_TRACE_SEED: when each step of the connection ends (one line each, wall clock).
|
|
380
|
+
const mark = (name) => seedEvent(`acp_${name}`, Date.now());
|
|
381
|
+
mark('connect');
|
|
374
382
|
record.app = app;
|
|
375
383
|
await session.transport?.close();
|
|
376
|
-
// The
|
|
384
|
+
// The user's T3 Code proxy setup, read again for every connection; the agent's variables apply when its process starts.
|
|
385
|
+
const proxy = this.#proxy();
|
|
386
|
+
// The sign-in is checked beside reading the conversation and its VM; it passes before anything starts in the VM. A
|
|
387
|
+
// proxy with its own credential needs no Shardflux Claude sign-in.
|
|
377
388
|
const signIn = (async () => {
|
|
378
389
|
await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
|
|
379
|
-
if (app === 'claude' && !(await cloudClaudeLogin(ctx)))
|
|
380
|
-
throw new RpcError(-32000, 'Authentication required: connect Claude Code.', { code: 'unauthenticated', fix: SIGN_IN });
|
|
390
|
+
if (app === 'claude' && !proxy.claudeCredential && !(await cloudClaudeLogin(ctx))) {
|
|
391
|
+
throw new RpcError(-32000, proxy.importable.includes('claude') ? 'Authentication required: connect Claude Code, or move your CLIProxyAPI into your account (shard cli-proxy import).' : 'Authentication required: connect Claude Code.', { code: 'unauthenticated', fix: SIGN_IN });
|
|
392
|
+
}
|
|
381
393
|
})();
|
|
382
394
|
signIn.catch(() => undefined);
|
|
383
395
|
if (!record.conversationId) {
|
|
@@ -394,25 +406,36 @@ export class ShardfluxAcp {
|
|
|
394
406
|
session.conversation = await resumeConversation(ctx, current);
|
|
395
407
|
session.ws = await conversationWorkspace(ctx, session.conversation);
|
|
396
408
|
rememberConversation(ctx, session.conversation, record.folder);
|
|
409
|
+
mark('conversation');
|
|
397
410
|
}
|
|
398
411
|
await signIn;
|
|
412
|
+
mark('signed_in');
|
|
399
413
|
let c = session.conversation;
|
|
400
414
|
const cell = session.ws.cell();
|
|
401
415
|
const menubar = ensureMenubar(ctx.store.dir, ctx.apiUrl, new MenubarManager({ home: ctx.io.env.HOME, signal: ctx.signal }));
|
|
402
416
|
await restoreConversationBinding(ctx, c, session.ws);
|
|
417
|
+
mark('binding');
|
|
403
418
|
// The project files are reconciled, device approvals served and the supervisor launched side by side; the native
|
|
404
|
-
// session starts once
|
|
419
|
+
// session starts once the supervisor is up, and the connection is done (the prompt goes) once the files are
|
|
420
|
+
// reconciled too (0.21.1+).
|
|
405
421
|
const files = (async () => {
|
|
406
422
|
const boundary = await projectSyncBoundary({ ...ctx, signal: AbortSignal.any([ctx.signal, session.stop.signal]) }, session.ws, projectIdentity(record.folder), {
|
|
407
423
|
root: c.worktree_path, agent: record.id.replace(/^sf-/, 'acp-'), seed: false,
|
|
408
424
|
stateKey: `acp:${ctx.apiUrl}:${record.id}:${record.folder}`, ...(session.hashes ? { cache: session.hashes } : {}),
|
|
409
425
|
// Before its first prompt the VM holds what this computer seeded it with, so edits made here since are not conflicts.
|
|
410
426
|
vmBaseline: !record.prompted,
|
|
427
|
+
// Large media of a prepared start that are still on their way land by themselves (0.22.0+).
|
|
428
|
+
inFlight: () => preparedInFlight(ctx, c.id),
|
|
411
429
|
});
|
|
412
430
|
await boundary.reconcile();
|
|
431
|
+
mark('files');
|
|
413
432
|
return boundary;
|
|
414
433
|
})();
|
|
415
|
-
|
|
434
|
+
// The agent starts with the proxy setup's variables, once the VM files it reads are in place.
|
|
435
|
+
const env = proxy.env[app];
|
|
436
|
+
const launch = this.#proxyReady(session.ws, proxy).then(() => cell.exec.run(['sf-agent', 'acp', app, '--conversation', c.id, '--detach'], {
|
|
437
|
+
cwd: c.worktree_path, timeoutMs: 30_000, signal: ctx.signal, maxOutputBytes: 1 << 20, ...(Object.keys(env).length ? { env } : {})
|
|
438
|
+
}));
|
|
416
439
|
files.catch(() => undefined);
|
|
417
440
|
launch.catch(() => undefined);
|
|
418
441
|
session.approvals ??= await serveAcpDeviceApprovals(ctx.store.dir, c.id, async (approval) => {
|
|
@@ -422,13 +445,15 @@ export class ShardfluxAcp {
|
|
|
422
445
|
const outcome = objectValue(result.outcome);
|
|
423
446
|
return outcome.outcome === 'selected' && (outcome.optionId === 'once' || outcome.optionId === 'always') ? outcome.optionId : 'deny';
|
|
424
447
|
});
|
|
425
|
-
|
|
448
|
+
mark('approvals');
|
|
426
449
|
const launched = await launch;
|
|
450
|
+
mark('launched');
|
|
427
451
|
await menubar;
|
|
428
452
|
if (launched.exitCode !== 0)
|
|
429
453
|
throw new Error(`ACP supervisor could not attach: ${launched.stderr.trim()}`);
|
|
430
454
|
const [claimed, statusText] = await Promise.all([getConversation(ctx, c.id), cell.files.readText(`/home/user/.local/share/shardflux/acp/${c.id}/status.json`, { length: 8192 })]);
|
|
431
455
|
c = claimed;
|
|
456
|
+
mark('status');
|
|
432
457
|
if (c.harness !== app)
|
|
433
458
|
throw new Error('The guest did not claim this conversation harness.');
|
|
434
459
|
session.conversation = c;
|
|
@@ -476,13 +501,16 @@ export class ShardfluxAcp {
|
|
|
476
501
|
};
|
|
477
502
|
session.transport = transport;
|
|
478
503
|
await transport.start();
|
|
504
|
+
mark('journal');
|
|
479
505
|
if (initialize) {
|
|
480
506
|
const initialized = objectValue(await transport.rpc.request('initialize', { protocolVersion: 1, clientInfo: { name: 'shardflux', version: CLI_VERSION }, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false } }));
|
|
481
507
|
if (initialized.protocolVersion !== 1)
|
|
482
508
|
throw new Error('Agent protocol version changed');
|
|
509
|
+
mark('initialized');
|
|
483
510
|
const nativeId = c.harness_session_id ?? record.nativeId;
|
|
484
511
|
const result = objectValue(await transport.rpc.request(nativeId ? 'session/load' : 'session/new', { cwd: c.worktree_path, mcpServers: [{ name: 'shardflux', command: '/usr/local/bin/sf-agent', args: ['conversation', 'mcp'], env: [{ name: 'SF_CONVERSATION_ID', value: c.id }] }], ...(nativeId ? { sessionId: nativeId } : {}) }));
|
|
485
512
|
const id = result.sessionId ?? nativeId;
|
|
513
|
+
mark('session');
|
|
486
514
|
if (typeof id !== 'string' || !id)
|
|
487
515
|
throw new Error('Agent returned no native session id');
|
|
488
516
|
record.nativeId = id;
|
|
@@ -505,6 +533,31 @@ export class ShardfluxAcp {
|
|
|
505
533
|
this.#cacheSession(session);
|
|
506
534
|
await this.#modelUpdate(session);
|
|
507
535
|
}
|
|
536
|
+
// Not connected until the files are: a failed reconcile closes the journal, so the next use connects again.
|
|
537
|
+
try {
|
|
538
|
+
session.files = await files;
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
await session.transport.close();
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
mark('files_ready');
|
|
545
|
+
}
|
|
546
|
+
/** The T3 Code proxy setup as a conversation VM follows it. Its credentials are kept out of every message. */
|
|
547
|
+
#proxy() { return accountVmProxy(this.#ctx); }
|
|
548
|
+
/** Puts the proxy's VM files in place once per VM and setup (prepareVmProxy). */
|
|
549
|
+
#proxyReady(ws, plan) {
|
|
550
|
+
if (!plan.hubInfo && !plan.codexConfig)
|
|
551
|
+
return Promise.resolve();
|
|
552
|
+
const key = JSON.stringify([plan.hubInfo, plan.codexConfig]);
|
|
553
|
+
const done = this.#proxied.get(ws.id);
|
|
554
|
+
if (done?.key === key)
|
|
555
|
+
return done.ready;
|
|
556
|
+
const ready = prepareVmProxy(ws.cell(), plan);
|
|
557
|
+
this.#proxied.set(ws.id, { key, ready });
|
|
558
|
+
ready.catch(() => { if (this.#proxied.get(ws.id)?.ready === ready)
|
|
559
|
+
this.#proxied.delete(ws.id); });
|
|
560
|
+
return ready;
|
|
508
561
|
}
|
|
509
562
|
async #close(session) {
|
|
510
563
|
this.#sessions.delete(session.record.id);
|
package/dist/agent-commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { linuxTerminalSession } from "./linux-terminal.js";
|
|
2
|
-
import { attachable, currentTerminal, openShardInTerminal, referenceFolder, reportTerminalOpen, storedTerminal } from "./terminal-apps.js";
|
|
2
|
+
import { attachable, currentTerminal, handedToTerminal, openShardInTerminal, referenceFolder, reportTerminalOpen, storedTerminal } from "./terminal-apps.js";
|
|
3
3
|
import { repositoryName } from "./files-mirror.js";
|
|
4
4
|
import { prepareSetupCache, recordSetupCacheFailure } from "./setup-cache.js";
|
|
5
5
|
import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
|
|
@@ -30,7 +30,7 @@ import { openInFor } from "./t3-mode.js";
|
|
|
30
30
|
import { storedTitlePrefix } from "./client-settings.js";
|
|
31
31
|
import { openClaudeDesktopSession, openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
32
32
|
import { guestFirstPrompt, INBOX_MAX_CHARS, submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
33
|
-
import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
33
|
+
import { conversationForOpen, conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
34
34
|
import { conversationClient } from "./conversation-commands.js";
|
|
35
35
|
import { conversationTerminal, reopenConversation } from "./conversation-terminal.js";
|
|
36
36
|
import { CHOICE_PENDING, choiceReport, hasChoice, launchChoice, modelChoice, runFlags } from "./agent-models.js";
|
|
@@ -325,7 +325,8 @@ export async function launchProjectWith(ctx, values, [path = './', explicitApp],
|
|
|
325
325
|
* terminal), and `--in terminal` typed in a terminal other than the chosen one.
|
|
326
326
|
*/
|
|
327
327
|
function opensInTerminalApp(ctx, values) {
|
|
328
|
-
|
|
328
|
+
// A terminal this CLI opened for the conversation attaches there, never in yet another one (0.23.0+).
|
|
329
|
+
if (values.detach === true || handedToTerminal(ctx.io.env))
|
|
329
330
|
return false;
|
|
330
331
|
const here = attachable(ctx.io);
|
|
331
332
|
if (values.in === undefined)
|
|
@@ -364,15 +365,26 @@ export const AGENT_HANDLERS = {
|
|
|
364
365
|
async 'agents open'(ctx, values, [agent = 'main']) {
|
|
365
366
|
if (values.app !== undefined && values.app !== 'claude' && values.app !== 'codex')
|
|
366
367
|
throw new UsageError('--app must be claude or codex');
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
368
|
+
// A Codex Cloud conversation opens its chat in Codex, never in a terminal (0.23.0+): it is resolved before any
|
|
369
|
+
// terminal hand-off, by its id or by its computer's key, which menus before Linux 0.1.9 and menu bar 0.8.4 pass.
|
|
370
|
+
// Nimblefox (2026-10-13): the hand-off came first, and from a Codex shell each new terminal opened another one.
|
|
371
|
+
// The person's terminal app (0.20.1+): after that lookup, the new tab owns the attach, credentials and prompts; a
|
|
372
|
+
// lookup this shell cannot make (signed out here, offline) leaves the hand-off to it.
|
|
373
|
+
const handOff = opensInTerminalApp(ctx, values);
|
|
370
374
|
let resolvedConversation;
|
|
371
|
-
if (/^conv_[a-z0-9]{26}$/.test(agent)) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
+
if (/^conv_[a-z0-9]{26}$/.test(agent) || agent.includes('/')) {
|
|
376
|
+
try {
|
|
377
|
+
resolvedConversation = await conversationForOpen(ctx, agent);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (!handOff)
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
375
383
|
}
|
|
384
|
+
if (resolvedConversation?.route === 'codex-cloud')
|
|
385
|
+
return openCodexConversation(ctx, resolvedConversation, values.original === true ? true : values.history);
|
|
386
|
+
if (handOff)
|
|
387
|
+
return openInTerminalApp(ctx, agent, values);
|
|
376
388
|
if (values.in === 'auto') {
|
|
377
389
|
// The menu and other launchers use the same CLI-owned target selection. Never put provider URLs in a client.
|
|
378
390
|
// A conversation id opens that conversation's own session, also when its computer hosts other threads.
|
|
@@ -405,6 +417,8 @@ export const AGENT_HANDLERS = {
|
|
|
405
417
|
// Codex's default open implementation owns Cloud/desktop routing. PAS-XB replaces that path; do not pin
|
|
406
418
|
// the menu to the old per-VM SSH target. An explicit saved desktop target still takes precedence.
|
|
407
419
|
if ((!saved || !detected.targets.includes(saved)) && app === 'codex') {
|
|
420
|
+
if (resolvedConversation?.route === 'terminal')
|
|
421
|
+
return terminalAuto(ctx, ws, opened, values);
|
|
408
422
|
const next = { ...values };
|
|
409
423
|
delete next.in;
|
|
410
424
|
return AGENT_HANDLERS['agents open'](ctx, next, [resolvedConversation?.id ?? ws.key]);
|
|
@@ -414,7 +428,7 @@ export const AGENT_HANDLERS = {
|
|
|
414
428
|
const handler = HANDLERS[`agents open ${saved}`];
|
|
415
429
|
if (!handler)
|
|
416
430
|
throw new UsageError('Choose an installed open target with shard agents targets.');
|
|
417
|
-
return handler(ctx, { ...values, in: saved }, [
|
|
431
|
+
return handler(ctx, { ...values, in: saved }, [opened]);
|
|
418
432
|
}
|
|
419
433
|
// Claude opens where people work (0.15.1+): the conversation in Claude Desktop when it is installed, else Claude
|
|
420
434
|
// Code in a terminal. The browser (claude.ai) only when it is the chosen target (--in web).
|
|
@@ -433,10 +447,10 @@ export const AGENT_HANDLERS = {
|
|
|
433
447
|
await syncCredentials(ctx);
|
|
434
448
|
let ws;
|
|
435
449
|
let conversation;
|
|
436
|
-
if (/^conv_[a-z0-9]{26}$/.test(agent)) {
|
|
450
|
+
if (resolvedConversation || /^conv_[a-z0-9]{26}$/.test(agent)) {
|
|
437
451
|
conversation = resolvedConversation ?? await getConversation(ctx, agent);
|
|
438
452
|
if (conversation.route === 'codex-cloud')
|
|
439
|
-
return openCodexConversation(ctx, conversation);
|
|
453
|
+
return openCodexConversation(ctx, conversation, values.original === true ? true : values.history);
|
|
440
454
|
conversation = await resumeConversation(ctx, conversation);
|
|
441
455
|
ws = await conversationWorkspace(ctx, conversation);
|
|
442
456
|
}
|
|
@@ -469,7 +483,7 @@ export const AGENT_HANDLERS = {
|
|
|
469
483
|
if (matches.length !== 1)
|
|
470
484
|
throw new UsageError('Use the conversation id.');
|
|
471
485
|
if (matches[0].route === 'codex-cloud')
|
|
472
|
-
return openCodexConversation(ctx, matches[0]);
|
|
486
|
+
return openCodexConversation(ctx, matches[0], values.original === true ? true : values.history);
|
|
473
487
|
conversation = await resumeConversation(ctx, matches[0]);
|
|
474
488
|
ws = await conversationWorkspace(ctx, conversation);
|
|
475
489
|
}
|
|
@@ -477,7 +491,7 @@ export const AGENT_HANDLERS = {
|
|
|
477
491
|
if (!conversation && ws.labels.shard_conversation) {
|
|
478
492
|
conversation = await getConversation(ctx, ws.labels.shard_conversation);
|
|
479
493
|
if (conversation.route === 'codex-cloud')
|
|
480
|
-
return openCodexConversation(ctx, conversation);
|
|
494
|
+
return openCodexConversation(ctx, conversation, values.original === true ? true : values.history);
|
|
481
495
|
conversation = await resumeConversation(ctx, conversation);
|
|
482
496
|
ws = await conversationWorkspace(ctx, conversation);
|
|
483
497
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Duplex } from 'node:stream';
|
|
2
|
-
import type { CellClient, Workspace } from '@shardflux/sdk';
|
|
2
|
+
import type { CellClient, T3Link, Workspace } from '@shardflux/sdk';
|
|
3
3
|
import type { Conversation } from './conversations.js';
|
|
4
4
|
import type { Ctx, Handler, Io } from './commands.js';
|
|
5
5
|
import { AgentTargets } from './agent-targets.js';
|
|
6
6
|
import type { DesktopTarget } from './agent-targets.js';
|
|
7
|
+
import { CodedError } from './errors.js';
|
|
7
8
|
import type { T3Install, T3ThreadRequest } from './t3-mode.js';
|
|
8
9
|
/** Optimistic file replacement preserves keys installed by other devices connecting at the same time. */
|
|
9
10
|
export declare function installDeviceKey(cell: CellClient, key: string, signal?: AbortSignal): Promise<void>;
|
|
@@ -35,5 +36,23 @@ export declare function openClaudeDesktopSession(ctx: Ctx, ws: Workspace, conver
|
|
|
35
36
|
export declare function openDesktopWorkspace(ctx: Ctx, ws: Workspace, target: DesktopTarget, t3Version?: string, prewarmed?: boolean, thread?: T3ThreadRequest, install?: T3Install, conversation?: string | null): Promise<number>;
|
|
36
37
|
/** T3 Code's Connections page (its t3code:// link), where a project linked with T3 Connect is listed. */
|
|
37
38
|
export declare const T3_CONNECTIONS_PAGE = "t3code://app/settings/connections";
|
|
39
|
+
/** A workspace's link as `agents open t3` reports it (`t3_link`): the PUT answer, or the outcome the status read gave. */
|
|
40
|
+
export type T3LinkView = Omit<T3Link, 'status'> & {
|
|
41
|
+
status: T3Link['status'] | 'error';
|
|
42
|
+
last_error?: string;
|
|
43
|
+
needs_login?: boolean;
|
|
44
|
+
};
|
|
45
|
+
/** How long `agents open t3` waits for a link in progress to be linked or refused (at most the wake timeout). */
|
|
46
|
+
export declare const T3_LINK_WAIT_MS = 30000;
|
|
47
|
+
/**
|
|
48
|
+
* The link T3 Code needs is refused (`t3_link_failed`, with the API's remedy: for T3 Connect's environment limit, its
|
|
49
|
+
* Connections page) or still linking after T3_LINK_WAIT_MS (`t3_link_pending`). Both name the workspace, conversation
|
|
50
|
+
* and any started thread, so a client opens what exists again rather than starting another.
|
|
51
|
+
*/
|
|
52
|
+
export declare function t3LinkNotReady(link: T3LinkView, ref: {
|
|
53
|
+
workspace_key: string;
|
|
54
|
+
conversation_id: string | null;
|
|
55
|
+
t3_thread_id: string | null;
|
|
56
|
+
}): CodedError;
|
|
38
57
|
export declare function queueT3Prewarm(ctx: Ctx, ws: Workspace, version: string): Promise<void>;
|
|
39
58
|
export declare const TARGET_HANDLERS: Record<string, Handler>;
|
|
@@ -5,19 +5,20 @@ import { ensureCloudClaude } from "./claude-cloud-login.js";
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { Readable, Duplex } from 'node:stream';
|
|
7
7
|
import { ShardfluxApiError, WorkspaceTunnels } from '@shardflux/sdk';
|
|
8
|
-
import {
|
|
8
|
+
import { conversationForOpen } from "./conversations.js";
|
|
9
9
|
import { out, resolveWorkspace } from "./commands.js";
|
|
10
10
|
import { MODEL_CHOICES } from "./agent-models.js";
|
|
11
11
|
import { AgentTargets, installedTargets, targetLabel } from "./agent-targets.js";
|
|
12
12
|
import { join, relative, resolve } from 'node:path';
|
|
13
13
|
import { projectIdentity } from "./project-identity.js";
|
|
14
14
|
import { projectWorkspacePath } from "./agent-repository.js";
|
|
15
|
-
import { UsageError, describeError } from "./errors.js";
|
|
15
|
+
import { CodedError, UsageError, describeError } from "./errors.js";
|
|
16
16
|
import { t3PhoneLink } from "./t3-phone.js";
|
|
17
17
|
import { prepareDesktopRouter } from "./conversation-router.js";
|
|
18
18
|
import { openUrl } from "./browser.js";
|
|
19
19
|
import { agentMachineName, prepareAgentMachine } from "./agent-machine.js";
|
|
20
20
|
import { prewarmT3 } from "./t3-runtime.js";
|
|
21
|
+
import { followT3ProxyInVm } from "./t3-proxy.js";
|
|
21
22
|
import { AgentStateStore } from "./agent-state.js";
|
|
22
23
|
import { workspaceRepositoryPath } from "./agent-repository.js";
|
|
23
24
|
import { openInChoices, openInTargets, openT3, saveOpenIn, startSshThread, t3Install, t3NotInstalled, t3OpenFailed, t3Status } from "./t3-mode.js";
|
|
@@ -221,16 +222,23 @@ async function openLink(url, env) {
|
|
|
221
222
|
});
|
|
222
223
|
}
|
|
223
224
|
const openTarget = (target) => async (ctx, values, [ref = 'main']) => {
|
|
225
|
+
// A conversation started in T3 (its Shardflux provider) opens its own thread there (0.17.0+); a Codex Cloud one opens
|
|
226
|
+
// its Codex chat (0.23.0+). openT3Conversation checks T3 Code itself where it needs it.
|
|
227
|
+
let conversation = await conversationForOpen(ctx, /^conv_[a-z0-9]{26}$/.test(ref) ? ref : keyOf(ref));
|
|
228
|
+
if (target === 't3' && conversation)
|
|
229
|
+
return openT3Conversation(ctx, conversation.id, values);
|
|
230
|
+
// A Codex Cloud conversation opens its existing thread in the Codex app, not an SSH environment (0.23.0+).
|
|
231
|
+
if (target === 'codex-app' && conversation?.route === 'codex-cloud')
|
|
232
|
+
return (await import("./codex-cloud.js")).openCodexConversation(ctx, conversation);
|
|
224
233
|
const detected = installedTargets(ctx.io.env);
|
|
225
234
|
if (target === 't3' && !detected.targets.includes('t3'))
|
|
226
235
|
throw t3NotInstalled();
|
|
236
|
+
if (target === 'codex-app' && !detected.targets.includes(target) && detected.clients.includes('codex-app')) {
|
|
237
|
+
throw new UsageError('On Linux the Codex app opens Codex Cloud conversations (shard agents open codex-app <conversation>).', `Open this one with: shard agents open ${ref}`);
|
|
238
|
+
}
|
|
227
239
|
if (target !== 'web' && !detected.targets.includes(target))
|
|
228
240
|
throw new UsageError(`${target} is not installed on this machine`);
|
|
229
|
-
// A conversation started in T3 (its Shardflux provider) opens its own thread there (0.17.0+).
|
|
230
|
-
if (target === 't3' && /^conv_[a-z0-9]{26}$/.test(ref))
|
|
231
|
-
return openT3Conversation(ctx, ref, values);
|
|
232
241
|
// A conversation id opens that conversation's own session on its computer (claude.ai, the desktop apps).
|
|
233
|
-
let conversation = /^conv_[a-z0-9]{26}$/.test(ref) ? await getConversation(ctx, ref) : undefined;
|
|
234
242
|
// A conversation whose VM was removed restores into a fresh one first (0.18.1+).
|
|
235
243
|
let ws;
|
|
236
244
|
if (conversation) {
|
|
@@ -269,6 +277,9 @@ export async function openDesktopWorkspace(ctx, ws, target, t3Version, prewarmed
|
|
|
269
277
|
await prepareAgentMachine(ctx, ws);
|
|
270
278
|
if (target === 't3' && t3Version && !prewarmed)
|
|
271
279
|
await queueT3Prewarm(ctx, ws, t3Version);
|
|
280
|
+
// The VM's T3 Code follows the user's T3 proxy setup, before its server starts for a thread or the T3 Connect link.
|
|
281
|
+
if (target === 't3')
|
|
282
|
+
await followT3ProxyInVm(ctx, ws);
|
|
272
283
|
const targets = new AgentTargets(ctx.io.env, ctx.store.dir);
|
|
273
284
|
const entry = (await (ws.labels.shard_router === 'project' ? targets.configureProject(ws.key, ctx.apiUrl, [target], ctx.signal) : targets.configure(ws.key, ctx.apiUrl, [target], false, ctx.signal))).find(e => e.workspace === ws.key);
|
|
274
285
|
const instructions = target === 't3'
|
|
@@ -291,8 +302,13 @@ export async function openDesktopWorkspace(ctx, ws, target, t3Version, prewarmed
|
|
|
291
302
|
// The first time a project is set up for T3 Code, T3 opens on its Connections page once the project is linked: the
|
|
292
303
|
// project is listed there (or T3's own Sign in to T3 Connect, when T3 Code is signed out).
|
|
293
304
|
if (target === 't3') {
|
|
294
|
-
|
|
295
|
-
|
|
305
|
+
// T3 Code lists the project only once its T3 Connect link is linked (0.23.0+): a refused or still pending link is
|
|
306
|
+
// reported as such, naming what exists, instead of an opened T3 that does not show it.
|
|
307
|
+
if (phone?.link.status === 'linking')
|
|
308
|
+
phone = await waitT3Link(ctx, ws, phone);
|
|
309
|
+
const ref = { workspace_key: ws.key, conversation_id: conversation ?? (ws.labels.shard_conversation || null), t3_thread_id: started?.t3_thread_id ?? null };
|
|
310
|
+
if (phone?.link.status === 'error' || phone?.link.status === 'linking')
|
|
311
|
+
throw t3LinkNotReady(phone.link, ref);
|
|
296
312
|
const page = fresh && phone?.link.status === 'linked' ? T3_CONNECTIONS_PAGE : undefined;
|
|
297
313
|
// The thread exists once startSshThread returns: a failure to show T3 names it rather than inviting a retry.
|
|
298
314
|
try {
|
|
@@ -338,25 +354,40 @@ const t3LinkLine = (link, signIn, needsLogin) => signIn
|
|
|
338
354
|
: needsLogin ? `To have ${link.label} appear in T3 Code and the T3 app, sign in with shard auth login, then run this again.\n`
|
|
339
355
|
: link.status === 'linked' ? `${link.label} is linked to your T3 account.\n`
|
|
340
356
|
: `Linking ${link.label} to your T3 account…\n`;
|
|
341
|
-
/**
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
357
|
+
/** How long `agents open t3` waits for a link in progress to be linked or refused (at most the wake timeout). */
|
|
358
|
+
export const T3_LINK_WAIT_MS = 30_000;
|
|
359
|
+
/** A link in progress: its outcome from the account's T3 status, read every 2 s for at most T3_LINK_WAIT_MS. */
|
|
360
|
+
async function waitT3Link(ctx, ws, phone) {
|
|
361
|
+
const until = Date.now() + Math.min(T3_LINK_WAIT_MS, ctx.wakeTimeoutMs ?? T3_LINK_WAIT_MS);
|
|
362
|
+
for (;;) {
|
|
346
363
|
try {
|
|
347
364
|
const known = (await ctx.cloud().t3.status()).links.find(l => l.workspace_id === ws.id);
|
|
348
|
-
if (known
|
|
349
|
-
const link = { ...phone.link, status: 'linked' };
|
|
365
|
+
if (known && known.status !== 'linking') {
|
|
366
|
+
const link = { ...phone.link, status: known.status === 'linked' ? 'linked' : known.status === 'needs_sign_in' ? 'needs_sign_in' : 'error', ...(known.last_error ? { last_error: known.last_error } : {}) };
|
|
350
367
|
return { line: t3LinkLine(link), link };
|
|
351
368
|
}
|
|
352
|
-
if (known && known.status !== 'linking')
|
|
353
|
-
return phone;
|
|
354
369
|
}
|
|
355
370
|
catch {
|
|
356
371
|
return phone;
|
|
357
372
|
}
|
|
373
|
+
if (Date.now() >= until || ctx.signal?.aborted)
|
|
374
|
+
return phone;
|
|
375
|
+
await delay(2000);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* The link T3 Code needs is refused (`t3_link_failed`, with the API's remedy: for T3 Connect's environment limit, its
|
|
380
|
+
* Connections page) or still linking after T3_LINK_WAIT_MS (`t3_link_pending`). Both name the workspace, conversation
|
|
381
|
+
* and any started thread, so a client opens what exists again rather than starting another.
|
|
382
|
+
*/
|
|
383
|
+
export function t3LinkNotReady(link, ref) {
|
|
384
|
+
const again = `shard agents open t3 ${ref.conversation_id ?? ref.workspace_key}`;
|
|
385
|
+
const details = { ...ref, t3_link: link };
|
|
386
|
+
if (link.status !== 'error') {
|
|
387
|
+
return new CodedError('t3_link_pending', `Linking ${link.label} to your T3 account. T3 Code lists it once it is linked.`, `Open it in a moment: ${again}`, details);
|
|
358
388
|
}
|
|
359
|
-
|
|
389
|
+
const limit = /environment limit/i.test(link.last_error ?? '');
|
|
390
|
+
return new CodedError('t3_link_failed', link.last_error ?? `T3 Connect could not link ${link.label}.`, limit ? `In T3 Code open Settings → Connections (${T3_CONNECTIONS_PAGE}), remove an environment you no longer need, then: ${again}` : `Then open it again: ${again}`, limit ? { ...details, recovery: { title: 'Open T3 Code Connections', url: T3_CONNECTIONS_PAGE } } : details);
|
|
360
391
|
}
|
|
361
392
|
export async function queueT3Prewarm(ctx, ws, version) {
|
|
362
393
|
try {
|
package/dist/agent-targets.js
CHANGED
|
@@ -8,6 +8,7 @@ import { UsageError } from "./errors.js";
|
|
|
8
8
|
import { projectWorkspacePath } from "./agent-repository.js";
|
|
9
9
|
import { withTargetLock } from "./target-lock.js";
|
|
10
10
|
import { linuxApplicationDirs, linuxSupportsAcp, linuxT3Apps } from "./t3-linux.js";
|
|
11
|
+
import { linuxCodexApps } from "./codex-linux.js";
|
|
11
12
|
import { t3ArchiveSupportsAcp } from "./t3-asar.js";
|
|
12
13
|
import { compareT3Versions } from "./t3-version.js";
|
|
13
14
|
const PERSISTED_TARGETS = ['claude-desktop', 'codex-app'];
|
|
@@ -132,7 +133,11 @@ export function installedTargets(env, platform = process.platform, appDirs) {
|
|
|
132
133
|
const clients = [hasApp('Claude.app', 'com.anthropic.claudefordesktop') && 'claude-desktop', (hasApp('Codex.app', 'com.openai.codex') || hasApp('ChatGPT.app', 'com.openai.codex')) && 'codex-app', executable('claude') && 'claude', executable('codex') && 'codex'].filter((v) => !!v);
|
|
133
134
|
if (t3)
|
|
134
135
|
clients.push('t3');
|
|
135
|
-
//
|
|
136
|
+
// The Codex app on Linux (0.23.0+): the entry that handles codex:// (OpenAI's ChatGPT app for Linux, or a community
|
|
137
|
+
// build). It opens Codex Cloud conversations; its SSH environments stay a macOS target.
|
|
138
|
+
if (platform === 'linux' && linuxCodexApps(dirs, env).length > 0)
|
|
139
|
+
clients.push('codex-app');
|
|
140
|
+
// Linux has T3 Code (0.17.0+); Claude Desktop and the Codex app's SSH environments are macOS targets.
|
|
136
141
|
const desktop = platform === 'darwin' ? clients.filter(c => c.endsWith('-app') || c === 'claude-desktop' || c === 't3') : platform === 'linux' && t3 ? ['t3'] : [];
|
|
137
142
|
return { targets: [...desktop, 'web'], clients, versions };
|
|
138
143
|
}
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -13,7 +13,7 @@ import { parseArgs } from 'node:util';
|
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
15
|
import { DEFAULT_CHOICE, MODEL_CHOICES } from "./agent-models.js";
|
|
16
|
-
export const CLI_VERSION = '0.
|
|
16
|
+
export const CLI_VERSION = '0.23.0';
|
|
17
17
|
export const GLOBAL_OPTIONS = {
|
|
18
18
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
19
19
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -1289,8 +1289,8 @@ const AGENT_COMMANDS = [
|
|
|
1289
1289
|
session: { type: 'string', value: '<id>', description: 'The session to move: a Claude Code session id or Codex thread id.' },
|
|
1290
1290
|
path: { type: 'string', value: '<path>', description: 'The project folder (default: the current folder).' },
|
|
1291
1291
|
plan: { type: 'boolean', description: 'Inspect all registered worktree paths, reference hits and file bytes before any upload.' },
|
|
1292
|
-
'preserve-all-worktrees': { type: 'boolean', description: '
|
|
1293
|
-
'this-worktree-only': { type: 'boolean', description: '
|
|
1292
|
+
'preserve-all-worktrees': { type: 'boolean', description: 'Also bring every registered worktree, all local refs, indices and ignored files. Reports paths and bytes; keeps a durable recovery archive.' },
|
|
1293
|
+
'this-worktree-only': { type: 'boolean', description: 'Move the session’s checkout only (the default): other worktrees and local refs stay on this computer.' },
|
|
1294
1294
|
claude: { type: 'boolean', description: 'With --last or --list: Claude Code sessions only.' },
|
|
1295
1295
|
codex: { type: 'boolean', description: 'With --last or --list: Codex sessions only.' },
|
|
1296
1296
|
list: { type: 'boolean', description: 'List this project’s sessions on this computer, and where the moved ones continue.' },
|
|
@@ -1316,7 +1316,7 @@ const AGENT_COMMANDS = [
|
|
|
1316
1316
|
},
|
|
1317
1317
|
})),
|
|
1318
1318
|
...['status', 'enable', 'disable'].map(action => ({ path: ['agents', 'codex', action], summary: `${action} the account's billed Codex Cloud connector`, positionals: [], options: {} })),
|
|
1319
|
-
{ path: ['agents', 'open'], summary: 'Open a conversation (Codex Cloud or the native Claude session)', description: 'Codex opens its existing Cloud thread without waking its VM. Claude attaches its native session.', positionals: [{ name: 'agent', required: true }], options: { ...AGENT_OPTIONS, app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' }, in: { type: 'string', value: '<terminal|auto|claude-desktop|codex-app|web|t3>', description: 'Where to open it. auto (0.15.1+): the saved target, else a Claude conversation in Claude Desktop when it is installed, else in this terminal; web opens claude.ai. terminal (0.20.1+): a new tab of the terminal app agents targets --terminal chose, or this terminal when it is that one.' },
|
|
1319
|
+
{ path: ['agents', 'open'], summary: 'Open a conversation (Codex Cloud or the native Claude session)', description: 'Codex opens its existing Cloud thread without waking its VM. Claude attaches its native session.', positionals: [{ name: 'agent', required: true }], options: { ...AGENT_OPTIONS, original: { type: 'boolean', description: 'Open the original Codex Cloud history after a connector change.' }, history: { type: 'string', value: '<thread-id>', description: 'Open an earlier thread listed in the Cloud conversation history.' }, app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' }, in: { type: 'string', value: '<terminal|auto|claude-desktop|codex-app|web|t3>', description: 'Where to open it. auto (0.15.1+): the saved target, else a Claude conversation in Claude Desktop when it is installed, else in this terminal; web opens claude.ai. terminal (0.20.1+): a new tab of the terminal app agents targets --terminal chose, or this terminal when it is that one.' },
|
|
1320
1320
|
window: { type: 'boolean', description: 'With --in terminal: a new window of the terminal app instead of a tab (0.20.1+).' } } },
|
|
1321
1321
|
{ path: ['agents', 'terminal-run'], hidden: true, summary: 'Run a command in the terminal app agents targets --terminal chose (0.20.1+)', positionals: [{ name: 'command', required: true, variadic: true }], options: {
|
|
1322
1322
|
cwd: { type: 'string', value: '<path>', description: 'The folder the new tab starts in (default the current folder).' },
|
|
@@ -1353,10 +1353,11 @@ const CLI_PROXY_COMMANDS = [
|
|
|
1353
1353
|
{ path: ['cli-proxy', 'import'], summary: 'Move CLIProxyAPI from this computer into your account, reachable at 127.0.0.1:8317 everywhere', description: `${CLI_PROXY_HELP} Import brings your config and logins over, stops the local copy (one copy refreshes each login), and connects this computer and your machine template. On a second computer it connects that computer.`, positionals: [], options: {
|
|
1354
1354
|
config: { type: 'string', value: '<path>', description: 'Your CLIProxyAPI config file (default: the one the running CLIProxyAPI uses, else the usual locations).' },
|
|
1355
1355
|
yes: { type: 'boolean', description: 'Stop the local CLIProxyAPI without asking.' },
|
|
1356
|
-
use: { type: 'string', multiple: true, value: '<email|file>', description: '
|
|
1356
|
+
use: { type: 'string', multiple: true, value: '<email|file>', description: 'Serve its provider with only this login; the provider’s others are kept, disabled (repeatable, one per provider). Default: every login as it is on this computer.' },
|
|
1357
1357
|
}, examples: ['shard cli-proxy import', 'shard cli-proxy'] },
|
|
1358
|
-
{ path: ['cli-proxy', 'use'], summary: '
|
|
1359
|
-
|
|
1358
|
+
{ path: ['cli-proxy', 'use'], summary: 'Choose which logins your hosted CLIProxyAPI uses: one for its provider, or all', description: 'Your hosted CLIProxyAPI routes requests across its enabled logins as its config says. `use <login>` keeps only that login enabled for its provider (the others stay in the hub, disabled); `use --all` enables every login again.', positionals: [{ name: 'login' }], options: {
|
|
1359
|
+
all: { type: 'boolean', description: 'Enable every login; the hub routes across them as its config says.' },
|
|
1360
|
+
}, examples: ['shard cli-proxy use ana@example.com', 'shard cli-proxy use --all'] },
|
|
1360
1361
|
{ path: ['cli-proxy', 'export'], summary: 'Bring your logins back to this computer and stop the hosted CLIProxyAPI', description: 'The hosted copy stops first, so only this computer refreshes your logins from then on. Its logins, as last refreshed, go to the auth folder the import took them from (0600), this computer stops forwarding 127.0.0.1:8317, and the command that starts your local CLIProxyAPI is shown. shard cli-proxy import moves them back.', positionals: [], options: {
|
|
1361
1362
|
yes: { type: 'boolean', description: 'Do it without asking.' },
|
|
1362
1363
|
} },
|
|
@@ -13,8 +13,6 @@ 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;
|
|
18
16
|
host?: string;
|
|
19
17
|
port?: number;
|
|
20
18
|
/** 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) =>
|
|
101
|
-
this.#server.on('upgrade', (req, socket, head) =>
|
|
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));
|
|
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,13 +327,6 @@ 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
|
-
}
|
|
337
330
|
async #handle(req, res) {
|
|
338
331
|
if (pathOnly(req.url) === IDENTITY_PATH) {
|
|
339
332
|
const info = this.#o.hub();
|