@shardflux/cli 0.16.0 → 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 +62 -2
- package/README.md +43 -5
- package/dist/agent-commands.d.ts +9 -1
- package/dist/agent-commands.js +65 -16
- 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 +6 -1
- package/dist/agent-target-commands.js +117 -16
- package/dist/agent-targets.d.ts +2 -0
- package/dist/agent-targets.js +10 -4
- package/dist/args.d.ts +5 -1
- package/dist/args.js +49 -9
- 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 +2 -1
- package/dist/codex-cloud.js +16 -6
- package/dist/command-center-actions.js +16 -1
- 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 +45 -6
- package/dist/conversations.d.ts +18 -2
- package/dist/conversations.js +12 -5
- 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 +5 -2
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +1 -1
- package/dist/handlers.js +3 -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 +29 -6
- package/dist/onboarding.d.ts +2 -0
- package/dist/onboarding.js +53 -5
- 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 +2 -2
- package/dist/thread-on-computer.js +13 -7
- package/dist/update.d.ts +1 -1
- package/dist/update.js +12 -1
- package/dist/worktree-twin-engine.d.ts +31 -1
- package/dist/worktree-twin-engine.js +253 -40
- package/dist/worktree-twin.d.ts +17 -0
- package/dist/worktree-twin.js +147 -22
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.0 (not yet published)
|
|
4
|
+
|
|
5
|
+
- Linux (Ubuntu 24.04, x64 and arm64): bare `shard` signs in and connects Claude/Codex in the terminal. The
|
|
6
|
+
systemd user helper keeps the live twin, files and conversation mirrors running; `shard device requests`,
|
|
7
|
+
`shard device approve <id>` and `shard device deny <id>` handle local approvals. Desktop launches open a terminal;
|
|
8
|
+
macOS keeps its extra menu bar app.
|
|
9
|
+
|
|
10
|
+
- A Codex project opens in T3 Code without a Claude sign-in (T3 shows Claude as signed out, and says how to sign in), and `--json` names the T3 Connect sign-in `shard` started for the phone (`t3_link.sign_in`, or `t3_link.needs_login` when this computer has no session).
|
|
11
|
+
- A conversation settles when its turn ends (`done` or `unresolved` right away) and its VM takes the next message.
|
|
12
|
+
`shard agents open`, `shard ./ --resume` and ACP reattach to that VM instead of resuming the conversation into a new
|
|
13
|
+
one, `shard agents computers` and `--on` offer its computer for new threads, Try locally keeps syncing across turns,
|
|
14
|
+
and the transcript mirror reads it once per change. A conversation has ended once its VM is removed, or it is
|
|
15
|
+
archived or discarded.
|
|
16
|
+
- The menu bar keeps running and updating your global install: running `shard` from a source checkout, a project's
|
|
17
|
+
`node_modules` or `npx` no longer replaces the install it records (`SHARDFLUX_RECORD_INSTALL=1` records any run).
|
|
18
|
+
- `--model <model>` and `--effort <level>` on `shard ./` and `shard agents new` (also with `--on` and `--in t3`)
|
|
19
|
+
(0.17.0+): a new Claude or Codex thread starts with that model and reasoning effort, such as `--model opus --effort
|
|
20
|
+
xhigh` or `--model gpt-6-astra --effort high`. The conversation keeps them across resumes and moves. `--json` adds
|
|
21
|
+
`model_choice: {model, effort, applied}`; a computer whose platform predates the choice starts the thread with the
|
|
22
|
+
agent's defaults and says so once. Unknown values are refused before anything starts.
|
|
23
|
+
- `shard agents targets --json` adds `models` (0.17.0+): for `claude` and `codex`, the `models` (`id`, `label`, `short`,
|
|
24
|
+
`description`), the `efforts` (`id`, `label`) and the agent's own `default` (`model`, `effort`; `null` where the
|
|
25
|
+
agent decides at start), so the menu bar and other apps show the same choices without hardcoding them.
|
|
26
|
+
- T3 Code mode: `shard agents new --in t3 --prompt-file <file>` (or `--prompt`) starts the first message as a thread in
|
|
27
|
+
T3 Code and brings T3 to the front. With T3's Shardflux provider (T3 builds with ACP providers) the thread is launched
|
|
28
|
+
on the running T3 and gets its own cloud VM; with T3 Code 0.0.45 it starts on the T3 server in the project VM, or with
|
|
29
|
+
`--on <conversation|computer>` on that computer's. `--image` attaches images to the first message; `--model` and
|
|
30
|
+
`--effort` take the same choices as every new thread. `--json` returns `{ target: "t3", mode, project, t3_thread_id,
|
|
31
|
+
conversation_id, workspace, opened: true, focused: false, model, effort }` with the model and effort T3 got. A
|
|
32
|
+
requested effort that T3 did not get is `effort: null` with `effort_reason`: `not_offered` (the model has no such
|
|
33
|
+
level) or `models_not_known` (with the Shardflux provider, before this project's first conversation on this computer
|
|
34
|
+
has shown its levels). Refusals have their own codes: `t3_not_installed`, `t3_not_connected`, `t3_server_unreachable`,
|
|
35
|
+
`t3_version_unsupported`, `t3_on_not_supported`, `t3_model_unavailable`, `t3_thread_failed`, and `t3_open_failed` for
|
|
36
|
+
a thread that started while T3 Code did not open; once the workspace, conversation or thread exists their `details`
|
|
37
|
+
name its `workspace_key`, `conversation_id` and `t3_thread_id`, so a client opens it instead of starting it again.
|
|
38
|
+
- `shard agents targets --json` reports T3 Code (`t3: { installed, mode, connected, version, last_used_at }`) and where
|
|
39
|
+
each app's new conversations open (`open_in: { claude, codex }`). `--open-in claude=t3,codex=t3` saves the choice
|
|
40
|
+
(Claude: terminal, claude-desktop, web or t3; Codex: codex-app or t3). Without one, T3 is the default when it is
|
|
41
|
+
connected (the Shardflux provider registered and this computer signed in; with T3 Code 0.0.45, this project set up
|
|
42
|
+
as a T3 environment from this computer) and the most recently used of the installed apps. `shard ./` and `--in auto`
|
|
43
|
+
open new conversations there, a conversation as itself. `--path <folder>` reports them for that project (as
|
|
44
|
+
`agents new --path` names it); an agent argument wins over it.
|
|
45
|
+
- `shard agents open t3 <conversation>` brings the conversation's T3 thread back (`t3_thread_id`) and, like
|
|
46
|
+
`agents open t3 <workspace>`, opens T3 with `--json` too (`opened: true`).
|
|
47
|
+
- JSON errors that come with a next step carry it as `error.fix`.
|
|
48
|
+
- T3 Code on your phone: `shard agents new --in t3` and `shard agents open t3` also add the project to the T3 app as **Shardflux · <project>**. The first time, open the link it prints and confirm the code once; every later project appears by itself. A message from the app wakes the VM.
|
|
49
|
+
- `shard cli-proxy import` moves CLIProxyAPI from this computer into your account: one hosted copy holds your Claude, Codex and other subscription logins, sleeps between requests and wakes on the next one. This computer and every new VM reach it at `http://127.0.0.1:8317`, so Claude Code (`ANTHROPIC_BASE_URL`), Codex (`base_url …/v1`) and T3 Code's usage view keep their settings; streaming responses and WebSockets pass through.
|
|
50
|
+
- The hosted copy is made from your own config: client API keys, routing, retries, model aliases and upstream keys carry over, and your management key keeps working.
|
|
51
|
+
- Your local CLIProxyAPI stops for good on the port its config names: a Homebrew service with `brew services stop`; after you confirm (`--yes` for scripts), a launchd agent (stopped, disabled, its plist set aside), a Docker container (`docker stop`) or a process. Only a program recognized as CLIProxyAPI is ever stopped; anything else on the port is named with the command to run.
|
|
52
|
+
- Your auth files move to the hub, and the local copies move to `<auth folder>.moved-to-shardflux` (0700), where no CLIProxyAPI reads them.
|
|
53
|
+
- Run it on a second computer to connect that one too: it adds only the accounts that are new to the hub, names them, and keeps the hub's settings. Every project of your account uses the same hosted copy. `--config <path>` names the config when it is somewhere else.
|
|
54
|
+
- VMs receive only the address of the hub's VM port, which serves the model routes (OpenAI, Claude, Gemini and Codex, WebSockets included) and nothing else; the hub's logins and settings are managed from the computers that set it up. A second computer's logins are matched to the hub's by account (provider plus every account, organization, workspace and project id and email the auth file names), whatever the file names, so two accounts of one email both move.
|
|
55
|
+
- `shard cli-proxy export` brings your logins back to this computer: the hosted copy stops first, the logins it last refreshed are written to your auth folder (0600), this computer stops forwarding, and the command that starts your local CLIProxyAPI is shown. `shard cli-proxy import` moves them back.
|
|
56
|
+
- `shard cli-proxy` shows the hosted copy: its accounts (provider and email), whether it is running or sleeping, and where it is reachable, in a VM too. It starts CLIProxyAPI again when the hub answers that nothing listens. `--json` for scripts.
|
|
57
|
+
- Revoking an API key keeps the hub reachable: when its address was created with that key, the next request through this computer, `shard cli-proxy` or an import gives the hub a new address, uses it at once and passes it to every new VM.
|
|
58
|
+
- On macOS the forwarder runs as the launchd agent `dev.shardflux.cli-proxy-forward` (at login, kept running, restarted when `shard` is updated). A request the hub did not receive (while it wakes or CLIProxyAPI starts again) is sent again, POST bodies included up to 32 MiB; a client that disconnects ends its request to the hub.
|
|
59
|
+
- For onboarding: `detectLocalCliProxy()` and `importCliProxy(ctx)` are exported.
|
|
60
|
+
|
|
61
|
+
- A finished conversation lands in your checkout as soon as its turn ends: the menu bar's helper keeps one wait per
|
|
62
|
+
active conversation and hears the finished turn the moment the conversation reports it (no polling every second).
|
|
63
|
+
Conversations that finish together for the same branch land in one go, in the order they finished.
|
|
64
|
+
|
|
3
65
|
## 0.16.0 (not yet published)
|
|
4
66
|
|
|
5
67
|
- Cloud sessions are named for the work: `shard ./` starts the agent with the repository or folder name and the first
|
|
@@ -308,7 +370,6 @@
|
|
|
308
370
|
|
|
309
371
|
## 0.12.0 (not yet published)
|
|
310
372
|
|
|
311
|
-
|
|
312
373
|
- Add `cpu-service-fresh-inputs-v1` with `fresh_env_names`: fresh owned startup hooks/children consume allowed task values while immutable CPU initialization stays credential-free. Relevant or secret inputs retain ordinary startup.
|
|
313
374
|
|
|
314
375
|
- Add explicit `pasteText` clipboard replacement with an application paste chord, LF-to-Return typing repair and unchanged screenshot settling.
|
|
@@ -316,7 +377,6 @@
|
|
|
316
377
|
Every command and option the README shows is available from the version named here. Breaking changes ship in minor
|
|
317
378
|
releases and are marked **Breaking**.
|
|
318
379
|
|
|
319
|
-
|
|
320
380
|
- Accept caller-supplied exact desktop pixel predicates without an environment gate. Completion uses the same frozen full frame encoded as PNG; pre-satisfied refusal and timeout return no success image. Omitted predicates retain 250 ms settling.
|
|
321
381
|
- Preserve explicit `initialized_cpu` service declarations through template YAML build/export/edit transport, matching SDK0.17.0.
|
|
322
382
|
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ See the [support guide](https://github.com/shardfluxdev/community/blob/main/SUPP
|
|
|
26
26
|
|
|
27
27
|
## Command center (0.15.0+)
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
On macOS, run `shard` in a terminal to see live conversations. Press **Enter** to open the selected one, **/** to search,
|
|
30
30
|
**D** to expand or collapse finished conversations, and **?** for targets, logs, fork, park and Git actions.
|
|
31
31
|
Press **n** to start a conversation: describe the task, confirm a local project folder, then choose Claude or Codex.
|
|
32
32
|
Both the task and project are required, and the task becomes the first user-authored message. **Esc** cancels;
|
|
@@ -50,6 +50,18 @@ Bulk transfers batch writes automatically. Active transfers retain their byte po
|
|
|
50
50
|
|
|
51
51
|
## Install
|
|
52
52
|
|
|
53
|
+
Linux (Ubuntu 24.04, x64 and arm64) and macOS are supported. On Linux, run `shard` to sign in and
|
|
54
|
+
connect Claude, Codex or both (0.17.0+), then run `shard ./` in your project to attach Claude Code or Codex in place. Sign-in opens your browser;
|
|
55
|
+
on a headless terminal, follow the displayed URL and device code. `--accept-terms` accepts the terms notice.
|
|
56
|
+
|
|
57
|
+
The Linux helper starts automatically after setup as `~/.config/systemd/user/shardflux-helper.service`
|
|
58
|
+
and restarts after a CLI update. It keeps the live twin, `~/Shardflux` files and conversation history in sync.
|
|
59
|
+
Use `shard device requests`, `shard device approve <id>` or `shard device deny <id>` for local approvals;
|
|
60
|
+
desktop notifications point to pending requests. On systems without a systemd user session, the helper runs
|
|
61
|
+
while your `shard` commands are open. `shard agents open` opens a terminal when launched outside one, using
|
|
62
|
+
`$TERMINAL`, `x-terminal-emulator` or `gnome-terminal`, or prints the attach command. The Mac app adds a menu
|
|
63
|
+
bar, launcher and Settings window.
|
|
64
|
+
|
|
53
65
|
```sh
|
|
54
66
|
npm i -g shardflux
|
|
55
67
|
shard --help
|
|
@@ -70,7 +82,7 @@ shard ./
|
|
|
70
82
|
|
|
71
83
|
Choose Claude Code or Codex. Claude asks for one cloud sign-in per Shardflux account; Codex uses your copied login.
|
|
72
84
|
Each `shard ./` starts its own conversation VM and opens your agent’s native terminal. Later turns stay in that
|
|
73
|
-
VM; `shard ./ --resume` selects a saved project conversation. Native terminal and ACP sessions receive the conversation tools for their own conversation, so the agent can fork, ask for a review, and report to its parent (0.14.0+). You can open conversations in separate terminals at the same time; each stays in the project's local history. `shard` opens the optional command center. On macOS the menu bar starts automatically the first
|
|
85
|
+
VM; `shard ./ --resume` selects a saved project conversation. Native terminal and ACP sessions receive the conversation tools for their own conversation, so the agent can fork, ask for a review, and report to its parent (0.14.0+). You can open conversations in separate terminals at the same time; each stays in the project's local history. On macOS, `shard` opens the optional command center. On macOS the menu bar starts automatically the first
|
|
74
86
|
time `shard` runs, with launch at login enabled; `shard menubar stop` turns it off. See [Cloud agents](https://docs.shardflux.dev/guides/cloud-agents).
|
|
75
87
|
|
|
76
88
|
T3 Code uses one **Shardflux** provider; choose a Claude Code or Codex model to start that agent in the cloud.
|
|
@@ -94,6 +106,32 @@ shard ./ --on fix-login --review # a read-only Claude reviewer of it
|
|
|
94
106
|
the new thread. Codex threads are Codex Cloud threads, on the web and your phone too. `shard agents list` shows each
|
|
95
107
|
thread's conversation and computer.
|
|
96
108
|
|
|
109
|
+
Choose the model and reasoning effort a new thread starts with (0.17.0+):
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
shard ./ --model opus --effort xhigh # Claude Opus at extra-high effort
|
|
113
|
+
shard ./ codex --model gpt-6-astra --effort high
|
|
114
|
+
shard agents new --app claude --path ./ --model sonnet --prompt "Tidy the README" --json
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Claude takes `fable`, `opus`, `sonnet` or `haiku` and `low`, `medium`, `high`, `xhigh` or `max`; Codex takes
|
|
118
|
+
`gpt-6.1-sol`, `gpt-6-astra` or `gpt-6-luna` and the same levels plus `ultra`. `default`, or leaving the flag out,
|
|
119
|
+
keeps the agent's own choice. The conversation keeps its model and effort when it is resumed or moved, and `--json`
|
|
120
|
+
adds `model_choice: {model, effort, applied}`. `shard agents targets --json` lists the choices as `models`, with menu
|
|
121
|
+
and pill text and the agent's own default, so apps never hardcode them. `--in t3` passes them to T3 Code.
|
|
122
|
+
|
|
123
|
+
Start a thread in T3 Code from the command line (0.17.0+):
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
shard agents new --path . --app claude --in t3 --prompt-file task.md
|
|
127
|
+
shard agents targets --open-in claude=t3,codex=t3 # shard ./ starts new conversations in T3 Code
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The first message becomes a T3 thread and T3 comes to the front. With the Shardflux provider the thread runs in its own
|
|
131
|
+
cloud VM; with T3 Code 0.0.45 it runs on the T3 server in the project's VM, or with `--on` on that computer's.
|
|
132
|
+
`--image` attaches images. `shard agents targets --json` shows T3 Code's mode and where each app's new conversations
|
|
133
|
+
open; `shard agents open t3 <conversation>` brings its thread back.
|
|
134
|
+
|
|
97
135
|
Git fetch and push use the coding guest’s platform transport when available, preserving Git’s binary packets (0.14.0+). Older guests retain their existing transport.
|
|
98
136
|
|
|
99
137
|
To try a conversation on your Mac:
|
|
@@ -104,11 +142,11 @@ shard conversations try <conversation>
|
|
|
104
142
|
|
|
105
143
|
Your branch opens in `.shard/worktrees/<name>`, syncs both ways, and its dev servers appear at localhost.
|
|
106
144
|
Ctrl-C stops local sync. Your main checkout stays available for your own work.
|
|
107
|
-
Device tools ask for approval on your
|
|
145
|
+
Device tools ask for approval on your computer before preparing the conversation’s local worktree. With CLI 0.15.0 and menu bar 0.3.0, Review shows the exact command, file direction, paths or port mapping; access outside the conversation folder only offers Allow once. Live transcripts stay in
|
|
108
146
|
`~/.config/shardflux/conversations/`; ended sessions enter Claude Code’s or Codex’s native history. Transcript files up to 64 MiB are supported (0.14.0+).
|
|
109
147
|
From 0.14.3, transcript copying runs independently of device approvals. Unknown entries are skipped, and failed copies retry with backoff; diagnostics name the conversation and affected entry.
|
|
110
148
|
|
|
111
|
-
Every conversation's committed files are on your
|
|
149
|
+
Every conversation's committed files are on your computer too (with the helper running; macOS 0.16.0+, Linux 0.17.0+):
|
|
112
150
|
`~/Shardflux/<repository>/<base branch>/<conversation>/` holds what each conversation changed or added, read-only,
|
|
113
151
|
and updates when its branch moves. Large files are listed as `<file>.placeholder.txt`; download one with the menu
|
|
114
152
|
bar's **Download** or:
|
|
@@ -1252,7 +1290,7 @@ connection it configured and the file it is in (0.13.3+).
|
|
|
1252
1290
|
|
|
1253
1291
|
`shard agents fork` or `f` in the command center copies the whole running state into an independent workspace and
|
|
1254
1292
|
conversation. Open in your terminal, Claude Desktop or the Codex app, and the terminal Claude conversation in
|
|
1255
|
-
claude.ai or mobile.
|
|
1293
|
+
claude.ai or mobile. Sessions are titled with their task and repository, like `Add CSV export · todo-app`. Waiting agents stay reachable on their own connections;
|
|
1256
1294
|
see the [settings and billing reference](https://docs.shardflux.dev/limits#cloud-agent-reachability).
|
|
1257
1295
|
Claude needs one cloud sign-in per account; Codex uses your copied login.
|
|
1258
1296
|
|
package/dist/agent-commands.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CellClient, PtySession, Workspace } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx, Handler } from './commands.js';
|
|
3
3
|
import type { Values } from './args.js';
|
|
4
|
+
import type { T3Install, T3ThreadRequest } from './t3-mode.js';
|
|
5
|
+
import type { ModelChoice } from './agent-models.js';
|
|
4
6
|
/** `<slug>-<6 hex of sha256(identity)>`: the same repository gets the same key on every machine. */
|
|
5
7
|
export declare function disambiguatedSlug(slug: string, identity: string): string;
|
|
6
8
|
export declare function projectTemplate(ctx: Ctx, remote: string | null): Promise<string>;
|
|
@@ -9,7 +11,10 @@ export declare function projectTemplate(ctx: Ctx, remote: string | null): Promis
|
|
|
9
11
|
export declare function agentPty(cell: CellClient, app: 'claude' | 'codex', agent: string, approvals: boolean, term: string, signal?: AbortSignal, place?: string, setupHash?: string, cwd?: string, ptyName?: string, onLaunch?: () => void, deferSetup?: boolean, conversation?: {
|
|
10
12
|
id: string;
|
|
11
13
|
session: string | null;
|
|
12
|
-
}, task?: string
|
|
14
|
+
}, task?: string, launch?: {
|
|
15
|
+
choice?: ModelChoice;
|
|
16
|
+
help?: () => Promise<string>;
|
|
17
|
+
}): Promise<PtySession>;
|
|
13
18
|
/** The first non-empty line of a prompt or title, bounded; the guest makes it the task part of the session title. */
|
|
14
19
|
export declare function sessionTask(text: string | undefined): string | undefined;
|
|
15
20
|
export declare function terminal(ctx: Ctx, ws: Workspace, app: 'claude' | 'codex', agent: string, project: string, values: Values, setupHash?: string, cwd?: string, conversation?: {
|
|
@@ -17,6 +22,9 @@ export declare function terminal(ctx: Ctx, ws: Workspace, app: 'claude' | 'codex
|
|
|
17
22
|
session: string | null;
|
|
18
23
|
pty?: string;
|
|
19
24
|
title?: string;
|
|
25
|
+
choice?: ModelChoice;
|
|
20
26
|
}): Promise<number>;
|
|
21
27
|
export declare const launchProject: Handler;
|
|
28
|
+
/** The project VM launch; with `--in t3` a first message becomes a thread on the T3 server in it (SSH mode, 0.17.0+). */
|
|
29
|
+
export declare function launchProjectWith(ctx: Ctx, values: Values, [path, explicitApp]: string[], thread?: T3ThreadRequest, install?: T3Install): Promise<number>;
|
|
22
30
|
export declare const AGENT_HANDLERS: Record<string, Handler>;
|
package/dist/agent-commands.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { linuxTerminalSession, openLinuxTerminal } from "./linux-terminal.js";
|
|
1
2
|
import { repositoryName } from "./files-mirror.js";
|
|
2
3
|
import { prepareSetupCache } from "./setup-cache.js";
|
|
3
4
|
import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
|
|
@@ -23,10 +24,12 @@ import { progress } from "./progress.js";
|
|
|
23
24
|
import { rememberLaunch } from "./agent-live.js";
|
|
24
25
|
import { prepareAgentMachine } from "./agent-machine.js";
|
|
25
26
|
import { installedTargets } from "./agent-targets.js";
|
|
27
|
+
import { openInFor } from "./t3-mode.js";
|
|
26
28
|
import { openClaudeDesktopSession, openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
27
29
|
import { submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
28
30
|
import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
29
31
|
import { conversationTerminal } from "./conversation-terminal.js";
|
|
32
|
+
import { CHOICE_PENDING, choiceReport, hasChoice, launchChoice, modelChoice, runFlags } from "./agent-models.js";
|
|
30
33
|
const active = (session) => session.state === 'running' || session.state === 'starting';
|
|
31
34
|
/** `<slug>-<6 hex of sha256(identity)>`: the same repository gets the same key on every machine. */
|
|
32
35
|
export function disambiguatedSlug(slug, identity) {
|
|
@@ -50,7 +53,7 @@ export async function projectTemplate(ctx, remote) {
|
|
|
50
53
|
}
|
|
51
54
|
/** `place` is the repository or folder name session titles end with ("todo-app"), `task` what a new session is titled
|
|
52
55
|
* with before the guest names it from its first prompt (a first prompt line or the conversation's title). */
|
|
53
|
-
export async function agentPty(cell, app, agent, approvals, term, signal, place, setupHash, cwd, ptyName, onLaunch, deferSetup = false, conversation, task) {
|
|
56
|
+
export async function agentPty(cell, app, agent, approvals, term, signal, place, setupHash, cwd, ptyName, onLaunch, deferSetup = false, conversation, task, launch) {
|
|
54
57
|
const id = `shard-${ptyName || agent}-${app}`;
|
|
55
58
|
// Arrival and changed locks queue the same durable guest job even when the
|
|
56
59
|
// native agent is already alive. Setup never becomes its first user turn.
|
|
@@ -71,18 +74,19 @@ export async function agentPty(cell, app, agent, approvals, term, signal, place,
|
|
|
71
74
|
onLaunch?.();
|
|
72
75
|
let conversationArgs = [];
|
|
73
76
|
if (conversation) {
|
|
74
|
-
const help = await
|
|
75
|
-
if (
|
|
77
|
+
const help = await (launch?.help ?? runHelp(cell, app, signal))();
|
|
78
|
+
if (help.includes('-conversation'))
|
|
76
79
|
conversationArgs = ['--conversation', conversation.id, ...(conversation.session ? ['--resume-session', conversation.session] : [])];
|
|
77
80
|
}
|
|
78
|
-
|
|
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)], {
|
|
79
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 } : {}),
|
|
80
84
|
});
|
|
81
|
-
if (
|
|
82
|
-
throw new Error(`agent launch failed: ${
|
|
85
|
+
if (started.exitCode !== 0)
|
|
86
|
+
throw new Error(`agent launch failed: ${started.stderr.trim() || `exit ${started.exitCode}`}`);
|
|
83
87
|
let result;
|
|
84
88
|
try {
|
|
85
|
-
result = JSON.parse(
|
|
89
|
+
result = JSON.parse(started.stdout);
|
|
86
90
|
}
|
|
87
91
|
catch {
|
|
88
92
|
throw new Error('agent launcher returned invalid JSON');
|
|
@@ -94,6 +98,11 @@ export async function agentPty(cell, app, agent, approvals, term, signal, place,
|
|
|
94
98
|
throw new Error(`agent exited before attach: ${session.state}`);
|
|
95
99
|
return session;
|
|
96
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
|
+
}
|
|
97
106
|
/** The first non-empty line of a prompt or title, bounded; the guest makes it the task part of the session title. */
|
|
98
107
|
export function sessionTask(text) {
|
|
99
108
|
const line = text?.split(/\r?\n/).map(l => l.replace(/[\s\p{Cc}]+/gu, ' ').trim()).find(Boolean);
|
|
@@ -113,9 +122,15 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
113
122
|
await prepareInitialCodexProject(cell, cwd, `shard-${ptyName || agent}-${app}`);
|
|
114
123
|
}
|
|
115
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`);
|
|
116
131
|
// A cold launch is remembered before it starts, so the listing can show it before the projection does (agent-live.ts).
|
|
117
132
|
const term = ctx.io.env.TERM && ctx.io.env.TERM !== 'dumb' ? ctx.io.env.TERM : 'xterm-256color';
|
|
118
|
-
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));
|
|
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 });
|
|
119
134
|
// A new conversation's guest can start the agent before this command reaches it, so the launch above never ran.
|
|
120
135
|
// Remember it either way: listing and `agents open` then read the guest until the projection lists the session.
|
|
121
136
|
rememberLaunch(ctx, ws.id);
|
|
@@ -136,7 +151,9 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
136
151
|
if (!attach) {
|
|
137
152
|
await preparation;
|
|
138
153
|
// conversation (0.16.0+): which conversation this session is, also when its computer hosts several threads.
|
|
139
|
-
|
|
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) } : {}) }));
|
|
140
157
|
return 0;
|
|
141
158
|
}
|
|
142
159
|
ctx.io.stderr.write(`Shardflux · ${ws.key}\nDetach: Ctrl-] then d\n`);
|
|
@@ -177,7 +194,9 @@ async function syncCredentials(ctx) {
|
|
|
177
194
|
throw new Error('agent credentials sync failed');
|
|
178
195
|
}
|
|
179
196
|
}
|
|
180
|
-
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) {
|
|
181
200
|
if (explicitApp && explicitApp !== 'claude' && explicitApp !== 'codex')
|
|
182
201
|
throw new UsageError('agent app must be claude or codex');
|
|
183
202
|
let like = typeof values.like === 'string' ? await resolveWorkspace(ctx, values.like) : undefined;
|
|
@@ -189,8 +208,12 @@ export const launchProject = async (ctx, values, [path = './', explicitApp]) =>
|
|
|
189
208
|
if (target && (!['claude-desktop', 'codex-app', 't3'].includes(target) || !detected?.targets.includes(target)))
|
|
190
209
|
throw new UsageError(`${target} is not an installed desktop target`);
|
|
191
210
|
const app = explicitApp ?? store.app(projectKey, like?.labels.app === 'codex' ? 'codex' : 'claude');
|
|
192
|
-
|
|
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)))
|
|
193
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');
|
|
194
217
|
const agent = typeof values.name === 'string' ? values.name : 'main';
|
|
195
218
|
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(agent))
|
|
196
219
|
throw new UsageError('agent name must match ^[a-z0-9][a-z0-9._-]{0,63}$');
|
|
@@ -236,8 +259,13 @@ export const launchProject = async (ctx, values, [path = './', explicitApp]) =>
|
|
|
236
259
|
await queueT3Prewarm(ctx, ws, detected.versions.t3);
|
|
237
260
|
if (app === 'claude' && !claudeReady)
|
|
238
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).
|
|
239
263
|
if (target === 't3' && app === 'codex')
|
|
240
|
-
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
|
+
});
|
|
241
269
|
if (!(await savedMachine(ctx))?.template)
|
|
242
270
|
await syncAgentConfig(ctx, ws);
|
|
243
271
|
// Seed once; two-way sync is scoped to explicit Try locally worktrees.
|
|
@@ -251,15 +279,17 @@ export const launchProject = async (ctx, values, [path = './', explicitApp]) =>
|
|
|
251
279
|
throw new Error(`project setup could not be queued: ${setup.stderr.trim() || `exit ${setup.exitCode}`}`);
|
|
252
280
|
}
|
|
253
281
|
store.saveTarget(projectKey, target);
|
|
254
|
-
return openDesktopWorkspace(ctx, ws, target, detected?.versions.t3, true);
|
|
282
|
+
return openDesktopWorkspace(ctx, ws, target, detected?.versions.t3, true, thread, install);
|
|
255
283
|
}
|
|
256
284
|
await prepareAgentMachine(ctx, ws);
|
|
257
285
|
return await terminal(ctx, ws, app, agent, projectKey, values, prepared?.setupHash);
|
|
258
|
-
}
|
|
286
|
+
}
|
|
259
287
|
export const AGENT_HANDLERS = {
|
|
260
288
|
async 'agents open'(ctx, values, [agent = 'main']) {
|
|
261
289
|
if (values.app !== undefined && values.app !== 'claude' && values.app !== 'codex')
|
|
262
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);
|
|
263
293
|
let resolvedConversation;
|
|
264
294
|
if (/^conv_[a-z0-9]{26}$/.test(agent)) {
|
|
265
295
|
resolvedConversation = await getConversation(ctx, agent);
|
|
@@ -274,7 +304,27 @@ export const AGENT_HANDLERS = {
|
|
|
274
304
|
const store = new AgentStateStore(ctx.store.dir);
|
|
275
305
|
const app = values.app ?? resolvedConversation?.harness ?? ws.labels.shard_app ?? ws.labels.app ?? store.app(project);
|
|
276
306
|
const detected = installedTargets(ctx.io.env);
|
|
277
|
-
|
|
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 ??
|
|
278
328
|
(project.startsWith('file://') ? store.read().projects?.[new URL(project).pathname]?.target : undefined);
|
|
279
329
|
// Codex's default open implementation owns Cloud/desktop routing. PAS-XB replaces that path; do not pin
|
|
280
330
|
// the menu to the old per-VM SSH target. An explicit saved desktop target still takes precedence.
|
|
@@ -295,7 +345,6 @@ export const AGENT_HANDLERS = {
|
|
|
295
345
|
if (detected.clients.includes('claude-desktop')) {
|
|
296
346
|
return openClaudeDesktopSession(ctx, ws, resolvedConversation);
|
|
297
347
|
}
|
|
298
|
-
const opened = resolvedConversation?.id ?? ws.key;
|
|
299
348
|
if (ctx.json) {
|
|
300
349
|
// An app runs this in the person's terminal (the menu bar opens Terminal with it).
|
|
301
350
|
out(ctx, `shard agents open ${opened}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', opened] });
|
|
@@ -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 ??= {};
|
|
@@ -4,6 +4,7 @@ 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 type { T3Install, T3ThreadRequest } from './t3-mode.js';
|
|
7
8
|
/** Optimistic file replacement preserves keys installed by other devices connecting at the same time. */
|
|
8
9
|
export declare function installDeviceKey(cell: CellClient, key: string, signal?: AbortSignal): Promise<void>;
|
|
9
10
|
/** Host trust is established over the authenticated files API, before sending any SSH protocol bytes. */
|
|
@@ -27,6 +28,10 @@ export declare const CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
|
|
|
27
28
|
* opens a Claude conversation when Claude Desktop is installed. --json prints {target, url, app_bundle_id} for apps.
|
|
28
29
|
*/
|
|
29
30
|
export declare function openClaudeDesktopSession(ctx: Ctx, ws: Workspace, conversation?: Pick<Conversation, 'harness_session_id'>): Promise<number>;
|
|
30
|
-
|
|
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>;
|
|
31
36
|
export declare function queueT3Prewarm(ctx: Ctx, ws: Workspace, version: string): Promise<void>;
|
|
32
37
|
export declare const TARGET_HANDLERS: Record<string, Handler>;
|