@shardflux/cli 0.14.5 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +76 -12
- package/dist/account-args.js +31 -10
- package/dist/account-commands.d.ts +2 -0
- package/dist/account-commands.js +158 -14
- package/dist/acp-catalog.js +2 -2
- package/dist/acp-login.d.ts +20 -0
- package/dist/acp-login.js +122 -0
- package/dist/acp.js +36 -13
- package/dist/agent-commands.js +71 -6
- package/dist/agent-git-commands.js +2 -1
- package/dist/agent-reachability.js +1 -1
- package/dist/agent-target-commands.d.ts +14 -0
- package/dist/agent-target-commands.js +79 -13
- package/dist/agent-targets.d.ts +7 -0
- package/dist/agent-targets.js +44 -32
- package/dist/args.d.ts +3 -1
- package/dist/args.js +21 -9
- package/dist/browser-login.d.ts +35 -0
- package/dist/browser-login.js +145 -0
- package/dist/codex-cloud.d.ts +49 -0
- package/dist/codex-cloud.js +157 -0
- package/dist/command-center-actions.js +7 -0
- package/dist/command-center-renderer.d.ts +3 -0
- package/dist/command-center-renderer.js +42 -38
- package/dist/command-center.js +65 -11
- package/dist/conversation-terminal.js +19 -0
- package/dist/conversations.d.ts +1 -1
- package/dist/conversations.js +21 -8
- package/dist/credential-prompts.d.ts +4 -2
- package/dist/credential-prompts.js +38 -16
- package/dist/device-bridge.d.ts +13 -0
- package/dist/device-bridge.js +14 -4
- package/dist/device-helper.js +2 -1
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +15 -2
- package/dist/format.js +3 -1
- package/dist/handlers.js +2 -1
- package/dist/initial-prompt.d.ts +26 -0
- package/dist/initial-prompt.js +196 -0
- package/dist/legal.d.ts +36 -0
- package/dist/legal.js +44 -0
- package/dist/main.js +6 -2
- package/dist/menubar.d.ts +8 -0
- package/dist/menubar.js +9 -1
- package/dist/onboarding-app/AGENTS.md +19 -0
- package/dist/onboarding-app/CLAUDE.md +19 -0
- package/dist/onboarding-app/README.md +12 -0
- package/dist/onboarding-app/dev.mjs +18 -0
- package/dist/onboarding-app/index.html +25 -0
- package/dist/onboarding-app/package.json +12 -0
- package/dist/onboarding-app/server.mjs +41 -0
- package/dist/onboarding-sample.d.ts +3 -0
- package/dist/onboarding-sample.js +29 -0
- package/dist/onboarding.d.ts +258 -0
- package/dist/onboarding.js +371 -0
- package/dist/terminal-brand.d.ts +5 -0
- package/dist/terminal-brand.js +18 -0
- package/package.json +3 -3
package/dist/agent-commands.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { prepareSetupCache } from "./setup-cache.js";
|
|
2
|
+
import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
|
|
2
3
|
import { agentReachability } from "./agent-reachability.js";
|
|
3
4
|
import { configureCredentialFill, watchCredentialPrompts } from "./credential-prompts.js";
|
|
4
5
|
import { createHash } from 'node:crypto';
|
|
@@ -10,7 +11,7 @@ import { prepareProjectTemplate } from "./project-template-setup.js";
|
|
|
10
11
|
import { projectIdentity, projectLabels } from "./project-identity.js";
|
|
11
12
|
import { ensureProjectGit } from "./project-git.js";
|
|
12
13
|
import { json } from "./format.js";
|
|
13
|
-
import { resolveWorkspace } from "./commands.js";
|
|
14
|
+
import { out, resolveWorkspace } from "./commands.js";
|
|
14
15
|
import { savedMachine } from "./filesystem-state.js";
|
|
15
16
|
import { startAgentConfigTunnels, syncAgentConfig } from "./agent-config-sync.js";
|
|
16
17
|
import { rememberGitAgent } from "./agent-git-commands.js";
|
|
@@ -21,7 +22,8 @@ import { progress } from "./progress.js";
|
|
|
21
22
|
import { rememberLaunch } from "./agent-live.js";
|
|
22
23
|
import { prepareAgentMachine } from "./agent-machine.js";
|
|
23
24
|
import { installedTargets } from "./agent-targets.js";
|
|
24
|
-
import { openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
25
|
+
import { openClaudeDesktopSession, openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
26
|
+
import { submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
25
27
|
import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
26
28
|
import { conversationTerminal } from "./conversation-terminal.js";
|
|
27
29
|
const active = (session) => session.state === 'running' || session.state === 'starting';
|
|
@@ -98,9 +100,16 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
98
100
|
// Laptop-local MCP servers are reachable while a terminal is attached (L6); --json/--detach start no tunnels.
|
|
99
101
|
const tunnels = attach ? await startAgentConfigTunnels(ctx, ws) : null;
|
|
100
102
|
try {
|
|
103
|
+
if (app === 'codex' && typeof values.initialPrompt === 'string') {
|
|
104
|
+
await prepareInitialCodexProject(cell, cwd, `shard-${ws.labels.agent_pty_name || agent}-${app}`);
|
|
105
|
+
}
|
|
101
106
|
const credentialFill = await configureCredentialFill(ctx, ws);
|
|
102
107
|
// A cold launch is remembered before it starts, so the listing can show it before the projection does (agent-live.ts).
|
|
103
|
-
const
|
|
108
|
+
const term = ctx.io.env.TERM && ctx.io.env.TERM !== 'dumb' ? ctx.io.env.TERM : 'xterm-256color';
|
|
109
|
+
const session = await agentPty(cell, app, agent, values.approvals === true, term, ctx.signal, ws.labels.shard_project || ws.key.split('/')[0], setupHash, cwd, ws.labels.agent_pty_name, () => { rememberLaunch(ctx, ws.id); progress(ctx, `Starting ${app === 'claude' ? 'Claude' : 'Codex'} in ${ws.key}…`); }, true, conversation);
|
|
110
|
+
// A new conversation's guest can start the agent before this command reaches it, so the launch above never ran.
|
|
111
|
+
// Remember it either way: listing and `agents open` then read the guest until the projection lists the session.
|
|
112
|
+
rememberLaunch(ctx, ws.id);
|
|
104
113
|
// The real agent is running before any cache provisioning or mount wait.
|
|
105
114
|
const preparation = prepareSetupCache(ctx, ws).catch((error) => {
|
|
106
115
|
ctx.io.stderr.write(`Account setup cache unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -112,9 +121,12 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
112
121
|
throw new Error(`project setup could not be queued: ${queued.stderr.trim() || `exit ${queued.exitCode}`}`);
|
|
113
122
|
});
|
|
114
123
|
store.save(project, app);
|
|
124
|
+
if (typeof values.initialPrompt === 'string') {
|
|
125
|
+
await submitInitialPrompt(signal => cellConnector(cell, session.session_id)(0, signal), values.initialPrompt, ws.key, ctx.signal, app, bytes => cell.pty.input(session.session_id, bytes));
|
|
126
|
+
}
|
|
115
127
|
if (!attach) {
|
|
116
128
|
await preparation;
|
|
117
|
-
ctx.io.stdout.write(json({ workspace: ws.data, session }));
|
|
129
|
+
ctx.io.stdout.write(json({ workspace: ws.data, session, ...(typeof values.initialPrompt === 'string' ? { initial_prompt: { submitted: true } } : {}) }));
|
|
118
130
|
return 0;
|
|
119
131
|
}
|
|
120
132
|
ctx.io.stderr.write(`Shardflux · ${ws.key}\nDetach: Ctrl-] then d\n`);
|
|
@@ -167,6 +179,8 @@ export const launchProject = async (ctx, values, [path = './', explicitApp]) =>
|
|
|
167
179
|
if (target && (!['claude-desktop', 'codex-app', 't3'].includes(target) || !detected?.targets.includes(target)))
|
|
168
180
|
throw new UsageError(`${target} is not an installed desktop target`);
|
|
169
181
|
const app = explicitApp ?? store.app(projectKey, like?.labels.app === 'codex' ? 'codex' : 'claude');
|
|
182
|
+
if (target === 'codex-app' || (app === 'codex' && !like && values.in === undefined))
|
|
183
|
+
return launchCodexCloud(ctx, values, path);
|
|
170
184
|
const agent = typeof values.name === 'string' ? values.name : 'main';
|
|
171
185
|
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(agent))
|
|
172
186
|
throw new UsageError('agent name must match ^[a-z0-9][a-z0-9._-]{0,63}$');
|
|
@@ -236,6 +250,49 @@ export const AGENT_HANDLERS = {
|
|
|
236
250
|
async 'agents open'(ctx, values, [agent = 'main']) {
|
|
237
251
|
if (values.app !== undefined && values.app !== 'claude' && values.app !== 'codex')
|
|
238
252
|
throw new UsageError('--app must be claude or codex');
|
|
253
|
+
let resolvedConversation;
|
|
254
|
+
if (/^conv_[a-z0-9]{26}$/.test(agent)) {
|
|
255
|
+
resolvedConversation = await getConversation(ctx, agent);
|
|
256
|
+
if (resolvedConversation.route === 'codex-cloud')
|
|
257
|
+
return openCodexConversation(ctx, resolvedConversation);
|
|
258
|
+
}
|
|
259
|
+
if (values.in === 'auto') {
|
|
260
|
+
// The menu and other launchers use the same CLI-owned target selection. Never put provider URLs in a client.
|
|
261
|
+
const ws = await resolveWorkspace(ctx, agent);
|
|
262
|
+
const project = ws.labels.repo_remote || ws.labels.shard_project || ws.key;
|
|
263
|
+
const store = new AgentStateStore(ctx.store.dir);
|
|
264
|
+
const app = values.app ?? ws.labels.shard_app ?? ws.labels.app ?? store.app(project);
|
|
265
|
+
const detected = installedTargets(ctx.io.env);
|
|
266
|
+
const saved = store.read().projects?.[project]?.target ??
|
|
267
|
+
(project.startsWith('file://') ? store.read().projects?.[new URL(project).pathname]?.target : undefined);
|
|
268
|
+
// Codex's default open implementation owns Cloud/desktop routing. PAS-XB replaces that path; do not pin
|
|
269
|
+
// the menu to the old per-VM SSH target. An explicit saved desktop target still takes precedence.
|
|
270
|
+
if ((!saved || !detected.targets.includes(saved)) && app === 'codex') {
|
|
271
|
+
const next = { ...values };
|
|
272
|
+
delete next.in;
|
|
273
|
+
return AGENT_HANDLERS['agents open'](ctx, next, [ws.key]);
|
|
274
|
+
}
|
|
275
|
+
if (saved && detected.targets.includes(saved)) {
|
|
276
|
+
const { HANDLERS } = await import("./handlers.js");
|
|
277
|
+
const handler = HANDLERS[`agents open ${saved}`];
|
|
278
|
+
if (!handler)
|
|
279
|
+
throw new UsageError('Choose an installed open target with shard agents targets.');
|
|
280
|
+
return handler(ctx, { ...values, in: saved }, [ws.key]);
|
|
281
|
+
}
|
|
282
|
+
// Claude opens where people work (0.15.1+): the conversation in Claude Desktop when it is installed, else Claude
|
|
283
|
+
// Code in a terminal. The browser (claude.ai) only when it is the chosen target (--in web).
|
|
284
|
+
if (detected.clients.includes('claude-desktop')) {
|
|
285
|
+
return openClaudeDesktopSession(ctx, ws);
|
|
286
|
+
}
|
|
287
|
+
if (ctx.json) {
|
|
288
|
+
// An app runs this in the person's terminal (the menu bar opens Terminal with it).
|
|
289
|
+
out(ctx, `shard agents open ${ws.key}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', ws.key] });
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
const next = { ...values };
|
|
293
|
+
delete next.in;
|
|
294
|
+
return AGENT_HANDLERS['agents open'](ctx, next, [ws.key]);
|
|
295
|
+
}
|
|
239
296
|
if (values.in !== undefined && values.in !== 'terminal') {
|
|
240
297
|
const { HANDLERS } = await import("./handlers.js");
|
|
241
298
|
const target = typeof values.in === 'string' ? HANDLERS[`agents open ${values.in}`] : undefined;
|
|
@@ -247,7 +304,10 @@ export const AGENT_HANDLERS = {
|
|
|
247
304
|
let ws;
|
|
248
305
|
let conversation;
|
|
249
306
|
if (/^conv_[a-z0-9]{26}$/.test(agent)) {
|
|
250
|
-
conversation =
|
|
307
|
+
conversation = resolvedConversation ?? await getConversation(ctx, agent);
|
|
308
|
+
if (conversation.route === 'codex-cloud')
|
|
309
|
+
return openCodexConversation(ctx, conversation);
|
|
310
|
+
conversation = await resumeConversation(ctx, conversation);
|
|
251
311
|
ws = await conversationWorkspace(ctx, conversation);
|
|
252
312
|
}
|
|
253
313
|
else {
|
|
@@ -278,12 +338,17 @@ export const AGENT_HANDLERS = {
|
|
|
278
338
|
throw error;
|
|
279
339
|
if (matches.length !== 1)
|
|
280
340
|
throw new UsageError('Use the conversation id.');
|
|
341
|
+
if (matches[0].route === 'codex-cloud')
|
|
342
|
+
return openCodexConversation(ctx, matches[0]);
|
|
281
343
|
conversation = await resumeConversation(ctx, matches[0]);
|
|
282
344
|
ws = await conversationWorkspace(ctx, conversation);
|
|
283
345
|
}
|
|
284
346
|
// The menu bar supplies workspace_key from agents list, not the conversation key.
|
|
285
347
|
if (!conversation && ws.labels.shard_conversation) {
|
|
286
|
-
conversation = await
|
|
348
|
+
conversation = await getConversation(ctx, ws.labels.shard_conversation);
|
|
349
|
+
if (conversation.route === 'codex-cloud')
|
|
350
|
+
return openCodexConversation(ctx, conversation);
|
|
351
|
+
conversation = await resumeConversation(ctx, conversation);
|
|
287
352
|
ws = await conversationWorkspace(ctx, conversation);
|
|
288
353
|
}
|
|
289
354
|
}
|
|
@@ -61,7 +61,8 @@ async function workspace(ctx, git, name, key) {
|
|
|
61
61
|
if (ws.data.deleted_at)
|
|
62
62
|
throw new UsageError(`workspace ${key} is gone; use shard agents restore ${name} if it was archived`);
|
|
63
63
|
const project = projectIdentity(git.root);
|
|
64
|
-
|
|
64
|
+
// A folder without an origin is identified by its path, as conversations label it (agent-repository.ts).
|
|
65
|
+
if (ws.labels.repo_remote && ws.labels.repo_remote !== (project.remote ?? `file://${project.folder}`))
|
|
65
66
|
throw new UsageError('agent repository does not match this local repository');
|
|
66
67
|
const saved = git.entries()[name];
|
|
67
68
|
if (saved && saved.workspace_id !== ws.id)
|
|
@@ -8,6 +8,6 @@ export function agentReachability(values) {
|
|
|
8
8
|
throw new UsageError('--reachable requires a duration');
|
|
9
9
|
const ms = /^0+(?:\.0+)?(?:ms|s|m|h)?$/.test(text.trim()) ? 0 : parseDuration(text, 'reachable');
|
|
10
10
|
if (ms % 1000 !== 0 || ms > 86_400_000)
|
|
11
|
-
throw new UsageError('--reachable must be whole seconds from 0 to 24h (
|
|
11
|
+
throw new UsageError('--reachable must be whole seconds from 0 to 24h (deprecated; accepted for compatibility)');
|
|
12
12
|
return { agentReachableSeconds: ms / 1000 };
|
|
13
13
|
}
|
|
@@ -8,6 +8,20 @@ export declare function installDeviceKey(cell: CellClient, key: string, signal?:
|
|
|
8
8
|
/** Host trust is established over the authenticated files API, before sending any SSH protocol bytes. */
|
|
9
9
|
export declare function prepareSsh(ws: Workspace, ctx: Ctx, targets: AgentTargets, host: string): Promise<CellClient>;
|
|
10
10
|
export declare function proxyStream(io: Io, ended?: () => void, outputGate?: Promise<unknown>): Duplex;
|
|
11
|
+
/**
|
|
12
|
+
* The Remote Control URL (https://claude.ai/code/session_…) of the workspace's live Claude session. A Claude process
|
|
13
|
+
* this computer just started can accept its first prompt before Remote Control registers, so an open of our recent
|
|
14
|
+
* launch probes the guest for up to 30 s; history browsing stays passive.
|
|
15
|
+
*/
|
|
16
|
+
export declare function remoteControlUrl(ctx: Ctx, ws: Workspace): Promise<string>;
|
|
17
|
+
/** Claude Desktop's own link to a Remote Control session: claude://code/<session id> opens it in its Code tab. */
|
|
18
|
+
export declare function claudeDesktopLink(remoteControl: string): string;
|
|
19
|
+
export declare const CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
|
|
20
|
+
/**
|
|
21
|
+
* Opens the conversation itself (its live Remote Control session) in Claude Desktop (0.15.1+), the way `--in auto`
|
|
22
|
+
* opens a Claude conversation when Claude Desktop is installed. --json prints {target, url, app_bundle_id} for apps.
|
|
23
|
+
*/
|
|
24
|
+
export declare function openClaudeDesktopSession(ctx: Ctx, ws: Workspace): Promise<number>;
|
|
11
25
|
export declare function openDesktopWorkspace(ctx: Ctx, ws: Workspace, target: DesktopTarget, t3Version?: string, prewarmed?: boolean): Promise<number>;
|
|
12
26
|
export declare function queueT3Prewarm(ctx: Ctx, ws: Workspace, version: string): Promise<void>;
|
|
13
27
|
export declare const TARGET_HANDLERS: Record<string, Handler>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import { guestSessions, LAUNCH_WINDOW_MS } from "./agent-live.js";
|
|
1
3
|
import { agentReachability } from "./agent-reachability.js";
|
|
2
4
|
import { ensureCloudClaude } from "./claude-cloud-login.js";
|
|
3
5
|
import { spawn } from 'node:child_process';
|
|
@@ -16,6 +18,8 @@ import { prewarmT3 } from "./t3-runtime.js";
|
|
|
16
18
|
import { AgentStateStore } from "./agent-state.js";
|
|
17
19
|
const missing = (e) => e instanceof ShardfluxApiError && e.status === 404;
|
|
18
20
|
const keyOf = (ref) => ref.includes('/') ? ref : `${projectIdentity('./').slug}/${ref}`;
|
|
21
|
+
/** How long opening a Claude conversation this computer just started waits for Claude to publish its Remote Control URL. */
|
|
22
|
+
const RECENT_LAUNCH_URL_WAIT_MS = 30_000;
|
|
19
23
|
/** Optimistic file replacement preserves keys installed by other devices connecting at the same time. */
|
|
20
24
|
export async function installDeviceKey(cell, key, signal) {
|
|
21
25
|
const path = '/home/user/.ssh/authorized_keys';
|
|
@@ -128,6 +132,78 @@ export function proxyStream(io, ended, outputGate) {
|
|
|
128
132
|
source.once('error', (error) => stream.destroy(error));
|
|
129
133
|
return stream;
|
|
130
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The Remote Control URL (https://claude.ai/code/session_…) of the workspace's live Claude session. A Claude process
|
|
137
|
+
* this computer just started can accept its first prompt before Remote Control registers, so an open of our recent
|
|
138
|
+
* launch probes the guest for up to 30 s; history browsing stays passive.
|
|
139
|
+
*/
|
|
140
|
+
export async function remoteControlUrl(ctx, ws) {
|
|
141
|
+
let cursor;
|
|
142
|
+
let url;
|
|
143
|
+
do {
|
|
144
|
+
const page = await ctx.cloud().agents.list({ limit: 200, ...(cursor ? { cursor } : {}) });
|
|
145
|
+
url = page.data.find(a => a.workspace_id === ws.id && a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
146
|
+
cursor = page.nextCursor ?? undefined;
|
|
147
|
+
} while (!url && cursor && !ctx.signal.aborted);
|
|
148
|
+
// A new Claude process can accept its first prompt before Remote Control registers.
|
|
149
|
+
// Only an explicit open of our recent launch probes the guest; history browsing stays passive.
|
|
150
|
+
const launchedAt = new AgentStateStore(ctx.store.dir).read().agent_launches?.[ws.id];
|
|
151
|
+
if (!url && typeof launchedAt === 'number' && Date.now() - launchedAt < LAUNCH_WINDOW_MS && ws.ready) {
|
|
152
|
+
const waitMs = Math.min(ctx.wakeTimeoutMs, RECENT_LAUNCH_URL_WAIT_MS);
|
|
153
|
+
const deadline = Date.now() + waitMs;
|
|
154
|
+
const cell = ws.cell({ agentLabel: ctx.agentLabel });
|
|
155
|
+
while (!url && Date.now() < deadline) {
|
|
156
|
+
const result = await cell.exec.run(['sf-agent', 'list'], {
|
|
157
|
+
timeoutMs: Math.min(10_000, Math.max(1, deadline - Date.now())), signal: ctx.signal, maxOutputBytes: 1 << 20,
|
|
158
|
+
});
|
|
159
|
+
if (result.exitCode !== 0)
|
|
160
|
+
break;
|
|
161
|
+
url = guestSessions(result.stdout).find(a => a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
162
|
+
if (!url && Date.now() < deadline)
|
|
163
|
+
await delay(Math.min(250, Math.max(1, deadline - Date.now())), undefined, { signal: ctx.signal });
|
|
164
|
+
}
|
|
165
|
+
if (!url)
|
|
166
|
+
throw new Error(`Claude in ${ws.key} did not publish a Remote Control URL within ${Math.ceil(waitMs / 1000)} s; run shard agents open ${ws.key} again`);
|
|
167
|
+
}
|
|
168
|
+
if (!url)
|
|
169
|
+
throw new UsageError(`${ws.key} has no active Remote Control URL; start Claude with Remote Control first`);
|
|
170
|
+
const parsed = new URL(url);
|
|
171
|
+
if (parsed.protocol !== 'https:' || parsed.hostname !== 'claude.ai' || !parsed.pathname.startsWith('/code/') || parsed.username || parsed.password)
|
|
172
|
+
throw new Error('agent returned an invalid Claude Remote Control URL');
|
|
173
|
+
return url;
|
|
174
|
+
}
|
|
175
|
+
/** Claude Desktop's own link to a Remote Control session: claude://code/<session id> opens it in its Code tab. */
|
|
176
|
+
export function claudeDesktopLink(remoteControl) {
|
|
177
|
+
const id = new URL(remoteControl).pathname.split('/')[2] ?? '';
|
|
178
|
+
if (!/^(cse|session)_[A-Za-z0-9_-]+$/.test(id))
|
|
179
|
+
throw new Error('agent returned an invalid Claude Remote Control URL');
|
|
180
|
+
return `claude://code/${id}`;
|
|
181
|
+
}
|
|
182
|
+
export const CLAUDE_DESKTOP_BUNDLE = 'com.anthropic.claudefordesktop';
|
|
183
|
+
/**
|
|
184
|
+
* Opens the conversation itself (its live Remote Control session) in Claude Desktop (0.15.1+), the way `--in auto`
|
|
185
|
+
* opens a Claude conversation when Claude Desktop is installed. --json prints {target, url, app_bundle_id} for apps.
|
|
186
|
+
*/
|
|
187
|
+
export async function openClaudeDesktopSession(ctx, ws) {
|
|
188
|
+
await ensureCloudClaude(ctx, ws);
|
|
189
|
+
const session = await remoteControlUrl(ctx, ws);
|
|
190
|
+
const url = claudeDesktopLink(session);
|
|
191
|
+
if (!ctx.json)
|
|
192
|
+
await openLink(url, ctx.io.env);
|
|
193
|
+
out(ctx, `${url}\n`, { target: 'claude-desktop', workspace: ws.data, url, remote_control_url: session, app_bundle_id: CLAUDE_DESKTOP_BUNDLE });
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
async function openLink(url, env) {
|
|
197
|
+
const child = spawn('open', [url], { env, stdio: 'ignore' });
|
|
198
|
+
await new Promise((resolve, reject) => {
|
|
199
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('Claude Desktop did not open in time')); }, 10_000);
|
|
200
|
+
child.once('error', e => { clearTimeout(timer); reject(e); });
|
|
201
|
+
child.once('exit', code => { clearTimeout(timer); if (code === 0)
|
|
202
|
+
resolve();
|
|
203
|
+
else
|
|
204
|
+
reject(new Error(`could not open ${url}`)); });
|
|
205
|
+
});
|
|
206
|
+
}
|
|
131
207
|
const openTarget = (target) => async (ctx, values, [ref = 'main']) => {
|
|
132
208
|
const detected = installedTargets(ctx.io.env);
|
|
133
209
|
if (target !== 'web' && !detected.targets.includes(target))
|
|
@@ -138,18 +214,7 @@ const openTarget = (target) => async (ctx, values, [ref = 'main']) => {
|
|
|
138
214
|
if (target !== 'codex-app')
|
|
139
215
|
await ensureCloudClaude(ctx, ws);
|
|
140
216
|
if (target === 'web') {
|
|
141
|
-
|
|
142
|
-
let url;
|
|
143
|
-
do {
|
|
144
|
-
const page = await ctx.cloud().agents.list({ limit: 200, ...(cursor ? { cursor } : {}) });
|
|
145
|
-
url = page.data.find(a => a.workspace_id === ws.id && a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
146
|
-
cursor = page.nextCursor ?? undefined;
|
|
147
|
-
} while (!url && cursor && !ctx.signal.aborted);
|
|
148
|
-
if (!url)
|
|
149
|
-
throw new UsageError(`${ws.key} has no active Remote Control URL; start Claude with Remote Control first`);
|
|
150
|
-
const parsed = new URL(url);
|
|
151
|
-
if (parsed.protocol !== 'https:' || parsed.hostname !== 'claude.ai' || !parsed.pathname.startsWith('/code/') || parsed.username || parsed.password)
|
|
152
|
-
throw new Error('agent returned an invalid Claude Remote Control URL');
|
|
217
|
+
const url = await remoteControlUrl(ctx, ws);
|
|
153
218
|
if (!ctx.json && !openUrl(url, ctx.io.env))
|
|
154
219
|
throw new Error(`could not open ${url}`);
|
|
155
220
|
out(ctx, `${url}\n`, { target, workspace: ws.data, url });
|
|
@@ -173,7 +238,8 @@ export async function openDesktopWorkspace(ctx, ws, target, t3Version, prewarmed
|
|
|
173
238
|
if (!ctx.json)
|
|
174
239
|
await openApp(target, ctx.io.env);
|
|
175
240
|
new AgentStateStore(ctx.store.dir).saveTarget(ws.labels.repo_remote || ws.labels.shard_project || entry.project, target);
|
|
176
|
-
out(ctx, `${instructions}\n`, { target, workspace: ws.data, host: entry.host, instructions
|
|
241
|
+
out(ctx, `${instructions}\n`, { target, workspace: ws.data, host: entry.host, instructions,
|
|
242
|
+
app_bundle_id: { 'claude-desktop': 'com.anthropic.claudefordesktop', 'codex-app': 'com.openai.codex', t3: 'com.t3tools.t3code' }[target] });
|
|
177
243
|
return 0;
|
|
178
244
|
}
|
|
179
245
|
export async function queueT3Prewarm(ctx, ws, version) {
|
package/dist/agent-targets.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface TargetEntry {
|
|
|
10
10
|
export declare function targetIdentity(key: string): Pick<TargetEntry, 'host' | 'workspace' | 'project' | 'agent'>;
|
|
11
11
|
/** Replace only a marked block, retaining every byte outside it. Put Includes before user Host blocks. */
|
|
12
12
|
export declare function managedBlock(text: string, body: string): string;
|
|
13
|
+
/** The installed T3 Code apps (any channel), with their path and version, in the given app folders. */
|
|
14
|
+
export declare function t3Apps(dirs: string[], platform?: NodeJS.Platform): Array<{
|
|
15
|
+
path: string;
|
|
16
|
+
version: string | null;
|
|
17
|
+
}>;
|
|
18
|
+
/** The app folders scanned for desktop apps: SHARDFLUX_APPLICATIONS_DIRS, else /Applications and ~/Applications. */
|
|
19
|
+
export declare function applicationDirs(env: Record<string, string | undefined>): string[];
|
|
13
20
|
export declare function installedTargets(env: Record<string, string | undefined>, platform?: NodeJS.Platform, appDirs?: string[]): {
|
|
14
21
|
targets: string[];
|
|
15
22
|
clients: string[];
|
package/dist/agent-targets.js
CHANGED
|
@@ -33,10 +33,47 @@ export function managedBlock(text, body) {
|
|
|
33
33
|
const stop = end + END.length + (text[end + END.length] === '\n' ? 1 : 0);
|
|
34
34
|
return text.slice(0, start) + block + text.slice(stop);
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/** The installed T3 Code apps (any channel), with their path and version, in the given app folders. */
|
|
37
|
+
export function t3Apps(dirs, platform = process.platform) {
|
|
38
|
+
const found = [];
|
|
39
|
+
if (platform !== 'darwin')
|
|
40
|
+
return found;
|
|
41
|
+
for (const dir of dirs) {
|
|
42
|
+
let names;
|
|
43
|
+
try {
|
|
44
|
+
names = readdirSync(dir);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const name of names.filter(name => name.endsWith('.app'))) {
|
|
50
|
+
const info = join(dir, name, 'Contents', 'Info.plist');
|
|
51
|
+
// XML and binary plists store this ASCII bundle id verbatim. Avoid a
|
|
52
|
+
// subprocess for every unrelated app on the normal no-T3 path.
|
|
53
|
+
try {
|
|
54
|
+
if (!readFileSync(info).includes('com.t3tools.t3code'))
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const result = spawnSync('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', '-c', 'Print :CFBundleShortVersionString', info], { input: '', encoding: 'utf8', timeout: 1000 });
|
|
61
|
+
const [bundle, version] = result.stdout?.trim().split('\n') ?? [];
|
|
62
|
+
if (bundle !== 'com.t3tools.t3code')
|
|
63
|
+
continue;
|
|
64
|
+
found.push({ path: join(dir, name), version: result.status === 0 && version && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version) ? version : null });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return found;
|
|
68
|
+
}
|
|
69
|
+
/** The app folders scanned for desktop apps: SHARDFLUX_APPLICATIONS_DIRS, else /Applications and ~/Applications. */
|
|
70
|
+
export function applicationDirs(env) {
|
|
37
71
|
const home = env.HOME || homedir();
|
|
72
|
+
return env.SHARDFLUX_APPLICATIONS_DIRS !== undefined ? env.SHARDFLUX_APPLICATIONS_DIRS.split(':').filter(Boolean) : ['/Applications', join(home, 'Applications')];
|
|
73
|
+
}
|
|
74
|
+
export function installedTargets(env, platform = process.platform, appDirs) {
|
|
38
75
|
// SHARDFLUX_APPLICATIONS_DIRS (colon-separated) replaces the scanned app folders, like SHARDFLUX_CONFIG_DIR does for config.
|
|
39
|
-
const dirs = appDirs ?? (env
|
|
76
|
+
const dirs = appDirs ?? applicationDirs(env);
|
|
40
77
|
const hasApp = (name, bundle) => platform === 'darwin' && dirs.map(dir => join(dir, name)).some(path => {
|
|
41
78
|
const info = join(path, 'Contents', 'Info.plist');
|
|
42
79
|
if (!existsSync(info))
|
|
@@ -45,36 +82,11 @@ export function installedTargets(env, platform = process.platform, appDirs) {
|
|
|
45
82
|
return result.status === 0 && result.stdout.trim() === bundle;
|
|
46
83
|
});
|
|
47
84
|
const versions = {};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
names = readdirSync(dir);
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
for (const name of names.filter(name => name.endsWith('.app'))) {
|
|
59
|
-
const info = join(dir, name, 'Contents', 'Info.plist');
|
|
60
|
-
// XML and binary plists store this ASCII bundle id verbatim. Avoid a
|
|
61
|
-
// subprocess for every unrelated app on the normal no-T3 path.
|
|
62
|
-
try {
|
|
63
|
-
if (!readFileSync(info).includes('com.t3tools.t3code'))
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
const result = spawnSync('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', '-c', 'Print :CFBundleShortVersionString', info], { input: '', encoding: 'utf8', timeout: 1000 });
|
|
70
|
-
const [bundle, version] = result.stdout?.trim().split('\n') ?? [];
|
|
71
|
-
if (bundle !== 'com.t3tools.t3code')
|
|
72
|
-
continue;
|
|
73
|
-
t3 = true;
|
|
74
|
-
if (result.status === 0 && version && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
75
|
-
versions.t3 ??= version;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
85
|
+
const t3Bundles = t3Apps(dirs, platform);
|
|
86
|
+
const t3 = t3Bundles.length > 0;
|
|
87
|
+
for (const app of t3Bundles)
|
|
88
|
+
if (app.version)
|
|
89
|
+
versions.t3 ??= app.version;
|
|
78
90
|
const executable = (name) => (env.PATH ?? '').split(':').filter(Boolean).some(p => { try {
|
|
79
91
|
accessSync(join(p, name), constants.X_OK);
|
|
80
92
|
return true;
|
package/dist/args.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.
|
|
1
|
+
export declare const CLI_VERSION = "0.15.1";
|
|
2
2
|
export interface OptionSpec {
|
|
3
3
|
type: 'string' | 'boolean';
|
|
4
4
|
short?: string;
|
|
@@ -6,6 +6,8 @@ export interface OptionSpec {
|
|
|
6
6
|
/** Placeholder shown in help, e.g. `<slug>`. */
|
|
7
7
|
value?: string;
|
|
8
8
|
description: string;
|
|
9
|
+
/** Accept the option for compatibility but omit it from help. */
|
|
10
|
+
hidden?: boolean;
|
|
9
11
|
}
|
|
10
12
|
export interface PositionalSpec {
|
|
11
13
|
name: string;
|
package/dist/args.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { parseArgs } from 'node:util';
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
|
-
export const CLI_VERSION = '0.
|
|
15
|
+
export const CLI_VERSION = '0.15.1';
|
|
16
16
|
export const GLOBAL_OPTIONS = {
|
|
17
17
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
18
18
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -268,7 +268,7 @@ const KEY_COMMANDS = [
|
|
|
268
268
|
'delete-after-idle': { type: 'string', value: '<days>d', description: 'Delete this persistent workspace after 1..3650 idle days (e.g. 14d).' },
|
|
269
269
|
like: { type: 'string', value: '<id|key>', description: 'Open a fresh workspace with the source’s template and configuration.' },
|
|
270
270
|
volume: { type: 'string', multiple: true, value: '<name:/path[:ro]>', description: 'Replace the named volume mounts; repeat for each mount.' },
|
|
271
|
-
reachable: { type: 'string', value: '<duration>', description: '
|
|
271
|
+
reachable: { type: 'string', value: '<duration>', hidden: true, description: 'Deprecated; ignored. Claude conversations stay reachable without a time limit.' },
|
|
272
272
|
'agent-idle-grace-seconds': { type: 'string', value: '<0-3600>', description: 'Grace after a coding agent waits or exits (default 300 seconds).' },
|
|
273
273
|
'idle-policy': { type: 'string', value: '<policy>', description: 'adaptive, never or fixed:<seconds>.' },
|
|
274
274
|
secret: {
|
|
@@ -876,9 +876,10 @@ const KEY_COMMANDS = [
|
|
|
876
876
|
{ path: ['credentials', 'list'], summary: 'Show pending login requests', positionals: [], options: {} },
|
|
877
877
|
{ path: ['credentials', 'enable'], summary: 'Enable credential fill in a workspace', positionals: [REF], options: {} },
|
|
878
878
|
{ path: ['credentials', 'deny'], summary: 'Deny a login request', positionals: [{ name: 'request-id', required: true }], options: {} },
|
|
879
|
-
{ path: ['credentials', 'forget'], summary: 'Remove a remembered site credential', positionals: [REF, { name: 'site', required: true }], options: {} },
|
|
879
|
+
{ path: ['credentials', 'forget'], summary: 'Remove a remembered site credential', positionals: [REF, { name: 'site', required: true }], options: { scope: { type: 'string', value: '<conversation|project|machine>', description: 'Remembered sign-in scope (default project).' } } },
|
|
880
880
|
{ path: ['credentials', 'approve'], summary: 'Approve a login directly from your vault', positionals: [{ name: 'request-id', required: true }], options: {
|
|
881
881
|
from: { type: 'string', description: 'hsec, 1password, bitwarden or keychain.' }, item: { type: 'string', description: 'Vault login item name.' },
|
|
882
|
+
scope: { type: 'string', value: '<ask|project|machine>', description: 'Future-request scope; unattended sign-in uses the same scope.' },
|
|
882
883
|
always: { type: 'boolean', description: 'Always allow these fields for this project on this laptop.' }, remember: { type: 'boolean', description: 'Remember for unattended use, encrypted with AWS KMS.' },
|
|
883
884
|
} },
|
|
884
885
|
{
|
|
@@ -1166,8 +1167,12 @@ const AGENT_CREDENTIAL_COMMANDS = [
|
|
|
1166
1167
|
...['sync', 'status', 'forget'].map((action) => ({ path: ['agents', 'credentials', action], summary: `${action} Codex copies and the account's cloud Claude login (contents never printed)`, positionals: [], options: {} })),
|
|
1167
1168
|
{ path: ['agents', 'credentials', 'login'], summary: 'Sign in once to cloud Claude for this account', positionals: [{ name: 'app', required: true }], options: { workspace: { type: 'string', value: '<id|key>', description: 'Use this workspace for the cloud sign-in.' } } },
|
|
1168
1169
|
];
|
|
1170
|
+
const COMPUTER_OPTION = { type: 'boolean', description: 'Give the new conversation a desktop with a browser its agent drives (computer use).' };
|
|
1169
1171
|
const AGENT_OPTIONS = {
|
|
1170
|
-
|
|
1172
|
+
prompt: { type: 'string', value: '<text>', description: 'First user message for a new conversation.' },
|
|
1173
|
+
'prompt-file': { type: 'string', value: '<file|->', description: 'Read the first user message from a UTF-8 file or stdin (-), up to 256 KiB.' },
|
|
1174
|
+
image: { type: 'string', multiple: true, value: '<file|->', description: 'Attach a PNG, JPEG, GIF or WebP image to the first message (repeatable, up to 5 of 10 MiB each; - reads PNG images back to back from stdin) (0.15.0+).' },
|
|
1175
|
+
reachable: { type: 'string', value: '<duration>', hidden: true, description: 'Deprecated; ignored. Claude conversations stay reachable without a time limit.' },
|
|
1171
1176
|
approvals: { type: 'boolean', description: 'Keep the agent’s permission prompts.' },
|
|
1172
1177
|
};
|
|
1173
1178
|
export const PROJECT_COMMAND = {
|
|
@@ -1175,6 +1180,7 @@ export const PROJECT_COMMAND = {
|
|
|
1175
1180
|
description: 'Opens Claude Code or Codex directly. --resume continues a project conversation. With --json it starts detached and prints the session. Detach with Ctrl-] then d.',
|
|
1176
1181
|
positionals: [{ name: 'path', required: true }, { name: 'claude|codex' }], options: {
|
|
1177
1182
|
...AGENT_OPTIONS,
|
|
1183
|
+
computer: COMPUTER_OPTION,
|
|
1178
1184
|
new: { type: 'boolean', description: 'Start a new conversation.' },
|
|
1179
1185
|
resume: { type: 'boolean', description: 'Continue a conversation from this project.' },
|
|
1180
1186
|
claude: { type: 'boolean', description: 'Same as the claude argument (0.14.4+).' },
|
|
@@ -1200,9 +1206,11 @@ const AGENT_COMMANDS = [
|
|
|
1200
1206
|
...(action === 'keep' ? { push: { type: 'boolean', description: 'Push the kept branch to origin and open a PR when gh is installed.' } } : {}),
|
|
1201
1207
|
},
|
|
1202
1208
|
})),
|
|
1203
|
-
|
|
1204
|
-
{ path: ['agents', '
|
|
1209
|
+
...['status', 'enable', 'disable'].map(action => ({ path: ['agents', 'codex', action], summary: `${action} the account's billed Codex Cloud connector`, positionals: [], options: {} })),
|
|
1210
|
+
{ 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.' } } },
|
|
1211
|
+
{ path: ['agents', 'new'], summary: 'Start a conversation in a fresh VM; Codex opens a genuine Cloud thread', positionals: [{ name: 'agent' }], options: {
|
|
1205
1212
|
...AGENT_OPTIONS,
|
|
1213
|
+
computer: COMPUTER_OPTION,
|
|
1206
1214
|
app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' },
|
|
1207
1215
|
path: { type: 'string', value: '<path>', description: 'Local project path for first launch.' },
|
|
1208
1216
|
name: { type: 'string', value: '<name>', description: 'New agent name; copies the project main workspace with --like.' },
|
|
@@ -1396,6 +1404,9 @@ function toParseArgsOptions(opts) {
|
|
|
1396
1404
|
* returns help text for --help, bare groups and non-TTY bare `shard`.
|
|
1397
1405
|
*/
|
|
1398
1406
|
export function parseCommandLine(argv, options = {}) {
|
|
1407
|
+
// `acp --login` is the ACP terminal sign-in an editor runs (ACP appends the method's args to `shard acp`).
|
|
1408
|
+
if (argv.length === 2 && argv[0] === 'acp' && argv[1] === '--login')
|
|
1409
|
+
return { kind: 'run', command: { path: ['acp'], summary: '', positionals: [], options: {} }, values: { login: true }, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
|
|
1399
1410
|
if (argv.length === 1 && (argv[0] === 'acp' || argv[0] === 'device-helper'))
|
|
1400
1411
|
return { kind: 'run', command: { path: [argv[0]], summary: '', positionals: [], options: {} }, values: {}, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
|
|
1401
1412
|
const terminator = argv.indexOf('--');
|
|
@@ -1559,7 +1570,7 @@ function emailedTokensAsPositionals(rest, command) {
|
|
|
1559
1570
|
}
|
|
1560
1571
|
// ---- help ----------------------------------------------------------------------------------
|
|
1561
1572
|
function optionLines(opts) {
|
|
1562
|
-
const rows = Object.entries(opts).map(([name, o]) => [`${o.short ? `-${o.short}, ` : ' '}--${name}${o.value ? ` ${o.value}` : ''}`, o.description]);
|
|
1573
|
+
const rows = Object.entries(opts).filter(([, o]) => !o.hidden).map(([name, o]) => [`${o.short ? `-${o.short}, ` : ' '}--${name}${o.value ? ` ${o.value}` : ''}`, o.description]);
|
|
1563
1574
|
const w = Math.max(0, ...rows.map((r) => r[0].length));
|
|
1564
1575
|
return rows.map(([l, d]) => ` ${l.padEnd(w)} ${d}`);
|
|
1565
1576
|
}
|
|
@@ -1574,8 +1585,9 @@ export function commandHelp(c) {
|
|
|
1574
1585
|
const lines = [`Usage: ${usageLine(c)}`, '', c.summary + '.'];
|
|
1575
1586
|
if (c.description)
|
|
1576
1587
|
lines.push('', c.description);
|
|
1577
|
-
|
|
1578
|
-
|
|
1588
|
+
const options = optionLines(c.options);
|
|
1589
|
+
if (options.length > 0)
|
|
1590
|
+
lines.push('', 'Options:', ...options);
|
|
1579
1591
|
lines.push('', 'Global options:', ...optionLines(GLOBAL_OPTIONS));
|
|
1580
1592
|
if (c.examples?.length)
|
|
1581
1593
|
lines.push('', 'Examples:', ...c.examples.map((e) => ` ${e}`));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AccountClientOptions, AuthUser } from '@shardflux/sdk';
|
|
2
|
+
export interface DeviceAuthorization {
|
|
3
|
+
device_code: string;
|
|
4
|
+
user_code: string;
|
|
5
|
+
verification_uri: string;
|
|
6
|
+
verification_uri_complete: string;
|
|
7
|
+
expires_in: number;
|
|
8
|
+
interval: number;
|
|
9
|
+
}
|
|
10
|
+
export interface DeviceSession {
|
|
11
|
+
status: 'authenticated';
|
|
12
|
+
user: AuthUser;
|
|
13
|
+
session_token: string;
|
|
14
|
+
session_expires_at: string;
|
|
15
|
+
}
|
|
16
|
+
export interface DevicePollOptions {
|
|
17
|
+
signal: AbortSignal;
|
|
18
|
+
/** Waits `ms` (ends early when the signal aborts). */
|
|
19
|
+
sleep(ms: number): Promise<void>;
|
|
20
|
+
now?: () => number;
|
|
21
|
+
/** A token poll that has not answered after this long is a connection timeout (default 15 s). */
|
|
22
|
+
requestTimeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/** The default name on the approval page: "shard CLI on <hostname>". */
|
|
25
|
+
export declare function defaultClientName(hostname: string): string;
|
|
26
|
+
/** POST /v1/auth/device. */
|
|
27
|
+
export declare function startDeviceAuthorization(opts: AccountClientOptions, clientName: string, signal: AbortSignal): Promise<DeviceAuthorization>;
|
|
28
|
+
/**
|
|
29
|
+
* Polls POST /v1/auth/device/token until the session is issued (RFC 8628 §3.4, §3.5): waits `interval` seconds before
|
|
30
|
+
* each poll; a slow_down adds 5 s (or takes the server's longer interval); a connection failure or timeout doubles the
|
|
31
|
+
* interval (exponential backoff); 429 waits its Retry-After and 5xx the interval, while the request can still be
|
|
32
|
+
* approved. Stops on access_denied, expired_token and every other refusal (thrown as the API's error), and when the
|
|
33
|
+
* request's lifetime has passed.
|
|
34
|
+
*/
|
|
35
|
+
export declare function waitForDeviceSession(opts: AccountClientOptions, d: DeviceAuthorization, poll: DevicePollOptions): Promise<DeviceSession>;
|