@shardflux/cli 0.15.2 → 0.17.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 +144 -14
- package/README.md +92 -7
- package/dist/acp-models.js +5 -1
- package/dist/agent-commands.d.ts +15 -1
- package/dist/agent-commands.js +84 -23
- package/dist/agent-models.d.ts +61 -0
- package/dist/agent-models.js +79 -0
- package/dist/agent-state.d.ts +10 -0
- package/dist/agent-state.js +13 -0
- package/dist/agent-target-commands.d.ts +13 -3
- package/dist/agent-target-commands.js +129 -23
- package/dist/agent-targets.d.ts +5 -0
- package/dist/agent-targets.js +14 -5
- package/dist/agent-terminal.d.ts +8 -0
- package/dist/agent-terminal.js +35 -2
- package/dist/args.d.ts +5 -1
- package/dist/args.js +74 -11
- package/dist/cli-proxy-forward.d.ts +45 -0
- package/dist/cli-proxy-forward.js +432 -0
- package/dist/cli-proxy-hub.d.ts +273 -0
- package/dist/cli-proxy-hub.js +749 -0
- package/dist/cli-proxy.d.ts +325 -0
- package/dist/cli-proxy.js +1016 -0
- package/dist/codex-cloud.d.ts +3 -0
- package/dist/codex-cloud.js +17 -5
- package/dist/command-center-actions.js +25 -1
- package/dist/commands.d.ts +22 -0
- package/dist/commands.js +54 -14
- package/dist/conversation-commands.js +6 -0
- package/dist/conversation-local.js +2 -2
- package/dist/conversation-stash.js +4 -3
- package/dist/conversation-terminal.d.ts +5 -0
- package/dist/conversation-terminal.js +54 -6
- package/dist/conversations.d.ts +31 -3
- package/dist/conversations.js +53 -10
- package/dist/device-approvals.d.ts +12 -0
- package/dist/device-approvals.js +73 -0
- package/dist/device-helper.d.ts +3 -1
- package/dist/device-helper.js +14 -4
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +12 -2
- package/dist/files-mirror.d.ts +164 -0
- package/dist/files-mirror.js +761 -0
- package/dist/handlers.js +5 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/linux-helper.d.ts +12 -0
- package/dist/linux-helper.js +88 -0
- package/dist/linux-terminal.d.ts +11 -0
- package/dist/linux-terminal.js +42 -0
- package/dist/main.js +38 -8
- package/dist/onboarding.d.ts +2 -0
- package/dist/onboarding.js +53 -5
- package/dist/registry.d.ts +2 -42
- package/dist/registry.js +6 -90
- package/dist/t3-launch.d.ts +37 -0
- package/dist/t3-launch.js +133 -0
- package/dist/t3-mode.d.ts +121 -0
- package/dist/t3-mode.js +654 -0
- package/dist/t3-phone.d.ts +17 -0
- package/dist/t3-phone.js +15 -0
- package/dist/t3-thread-script.d.ts +18 -0
- package/dist/t3-thread-script.js +245 -0
- package/dist/terminal-onboarding.d.ts +11 -0
- package/dist/terminal-onboarding.js +75 -0
- package/dist/thread-on-computer.d.ts +35 -0
- package/dist/thread-on-computer.js +206 -0
- package/dist/update.d.ts +1 -1
- package/dist/update.js +12 -1
- package/dist/wait-status.d.ts +22 -0
- package/dist/wait-status.js +59 -0
- package/dist/worktree-twin-engine.d.ts +125 -0
- package/dist/worktree-twin-engine.js +615 -0
- package/dist/worktree-twin-land.py +331 -0
- package/dist/worktree-twin.d.ts +313 -0
- package/dist/worktree-twin.js +843 -0
- package/package.json +3 -3
package/dist/agent-commands.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { linuxTerminalSession, openLinuxTerminal } from "./linux-terminal.js";
|
|
2
|
+
import { repositoryName } from "./files-mirror.js";
|
|
1
3
|
import { prepareSetupCache } from "./setup-cache.js";
|
|
2
4
|
import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
|
|
3
5
|
import { agentReachability } from "./agent-reachability.js";
|
|
@@ -22,10 +24,12 @@ import { progress } from "./progress.js";
|
|
|
22
24
|
import { rememberLaunch } from "./agent-live.js";
|
|
23
25
|
import { prepareAgentMachine } from "./agent-machine.js";
|
|
24
26
|
import { installedTargets } from "./agent-targets.js";
|
|
27
|
+
import { openInFor } from "./t3-mode.js";
|
|
25
28
|
import { openClaudeDesktopSession, openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
26
29
|
import { submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
27
30
|
import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
28
31
|
import { conversationTerminal } from "./conversation-terminal.js";
|
|
32
|
+
import { CHOICE_PENDING, choiceReport, hasChoice, launchChoice, modelChoice, runFlags } from "./agent-models.js";
|
|
29
33
|
const active = (session) => session.state === 'running' || session.state === 'starting';
|
|
30
34
|
/** `<slug>-<6 hex of sha256(identity)>`: the same repository gets the same key on every machine. */
|
|
31
35
|
export function disambiguatedSlug(slug, identity) {
|
|
@@ -47,7 +51,9 @@ export async function projectTemplate(ctx, remote) {
|
|
|
47
51
|
throw error;
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
|
-
|
|
54
|
+
/** `place` is the repository or folder name session titles end with ("todo-app"), `task` what a new session is titled
|
|
55
|
+
* with before the guest names it from its first prompt (a first prompt line or the conversation's title). */
|
|
56
|
+
export async function agentPty(cell, app, agent, approvals, term, signal, place, setupHash, cwd, ptyName, onLaunch, deferSetup = false, conversation, task, launch) {
|
|
51
57
|
const id = `shard-${ptyName || agent}-${app}`;
|
|
52
58
|
// Arrival and changed locks queue the same durable guest job even when the
|
|
53
59
|
// native agent is already alive. Setup never becomes its first user turn.
|
|
@@ -68,18 +74,19 @@ export async function agentPty(cell, app, agent, approvals, term, signal, projec
|
|
|
68
74
|
onLaunch?.();
|
|
69
75
|
let conversationArgs = [];
|
|
70
76
|
if (conversation) {
|
|
71
|
-
const help = await
|
|
72
|
-
if (
|
|
77
|
+
const help = await (launch?.help ?? runHelp(cell, app, signal))();
|
|
78
|
+
if (help.includes('-conversation'))
|
|
73
79
|
conversationArgs = ['--conversation', conversation.id, ...(conversation.session ? ['--resume-session', conversation.session] : [])];
|
|
74
80
|
}
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
// A model and effort choice reaches only a guest whose run accepts it (the caller checked).
|
|
82
|
+
const started = await cell.exec.run(['sf-agent', 'run', app, '--name', agent, '--detach', ...conversationArgs, ...(ptyName ? ['--pty-name', ptyName] : []), ...(approvals ? ['--approvals'] : []), ...(setupHash ? ['--setup', '--setup-deps-hash', setupHash] : []), ...runFlags(launch?.choice)], {
|
|
83
|
+
env: { TERM: term, ...(place ? { SF_PROJECT_NAME: place, SF_REPO_NAME: place } : {}), ...(task ? { SF_TASK_TITLE: task } : {}), ...(setupHash && deferSetup ? { SF_SETUP_DEFERRED: '1' } : {}) }, timeoutMs: 30_000, maxOutputBytes: 4096, signal, ...(cwd ? { cwd } : {}),
|
|
77
84
|
});
|
|
78
|
-
if (
|
|
79
|
-
throw new Error(`agent launch failed: ${
|
|
85
|
+
if (started.exitCode !== 0)
|
|
86
|
+
throw new Error(`agent launch failed: ${started.stderr.trim() || `exit ${started.exitCode}`}`);
|
|
80
87
|
let result;
|
|
81
88
|
try {
|
|
82
|
-
result = JSON.parse(
|
|
89
|
+
result = JSON.parse(started.stdout);
|
|
83
90
|
}
|
|
84
91
|
catch {
|
|
85
92
|
throw new Error('agent launcher returned invalid JSON');
|
|
@@ -91,7 +98,18 @@ export async function agentPty(cell, app, agent, approvals, term, signal, projec
|
|
|
91
98
|
throw new Error(`agent exited before attach: ${session.state}`);
|
|
92
99
|
return session;
|
|
93
100
|
}
|
|
101
|
+
/** `sf-agent run <app> --help` of this computer's guest, read at most once per launch. */
|
|
102
|
+
function runHelp(cell, app, signal) {
|
|
103
|
+
let text;
|
|
104
|
+
return () => text ??= cell.exec.run(['sf-agent', 'run', app, '--help'], { timeoutMs: 10_000, maxOutputBytes: 8192, signal }).then(r => r.stdout + r.stderr);
|
|
105
|
+
}
|
|
106
|
+
/** The first non-empty line of a prompt or title, bounded; the guest makes it the task part of the session title. */
|
|
107
|
+
export function sessionTask(text) {
|
|
108
|
+
const line = text?.split(/\r?\n/).map(l => l.replace(/[\s\p{Cc}]+/gu, ' ').trim()).find(Boolean);
|
|
109
|
+
return line ? [...line].slice(0, 200).join('') : undefined;
|
|
110
|
+
}
|
|
94
111
|
export async function terminal(ctx, ws, app, agent, project, values, setupHash, cwd = workspaceRepositoryPath(ws), conversation) {
|
|
112
|
+
const ptyName = conversation?.pty ?? ws.labels.agent_pty_name;
|
|
95
113
|
const store = new AgentStateStore(ctx.store.dir);
|
|
96
114
|
if (!ws.ready)
|
|
97
115
|
await ws.wake({ timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal });
|
|
@@ -101,12 +119,18 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
101
119
|
const tunnels = attach ? await startAgentConfigTunnels(ctx, ws) : null;
|
|
102
120
|
try {
|
|
103
121
|
if (app === 'codex' && typeof values.initialPrompt === 'string') {
|
|
104
|
-
await prepareInitialCodexProject(cell, cwd, `shard-${
|
|
122
|
+
await prepareInitialCodexProject(cell, cwd, `shard-${ptyName || agent}-${app}`);
|
|
105
123
|
}
|
|
106
124
|
const credentialFill = await configureCredentialFill(ctx, ws);
|
|
125
|
+
// This command's --model/--effort, else the conversation's own (a relaunch keeps its model). A computer whose guest
|
|
126
|
+
// predates the choice starts the agent with its defaults, and this command says so once.
|
|
127
|
+
const help = runHelp(cell, app, ctx.signal), explicit = modelChoice(values, app);
|
|
128
|
+
const { choice, applied } = await launchChoice(explicit, conversation?.choice, !!conversation, help);
|
|
129
|
+
if (hasChoice(explicit) && !applied && !ctx.json)
|
|
130
|
+
ctx.io.stderr.write(`${CHOICE_PENDING}\n`);
|
|
107
131
|
// A cold launch is remembered before it starts, so the listing can show it before the projection does (agent-live.ts).
|
|
108
132
|
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.
|
|
133
|
+
const session = await agentPty(cell, app, agent, values.approvals === true, term, ctx.signal, repositoryName(ws.labels.repo_remote || project), setupHash, cwd, ptyName, () => { rememberLaunch(ctx, ws.id); progress(ctx, `Starting ${app === 'claude' ? 'Claude' : 'Codex'} in ${ws.key}…`); }, true, conversation && { id: conversation.id, session: conversation.session }, sessionTask(typeof values.initialPrompt === 'string' ? values.initialPrompt : conversation?.title), { ...(choice ? { choice } : {}), help });
|
|
110
134
|
// A new conversation's guest can start the agent before this command reaches it, so the launch above never ran.
|
|
111
135
|
// Remember it either way: listing and `agents open` then read the guest until the projection lists the session.
|
|
112
136
|
rememberLaunch(ctx, ws.id);
|
|
@@ -126,7 +150,10 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
126
150
|
}
|
|
127
151
|
if (!attach) {
|
|
128
152
|
await preparation;
|
|
129
|
-
|
|
153
|
+
// conversation (0.16.0+): which conversation this session is, also when its computer hosts several threads.
|
|
154
|
+
// model_choice (0.17.0+): the requested model and effort, and whether this thread started with them.
|
|
155
|
+
ctx.io.stdout.write(json({ workspace: ws.data, session, ...(conversation ? { conversation: { id: conversation.id } } : {}), ...(typeof values.initialPrompt === 'string' ? { initial_prompt: { submitted: true } } : {}),
|
|
156
|
+
...(hasChoice(explicit) ? { model_choice: choiceReport(explicit, applied) } : {}) }));
|
|
130
157
|
return 0;
|
|
131
158
|
}
|
|
132
159
|
ctx.io.stderr.write(`Shardflux · ${ws.key}\nDetach: Ctrl-] then d\n`);
|
|
@@ -167,7 +194,9 @@ async function syncCredentials(ctx) {
|
|
|
167
194
|
throw new Error('agent credentials sync failed');
|
|
168
195
|
}
|
|
169
196
|
}
|
|
170
|
-
export const launchProject =
|
|
197
|
+
export const launchProject = (ctx, values, args) => launchProjectWith(ctx, values, args);
|
|
198
|
+
/** The project VM launch; with `--in t3` a first message becomes a thread on the T3 server in it (SSH mode, 0.17.0+). */
|
|
199
|
+
export async function launchProjectWith(ctx, values, [path = './', explicitApp], thread, install) {
|
|
171
200
|
if (explicitApp && explicitApp !== 'claude' && explicitApp !== 'codex')
|
|
172
201
|
throw new UsageError('agent app must be claude or codex');
|
|
173
202
|
let like = typeof values.like === 'string' ? await resolveWorkspace(ctx, values.like) : undefined;
|
|
@@ -179,8 +208,12 @@ export const launchProject = async (ctx, values, [path = './', explicitApp]) =>
|
|
|
179
208
|
if (target && (!['claude-desktop', 'codex-app', 't3'].includes(target) || !detected?.targets.includes(target)))
|
|
180
209
|
throw new UsageError(`${target} is not an installed desktop target`);
|
|
181
210
|
const app = explicitApp ?? store.app(projectKey, like?.labels.app === 'codex' ? 'codex' : 'claude');
|
|
182
|
-
|
|
211
|
+
// A typo in --model or --effort is refused before anything starts; T3 Code reads the same values (0.17.0+).
|
|
212
|
+
const choice = modelChoice(values, target === 'codex-app' ? 'codex' : app);
|
|
213
|
+
if (target === 'codex-app' || (app === 'codex' && !like && values.in === undefined && !linuxTerminalSession(ctx, values)))
|
|
183
214
|
return launchCodexCloud(ctx, values, path);
|
|
215
|
+
if (target === 'claude-desktop' && hasChoice(choice))
|
|
216
|
+
ctx.io.stderr.write('Claude Desktop starts the session with the model chosen in the app.\n');
|
|
184
217
|
const agent = typeof values.name === 'string' ? values.name : 'main';
|
|
185
218
|
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(agent))
|
|
186
219
|
throw new UsageError('agent name must match ^[a-z0-9][a-z0-9._-]{0,63}$');
|
|
@@ -226,8 +259,13 @@ export const launchProject = async (ctx, values, [path = './', explicitApp]) =>
|
|
|
226
259
|
await queueT3Prewarm(ctx, ws, detected.versions.t3);
|
|
227
260
|
if (app === 'claude' && !claudeReady)
|
|
228
261
|
await loginClaudeInWorkspace(ctx, ws);
|
|
262
|
+
// T3 lists both agents; a Codex project still opens when Claude is not signed in (T3 shows Claude as signed out).
|
|
229
263
|
if (target === 't3' && app === 'codex')
|
|
230
|
-
await ensureCloudClaude(ctx, ws)
|
|
264
|
+
await ensureCloudClaude(ctx, ws).catch((error) => {
|
|
265
|
+
if (!(error instanceof UsageError))
|
|
266
|
+
throw error;
|
|
267
|
+
ctx.io.stderr.write(`Claude Code in T3: ${error.message}\n`);
|
|
268
|
+
});
|
|
231
269
|
if (!(await savedMachine(ctx))?.template)
|
|
232
270
|
await syncAgentConfig(ctx, ws);
|
|
233
271
|
// Seed once; two-way sync is scoped to explicit Try locally worktrees.
|
|
@@ -241,15 +279,17 @@ export const launchProject = async (ctx, values, [path = './', explicitApp]) =>
|
|
|
241
279
|
throw new Error(`project setup could not be queued: ${setup.stderr.trim() || `exit ${setup.exitCode}`}`);
|
|
242
280
|
}
|
|
243
281
|
store.saveTarget(projectKey, target);
|
|
244
|
-
return openDesktopWorkspace(ctx, ws, target, detected?.versions.t3, true);
|
|
282
|
+
return openDesktopWorkspace(ctx, ws, target, detected?.versions.t3, true, thread, install);
|
|
245
283
|
}
|
|
246
284
|
await prepareAgentMachine(ctx, ws);
|
|
247
285
|
return await terminal(ctx, ws, app, agent, projectKey, values, prepared?.setupHash);
|
|
248
|
-
}
|
|
286
|
+
}
|
|
249
287
|
export const AGENT_HANDLERS = {
|
|
250
288
|
async 'agents open'(ctx, values, [agent = 'main']) {
|
|
251
289
|
if (values.app !== undefined && values.app !== 'claude' && values.app !== 'codex')
|
|
252
290
|
throw new UsageError('--app must be claude or codex');
|
|
291
|
+
if (process.platform === 'linux' && !ctx.json && !ctx.io.stdin.isTTY && values.detach !== true && (values.in === undefined || values.in === 'terminal'))
|
|
292
|
+
return openLinuxTerminal(ctx, agent, typeof values.app === 'string' ? values.app : undefined);
|
|
253
293
|
let resolvedConversation;
|
|
254
294
|
if (/^conv_[a-z0-9]{26}$/.test(agent)) {
|
|
255
295
|
resolvedConversation = await getConversation(ctx, agent);
|
|
@@ -258,19 +298,40 @@ export const AGENT_HANDLERS = {
|
|
|
258
298
|
}
|
|
259
299
|
if (values.in === 'auto') {
|
|
260
300
|
// The menu and other launchers use the same CLI-owned target selection. Never put provider URLs in a client.
|
|
261
|
-
|
|
301
|
+
// A conversation id opens that conversation's own session, also when its computer hosts other threads.
|
|
302
|
+
const ws = resolvedConversation ? await conversationWorkspace(ctx, resolvedConversation) : await resolveWorkspace(ctx, agent);
|
|
262
303
|
const project = ws.labels.repo_remote || ws.labels.shard_project || ws.key;
|
|
263
304
|
const store = new AgentStateStore(ctx.store.dir);
|
|
264
|
-
const app = values.app ?? ws.labels.shard_app ?? ws.labels.app ?? store.app(project);
|
|
305
|
+
const app = values.app ?? resolvedConversation?.harness ?? ws.labels.shard_app ?? ws.labels.app ?? store.app(project);
|
|
265
306
|
const detected = installedTargets(ctx.io.env);
|
|
266
|
-
|
|
307
|
+
// open_in (0.17.0+): a choice saved with `agents targets --open-in`, or T3 when it is the default, comes first.
|
|
308
|
+
// A conversation opens as itself (its id and session), as the fallbacks below open it.
|
|
309
|
+
const choice = app === 'claude' || app === 'codex' ? openInFor(ctx, ws.labels.shard_project || ws.key.split('/')[0], { clients: detected.clients })[app] : undefined;
|
|
310
|
+
const chosen = choice && (choice.saved || choice.target === 't3') ? choice.target : undefined;
|
|
311
|
+
const opened = resolvedConversation?.id ?? ws.key;
|
|
312
|
+
if (chosen === 't3' || chosen === 'web') {
|
|
313
|
+
const { HANDLERS } = await import("./handlers.js");
|
|
314
|
+
return HANDLERS[`agents open ${chosen}`](ctx, { ...values, in: chosen }, [opened]);
|
|
315
|
+
}
|
|
316
|
+
if (chosen === 'claude-desktop' && app === 'claude')
|
|
317
|
+
return openClaudeDesktopSession(ctx, ws, resolvedConversation);
|
|
318
|
+
if (chosen === 'terminal' && app === 'claude') {
|
|
319
|
+
if (ctx.json) {
|
|
320
|
+
out(ctx, `shard agents open ${opened}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', opened] });
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
const next = { ...values };
|
|
324
|
+
delete next.in;
|
|
325
|
+
return AGENT_HANDLERS['agents open'](ctx, next, [opened]);
|
|
326
|
+
}
|
|
327
|
+
const saved = chosen === 'codex-app' ? undefined : store.read().projects?.[project]?.target ??
|
|
267
328
|
(project.startsWith('file://') ? store.read().projects?.[new URL(project).pathname]?.target : undefined);
|
|
268
329
|
// Codex's default open implementation owns Cloud/desktop routing. PAS-XB replaces that path; do not pin
|
|
269
330
|
// the menu to the old per-VM SSH target. An explicit saved desktop target still takes precedence.
|
|
270
331
|
if ((!saved || !detected.targets.includes(saved)) && app === 'codex') {
|
|
271
332
|
const next = { ...values };
|
|
272
333
|
delete next.in;
|
|
273
|
-
return AGENT_HANDLERS['agents open'](ctx, next, [ws.key]);
|
|
334
|
+
return AGENT_HANDLERS['agents open'](ctx, next, [resolvedConversation?.id ?? ws.key]);
|
|
274
335
|
}
|
|
275
336
|
if (saved && detected.targets.includes(saved)) {
|
|
276
337
|
const { HANDLERS } = await import("./handlers.js");
|
|
@@ -282,16 +343,16 @@ export const AGENT_HANDLERS = {
|
|
|
282
343
|
// Claude opens where people work (0.15.1+): the conversation in Claude Desktop when it is installed, else Claude
|
|
283
344
|
// Code in a terminal. The browser (claude.ai) only when it is the chosen target (--in web).
|
|
284
345
|
if (detected.clients.includes('claude-desktop')) {
|
|
285
|
-
return openClaudeDesktopSession(ctx, ws);
|
|
346
|
+
return openClaudeDesktopSession(ctx, ws, resolvedConversation);
|
|
286
347
|
}
|
|
287
348
|
if (ctx.json) {
|
|
288
349
|
// An app runs this in the person's terminal (the menu bar opens Terminal with it).
|
|
289
|
-
out(ctx, `shard agents open ${
|
|
350
|
+
out(ctx, `shard agents open ${opened}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', opened] });
|
|
290
351
|
return 0;
|
|
291
352
|
}
|
|
292
353
|
const next = { ...values };
|
|
293
354
|
delete next.in;
|
|
294
|
-
return AGENT_HANDLERS['agents open'](ctx, next, [
|
|
355
|
+
return AGENT_HANDLERS['agents open'](ctx, next, [opened]);
|
|
295
356
|
}
|
|
296
357
|
if (values.in !== undefined && values.in !== 'terminal') {
|
|
297
358
|
const { HANDLERS } = await import("./handlers.js");
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Values } from './args.js';
|
|
2
|
+
export type ModelApp = 'claude' | 'codex';
|
|
3
|
+
export interface Choice {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `label` is the menu text, `short` the pill text and `description` what the model is for. A model may take fewer
|
|
9
|
+
* effort levels than its app (its own `efforts`, ids); without it, all of the app's.
|
|
10
|
+
*/
|
|
11
|
+
export interface ModelOption extends Choice {
|
|
12
|
+
short: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
efforts?: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `default` names what the agent starts with when no flag is sent, as concrete ids, or null where the agent decides at
|
|
18
|
+
* start: Claude Code resolves its default model and effort from the account's plan and organization, and Codex its
|
|
19
|
+
* default effort from the model catalog it fetches with the account's login. A Codex Cloud thread starts with
|
|
20
|
+
* gpt-6.1-sol (services/guest-agent/internal/codexcloud CloudModel), which is also Codex 0.160.0's first model.
|
|
21
|
+
*/
|
|
22
|
+
export interface AppModels {
|
|
23
|
+
models: ModelOption[];
|
|
24
|
+
efforts: Choice[];
|
|
25
|
+
default: {
|
|
26
|
+
model: string | null;
|
|
27
|
+
effort: string | null;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare const DEFAULT_CHOICE = "default";
|
|
31
|
+
/** Claude Code `--model <alias>` and `--effort <level>`; Codex `-m <model>` and `-c model_reasoning_effort=<level>`. */
|
|
32
|
+
export declare const MODEL_CHOICES: Readonly<Record<ModelApp, AppModels>>;
|
|
33
|
+
/** What a thread starts with: absent fields are the agent's own default (no flag). */
|
|
34
|
+
export interface ModelChoice {
|
|
35
|
+
model?: string;
|
|
36
|
+
effort?: string;
|
|
37
|
+
}
|
|
38
|
+
/** `--model` and `--effort` for this app, checked against its list. Throws a usage error for anything else. */
|
|
39
|
+
export declare function modelChoice(values: Values, app: ModelApp): ModelChoice;
|
|
40
|
+
export declare const hasChoice: (c: ModelChoice | undefined) => c is ModelChoice;
|
|
41
|
+
/** The guest's `sf-agent run` flags for a choice (sf-agent maps them to the agent's own flags). */
|
|
42
|
+
export declare function runFlags(c: ModelChoice | undefined): string[];
|
|
43
|
+
/** Whether a guest's `sf-agent run --help` lists the model and effort flags (coding templates from train 14). */
|
|
44
|
+
export declare const runAcceptsChoice: (help: string) => boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The choice a launch passes (this command's, else the conversation's recorded one, so a relaunch keeps its model) and
|
|
47
|
+
* whether the thread starts with this command's choice: the computer's guest accepts it and, for a conversation, its
|
|
48
|
+
* record carries it (the guest starts a conversation's agent from the record).
|
|
49
|
+
*/
|
|
50
|
+
export declare function launchChoice(explicit: ModelChoice, recorded: ModelChoice | undefined, conversation: boolean, help: () => Promise<string>): Promise<{
|
|
51
|
+
choice?: ModelChoice;
|
|
52
|
+
applied: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
/** Said once when a computer's platform predates the choice: the thread starts with the agent's defaults. */
|
|
55
|
+
export declare const CHOICE_PENDING = "The model and effort choice applies to new threads after the next platform update; this thread uses the defaults.";
|
|
56
|
+
/** The record the JSON output carries for a requested choice. */
|
|
57
|
+
export declare function choiceReport(c: ModelChoice, applied: boolean): {
|
|
58
|
+
model: string;
|
|
59
|
+
effort: string;
|
|
60
|
+
applied: boolean;
|
|
61
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { UsageError } from "./errors.js";
|
|
2
|
+
export const DEFAULT_CHOICE = 'default';
|
|
3
|
+
const DEFAULT_MODEL = { id: DEFAULT_CHOICE, label: 'Default', short: 'Default' };
|
|
4
|
+
const LEVELS = { low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', max: 'Max', ultra: 'Ultra' };
|
|
5
|
+
const levels = (...ids) => [{ id: DEFAULT_CHOICE, label: 'Default' }, ...ids.map(id => ({ id, label: LEVELS[id] }))];
|
|
6
|
+
/** Claude Code `--model <alias>` and `--effort <level>`; Codex `-m <model>` and `-c model_reasoning_effort=<level>`. */
|
|
7
|
+
export const MODEL_CHOICES = {
|
|
8
|
+
claude: {
|
|
9
|
+
// The aliases always name the latest model of each family; descriptions are Claude Code's own.
|
|
10
|
+
models: [DEFAULT_MODEL,
|
|
11
|
+
{ id: 'fable', label: 'Fable', short: 'Fable', description: 'For your toughest challenges' },
|
|
12
|
+
{ id: 'opus', label: 'Opus', short: 'Opus', description: 'Most capable for ambitious work' },
|
|
13
|
+
{ id: 'sonnet', label: 'Sonnet', short: 'Sonnet', description: 'Most efficient for everyday tasks' },
|
|
14
|
+
{ id: 'haiku', label: 'Haiku', short: 'Haiku', description: 'Fastest for quick answers' }],
|
|
15
|
+
efforts: levels('low', 'medium', 'high', 'xhigh', 'max'),
|
|
16
|
+
default: { model: null, effort: null },
|
|
17
|
+
},
|
|
18
|
+
codex: {
|
|
19
|
+
// Descriptions are Codex's own model catalog.
|
|
20
|
+
models: [DEFAULT_MODEL,
|
|
21
|
+
{ id: 'gpt-6.1-sol', label: 'GPT-6.1 Sol', short: 'GPT-6.1 Sol', description: 'Latest workhorse model for coding and everyday work' },
|
|
22
|
+
{ id: 'gpt-6-astra', label: 'GPT-6 Astra', short: 'GPT-6 Astra', description: 'Frontier intelligence for the most demanding work' },
|
|
23
|
+
{ id: 'gpt-6-luna', label: 'GPT-6 Luna', short: 'GPT-6 Luna', description: 'Fast and affordable model for easier tasks', efforts: [DEFAULT_CHOICE, 'low', 'medium', 'high', 'xhigh', 'max'] }],
|
|
24
|
+
efforts: levels('low', 'medium', 'high', 'xhigh', 'max', 'ultra'),
|
|
25
|
+
default: { model: 'gpt-6.1-sol', effort: null },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const list = (ids) => ids.length > 1 ? `${ids.slice(0, -1).join(', ')} or ${ids.at(-1)}` : ids[0];
|
|
29
|
+
const label = (app) => app === 'claude' ? 'Claude' : 'Codex';
|
|
30
|
+
/** `--model` and `--effort` for this app, checked against its list. Throws a usage error for anything else. */
|
|
31
|
+
export function modelChoice(values, app) {
|
|
32
|
+
const raw = (name) => {
|
|
33
|
+
const value = values[name];
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (typeof value !== 'string' || !value.trim())
|
|
37
|
+
throw new UsageError(`--${name} needs a value.`);
|
|
38
|
+
return value.trim().toLowerCase();
|
|
39
|
+
};
|
|
40
|
+
const model = raw('model'), effort = raw('effort'), choices = MODEL_CHOICES[app];
|
|
41
|
+
const option = model === undefined ? undefined : choices.models.find(m => m.id === model);
|
|
42
|
+
if (model !== undefined && !option)
|
|
43
|
+
throw new UsageError(`${label(app)} models: ${list(choices.models.map(m => m.id))}.`);
|
|
44
|
+
const allowed = option?.efforts ?? choices.efforts.map(e => e.id);
|
|
45
|
+
if (effort !== undefined && !allowed.includes(effort)) {
|
|
46
|
+
throw new UsageError(`${option?.efforts ? option.label : label(app)} effort levels: ${list(allowed)}.`);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
...(model && model !== DEFAULT_CHOICE ? { model } : {}),
|
|
50
|
+
...(effort && effort !== DEFAULT_CHOICE ? { effort } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export const hasChoice = (c) => !!(c?.model || c?.effort);
|
|
54
|
+
/** The guest's `sf-agent run` flags for a choice (sf-agent maps them to the agent's own flags). */
|
|
55
|
+
export function runFlags(c) {
|
|
56
|
+
return [...(c?.model ? ['--model', c.model] : []), ...(c?.effort ? ['--effort', c.effort] : [])];
|
|
57
|
+
}
|
|
58
|
+
/** Whether a guest's `sf-agent run --help` lists the model and effort flags (coding templates from train 14). */
|
|
59
|
+
export const runAcceptsChoice = (help) => /-model\b/.test(help) && /-effort\b/.test(help);
|
|
60
|
+
/**
|
|
61
|
+
* The choice a launch passes (this command's, else the conversation's recorded one, so a relaunch keeps its model) and
|
|
62
|
+
* whether the thread starts with this command's choice: the computer's guest accepts it and, for a conversation, its
|
|
63
|
+
* record carries it (the guest starts a conversation's agent from the record).
|
|
64
|
+
*/
|
|
65
|
+
export async function launchChoice(explicit, recorded, conversation, help) {
|
|
66
|
+
const choice = hasChoice(explicit) ? explicit : recorded;
|
|
67
|
+
if (!hasChoice(choice))
|
|
68
|
+
return { applied: true };
|
|
69
|
+
// A guest that cannot say what it accepts gets no flags: the thread starts either way.
|
|
70
|
+
const accepted = runAcceptsChoice(await help().catch(() => ''));
|
|
71
|
+
const kept = !conversation || !hasChoice(explicit) || (recorded?.model === explicit.model && recorded?.effort === explicit.effort);
|
|
72
|
+
return { ...(accepted ? { choice } : {}), applied: accepted && kept };
|
|
73
|
+
}
|
|
74
|
+
/** Said once when a computer's platform predates the choice: the thread starts with the agent's defaults. */
|
|
75
|
+
export const CHOICE_PENDING = 'The model and effort choice applies to new threads after the next platform update; this thread uses the defaults.';
|
|
76
|
+
/** The record the JSON output carries for a requested choice. */
|
|
77
|
+
export function choiceReport(c, applied) {
|
|
78
|
+
return { model: c.model ?? DEFAULT_CHOICE, effort: c.effort ?? DEFAULT_CHOICE, applied };
|
|
79
|
+
}
|
package/dist/agent-state.d.ts
CHANGED
|
@@ -12,6 +12,16 @@ export declare class AgentStateStore {
|
|
|
12
12
|
read(): State;
|
|
13
13
|
app(project: string, fallback?: 'claude' | 'codex'): 'claude' | 'codex';
|
|
14
14
|
target(project: string, available: string[]): string;
|
|
15
|
+
/** T3 Code environments set up from this computer (the SSH route): targets.json keeps T3 out of its manifest. */
|
|
16
|
+
t3Environments(apiUrl: string): Array<{
|
|
17
|
+
workspace: string;
|
|
18
|
+
host: string;
|
|
19
|
+
}>;
|
|
20
|
+
saveT3Environment(entry: {
|
|
21
|
+
workspace: string;
|
|
22
|
+
host: string;
|
|
23
|
+
api_url: string;
|
|
24
|
+
}): void;
|
|
15
25
|
saveTarget(project: string, target: string): void;
|
|
16
26
|
update(change: (state: State) => void): void;
|
|
17
27
|
save(project: string, app: 'claude' | 'codex'): void;
|
package/dist/agent-state.js
CHANGED
|
@@ -28,6 +28,19 @@ export class AgentStateStore {
|
|
|
28
28
|
const target = this.read().projects?.[project]?.target;
|
|
29
29
|
return target && available.includes(target) ? target : available.includes('t3') ? 't3' : 'terminal';
|
|
30
30
|
}
|
|
31
|
+
/** T3 Code environments set up from this computer (the SSH route): targets.json keeps T3 out of its manifest. */
|
|
32
|
+
t3Environments(apiUrl) {
|
|
33
|
+
const list = this.read().t3;
|
|
34
|
+
return (Array.isArray(list) ? list : []).filter((e) => !!e && typeof e === 'object' && typeof e.workspace === 'string' && typeof e.host === 'string'
|
|
35
|
+
&& e.api_url === apiUrl).map(({ workspace, host }) => ({ workspace, host }));
|
|
36
|
+
}
|
|
37
|
+
saveT3Environment(entry) {
|
|
38
|
+
this.update(doc => {
|
|
39
|
+
const list = (Array.isArray(doc.t3) ? doc.t3 : []);
|
|
40
|
+
// The newest last: the onboarding card names the one most recently set up.
|
|
41
|
+
doc.t3 = [...list.filter(e => !(e.host === entry.host && e.api_url === entry.api_url)), entry];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
31
44
|
saveTarget(project, target) {
|
|
32
45
|
this.update(doc => {
|
|
33
46
|
doc.projects ??= {};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Duplex } from 'node:stream';
|
|
2
2
|
import type { CellClient, Workspace } from '@shardflux/sdk';
|
|
3
|
+
import type { Conversation } from './conversations.js';
|
|
3
4
|
import type { Ctx, Handler, Io } from './commands.js';
|
|
4
5
|
import { AgentTargets } from './agent-targets.js';
|
|
5
6
|
import type { DesktopTarget } from './agent-targets.js';
|
|
7
|
+
import type { T3Install, T3ThreadRequest } from './t3-mode.js';
|
|
6
8
|
/** Optimistic file replacement preserves keys installed by other devices connecting at the same time. */
|
|
7
9
|
export declare function installDeviceKey(cell: CellClient, key: string, signal?: AbortSignal): Promise<void>;
|
|
8
10
|
/** Host trust is established over the authenticated files API, before sending any SSH protocol bytes. */
|
|
@@ -13,7 +15,11 @@ export declare function proxyStream(io: Io, ended?: () => void, outputGate?: Pro
|
|
|
13
15
|
* this computer just started can accept its first prompt before Remote Control registers, so an open of our recent
|
|
14
16
|
* launch probes the guest for up to 30 s; history browsing stays passive.
|
|
15
17
|
*/
|
|
16
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The Claude Remote Control URL of a session in `ws`. With several threads on one computer (0.16.0+) a conversation's
|
|
20
|
+
* own session is chosen by its harness session id; a thread that has not reported one yet is unambiguous only alone.
|
|
21
|
+
*/
|
|
22
|
+
export declare function remoteControlUrl(ctx: Ctx, ws: Workspace, conversation?: Pick<Conversation, 'harness_session_id'>): Promise<string>;
|
|
17
23
|
/** Claude Desktop's own link to a Remote Control session: claude://code/<session id> opens it in its Code tab. */
|
|
18
24
|
export declare function claudeDesktopLink(remoteControl: string): string;
|
|
19
25
|
export declare const CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
|
|
@@ -21,7 +27,11 @@ export declare const CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
|
|
|
21
27
|
* Opens the conversation itself (its live Remote Control session) in Claude Desktop (0.15.1+), the way `--in auto`
|
|
22
28
|
* opens a Claude conversation when Claude Desktop is installed. --json prints {target, url, app_bundle_id} for apps.
|
|
23
29
|
*/
|
|
24
|
-
export declare function openClaudeDesktopSession(ctx: Ctx, ws: Workspace): Promise<number>;
|
|
25
|
-
|
|
30
|
+
export declare function openClaudeDesktopSession(ctx: Ctx, ws: Workspace, conversation?: Pick<Conversation, 'harness_session_id'>): Promise<number>;
|
|
31
|
+
/**
|
|
32
|
+
* Opens a workspace as a desktop app's SSH environment. For T3 (0.17.0+) a first message becomes a thread on the T3
|
|
33
|
+
* server in the VM (SSH mode), and T3 is brought to the front with --json too (`opened: true`).
|
|
34
|
+
*/
|
|
35
|
+
export declare function openDesktopWorkspace(ctx: Ctx, ws: Workspace, target: DesktopTarget, t3Version?: string, prewarmed?: boolean, thread?: T3ThreadRequest, install?: T3Install, conversation?: string | null): Promise<number>;
|
|
26
36
|
export declare function queueT3Prewarm(ctx: Ctx, ws: Workspace, version: string): Promise<void>;
|
|
27
37
|
export declare const TARGET_HANDLERS: Record<string, Handler>;
|