@shardflux/cli 0.11.1 → 0.13.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 +60 -1
- package/README.md +83 -15
- package/dist/account-args.js +2 -0
- package/dist/account-commands.js +39 -0
- package/dist/agent-commands.d.ts +8 -0
- package/dist/agent-commands.js +223 -0
- package/dist/agent-config-files.d.ts +23 -0
- package/dist/agent-config-files.js +203 -0
- package/dist/agent-config-mcp.d.ts +36 -0
- package/dist/agent-config-mcp.js +225 -0
- package/dist/agent-config-sync.d.ts +58 -0
- package/dist/agent-config-sync.js +366 -0
- package/dist/agent-credentials.d.ts +14 -0
- package/dist/agent-credentials.js +93 -0
- package/dist/agent-git-commands.d.ts +10 -0
- package/dist/agent-git-commands.js +294 -0
- package/dist/agent-git.d.ts +47 -0
- package/dist/agent-git.js +284 -0
- package/dist/agent-reachability.d.ts +4 -0
- package/dist/agent-reachability.js +13 -0
- package/dist/agent-repository.d.ts +13 -0
- package/dist/agent-repository.js +182 -0
- package/dist/agent-state.d.ts +16 -0
- package/dist/agent-state.js +63 -0
- package/dist/agent-target-commands.d.ts +10 -0
- package/dist/agent-target-commands.js +171 -0
- package/dist/agent-targets.d.ts +33 -0
- package/dist/agent-targets.js +267 -0
- package/dist/agent-terminal.d.ts +35 -0
- package/dist/agent-terminal.js +209 -0
- package/dist/args.d.ts +8 -4
- package/dist/args.js +120 -10
- package/dist/assisted-setup.d.ts +24 -0
- package/dist/assisted-setup.js +77 -0
- package/dist/browser-commands.d.ts +3 -0
- package/dist/browser-commands.js +83 -0
- package/dist/browser-profile-copy.d.ts +2 -0
- package/dist/browser-profile-copy.js +2 -0
- package/dist/browser-readers.d.ts +13 -0
- package/dist/browser-readers.js +249 -0
- package/dist/browser-sites.d.ts +1 -0
- package/dist/browser-sites.js +13 -0
- package/dist/claude-cloud-login.d.ts +9 -0
- package/dist/claude-cloud-login.js +143 -0
- package/dist/command-center-actions.d.ts +6 -0
- package/dist/command-center-actions.js +132 -0
- package/dist/command-center-keys.d.ts +6 -0
- package/dist/command-center-keys.js +38 -0
- package/dist/command-center-model.d.ts +29 -0
- package/dist/command-center-model.js +90 -0
- package/dist/command-center-renderer.d.ts +25 -0
- package/dist/command-center-renderer.js +100 -0
- package/dist/command-center.d.ts +18 -0
- package/dist/command-center.js +320 -0
- package/dist/commands.d.ts +8 -2
- package/dist/commands.js +123 -8
- package/dist/credential-prompts.d.ts +22 -0
- package/dist/credential-prompts.js +193 -0
- package/dist/filesystem-commands.d.ts +2 -0
- package/dist/filesystem-commands.js +63 -0
- package/dist/filesystem-state.d.ts +3 -0
- package/dist/filesystem-state.js +12 -0
- package/dist/git-remote-bin.d.ts +2 -0
- package/dist/git-remote-bin.js +37 -0
- package/dist/git-remote.d.ts +14 -0
- package/dist/git-remote.js +136 -0
- package/dist/handlers.js +9 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/input.d.ts +5 -0
- package/dist/main.js +21 -5
- package/dist/menubar.d.ts +32 -0
- package/dist/menubar.js +180 -0
- package/dist/project-identity.d.ts +14 -0
- package/dist/project-identity.js +61 -0
- package/dist/project-sync-manifest.d.ts +27 -0
- package/dist/project-sync-manifest.js +225 -0
- package/dist/project-sync-transfer.d.ts +14 -0
- package/dist/project-sync-transfer.js +240 -0
- package/dist/project-sync.d.ts +38 -0
- package/dist/project-sync.js +243 -0
- package/dist/project-template-setup.d.ts +17 -0
- package/dist/project-template-setup.js +89 -0
- package/dist/vault.d.ts +16 -0
- package/dist/vault.js +153 -0
- package/package.json +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0 (not yet published)
|
|
4
|
+
|
|
5
|
+
- Start the native agent while supervised repository setup runs in the background; show per-step setup progress and one failed-step retry in the command center. Changed runtime, CI, workspace and dependency inputs queue the next template preparation.
|
|
6
|
+
- `shard agents fork` returns the independent agent session after a full-state fork; preserve the inherited PTY when reopening.
|
|
7
|
+
|
|
8
|
+
- `shard template` is now the project template command (status, save, rollback); platform template commands stay under `shard templates` (the `template` shorthand for them is removed). `shard agents config` is gone: config sync is automatic.
|
|
9
|
+
- Keep cloud agent desktop target names prefixed with `[SF] `.
|
|
10
|
+
- Private account machine and project filesystem status, save and rollback; managed agent login files remain excluded from captures.
|
|
11
|
+
|
|
12
|
+
- Keep waiting agents reachable on their outbound connections for a bounded window with `--reachable 30m`; default 30 minutes, `0` off. Packet wakes remain parked time.
|
|
13
|
+
|
|
14
|
+
- Renew opted-in credential relays without a laptop after expiry, ownership changes and guest restart; preserve issuer revocation and project scope.
|
|
15
|
+
|
|
16
|
+
- One cloud Claude sign-in through the workspace terminal; sync reads only Codex, and sign-in failures appear in the command center.
|
|
17
|
+
- Keep attached project sync reconnecting across repeated suspend/resume cycles. A healthy guest watch resets the consecutive-failure budget.
|
|
18
|
+
|
|
19
|
+
- Browser logins, dotfiles and Claude/Codex config are saved as part of the machine; configure them normally inside the VM and use the same machine/template save and rollback commands there.
|
|
20
|
+
|
|
21
|
+
- `shard agents credentials sync/status/forget` manages Claude cloud sign-in metadata and copied Codex files without printing contents.
|
|
22
|
+
- Import secrets from hsec, 1Password, Bitwarden and Keychain without exposing values; approve browser fills in attached terminals with per-site vault choices.
|
|
23
|
+
|
|
24
|
+
- Share a bounded Git bundle seed and `/home/user/<project-slug>` repository between project sync and extra-agent hidden refs; sync only the main agent, before its setup turn.
|
|
25
|
+
- Add attached two-way project sync with bounded hash manifests, coalesced guest file watches, detached catch-up and preserved conflict copies.
|
|
26
|
+
|
|
27
|
+
- Add managed SSH/Claude Desktop/Codex app targets, reversible target configuration, Remote Control URL opening, and a wake-on-connect `shard ssh-proxy` with API-pinned host keys.
|
|
28
|
+
- Fetch and push Git smart protocol streams over workspace exec with the packaged `git-remote-shard` helper. Extra agents use hidden refs; `agents worktree`, `merge`, `keep [--push]`, `discard` and `restore` manage local work and workspace cleanup. Extra agent workspaces receive seven-day idle retention; startup housekeeping archives saved commits for 30 days.
|
|
29
|
+
- Install and control the macOS menu bar app with `shard menubar start|stop|status --login on|off`; add its optional macOS package and a one-time command center hint.
|
|
30
|
+
- Shared volume quotas use one account total; opening a new volume without a size defaults to 1 GiB.
|
|
31
|
+
|
|
32
|
+
- Add the TTY cloud agent command center with attention sorting, scoped projects, live status, filtering, attach/return, new sessions and agents, fork, idle park, and PTY offset logs. Automation can use the same actions with JSON output.
|
|
33
|
+
- Sync personal Claude/Codex configuration on project launch and agent attach; add `shard agents config sync|status --json`, workspace manifests, consented workspace MCP secrets and attachment-owned localhost tunnels.
|
|
34
|
+
- Set up first-run projects and changed dependencies, select prepared versions, and show them with shard projects template.
|
|
35
|
+
|
|
36
|
+
- Attach agents directly to their deterministic guest PTY; launch with exec only when needed and retain only the remembered app choice locally.
|
|
37
|
+
|
|
38
|
+
- Add shard agents list with state/label filters, pagination and JSON output.
|
|
39
|
+
|
|
40
|
+
- Start or reattach cloud agents with `shard <path> [codex]`, and attach with `shard agents open <agent> --in terminal`. Terminal resize and reconnect preserve PTY output offsets; Ctrl-] then d detaches.
|
|
41
|
+
- Derive project keys and labels from normalized git remotes, remember the project app, and use project template bindings when available.
|
|
42
|
+
- Add `shard volumes open <name>`, `workspaces open --like` and repeatable `--volume name:/path[:ro]`.
|
|
43
|
+
- Add `shard status [--json]` with all workspaces, attachable volumes and agents when the API provides `/v1/agents`.
|
|
44
|
+
|
|
45
|
+
## 0.12.0 (not yet published)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
- 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.
|
|
49
|
+
|
|
50
|
+
- Add explicit `pasteText` clipboard replacement with an application paste chord, LF-to-Return typing repair and unchanged screenshot settling.
|
|
51
|
+
|
|
3
52
|
Every command and option the README shows is available from the version named here. Breaking changes ship in minor
|
|
4
53
|
releases and are marked **Breaking**.
|
|
5
54
|
|
|
6
|
-
|
|
55
|
+
|
|
56
|
+
- 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.
|
|
57
|
+
- Preserve explicit `initialized_cpu` service declarations through template YAML build/export/edit transport, matching SDK0.17.0.
|
|
58
|
+
|
|
59
|
+
- Follows SDK 0.17.0's new minor dependency range.
|
|
60
|
+
- Add `shard files read-many` for one explicitly suspended sealed generation, with exact base64 bytes and explicit incomplete-envelope errors; it never wakes the workspace.
|
|
61
|
+
- Git partial clone and indexing/checkout controls remain explicit Git arguments through `shard ws exec`. No new browser convenience command is added.
|
|
62
|
+
|
|
63
|
+
- Batch reverse-tunnel writes and retry after stalled acknowledgements, keeping active transfers moving without duplicate traffic.
|
|
64
|
+
|
|
65
|
+
## 0.11.1 — 2026-10-05
|
|
7
66
|
|
|
8
67
|
- Batch reverse-tunnel writes and retry after stalled acknowledgements, keeping active transfers moving without duplicate traffic.
|
|
9
68
|
|
package/README.md
CHANGED
|
@@ -53,25 +53,20 @@ Or run it without installing: `npx @shardflux/cli --help`. The unscoped
|
|
|
53
53
|
|
|
54
54
|
## Quick start
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
password. With the Codex CLI signed in with ChatGPT on this machine, your email is verified by it.
|
|
56
|
+
Start your coding agent on a local project (0.13.0+):
|
|
58
57
|
|
|
59
58
|
```sh
|
|
60
|
-
shard
|
|
61
|
-
|
|
62
|
-
shard
|
|
63
|
-
shard files read acme/demo /home/user/notes.txt
|
|
64
|
-
shard ws suspend acme/demo --wait
|
|
59
|
+
shard setup
|
|
60
|
+
cd your-project
|
|
61
|
+
shard ./
|
|
65
62
|
```
|
|
66
63
|
|
|
67
|
-
|
|
64
|
+
Choose Claude Code or Codex. Claude asks for one cloud sign-in per Shardflux account; Codex uses your copied login.
|
|
65
|
+
The agent is usable immediately while setup prepares the project in the background. `shard` opens the command
|
|
66
|
+
center for every project; `shard ./` returns to this project. See [Cloud agents](https://docs.shardflux.dev/guides/cloud-agents).
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
shard login # verify the key: organization, project, tools
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
`shard --help` lists every command, and every command has its own `--help`.
|
|
68
|
+
For application workspace commands, `shard signup` creates an account and saves a project key, or set
|
|
69
|
+
`SHARDFLUX_API_KEY` to a key from the console and run `shard login`. `shard --help` lists every command.
|
|
75
70
|
|
|
76
71
|
## Feedback: agents, use this while you work
|
|
77
72
|
|
|
@@ -1017,7 +1012,6 @@ shard operations wait <op> --durable # the same for a suspend or fork you a
|
|
|
1017
1012
|
|
|
1018
1013
|
Apache-2.0
|
|
1019
1014
|
|
|
1020
|
-
|
|
1021
1015
|
## Integration controls (0.7.0+)
|
|
1022
1016
|
|
|
1023
1017
|
```sh
|
|
@@ -1079,3 +1073,77 @@ exactly, and a workspace's own setting wins. With no policy, workspaces stay unt
|
|
|
1079
1073
|
## Workspace upgrade (0.11.0+)
|
|
1080
1074
|
|
|
1081
1075
|
`shard ws upgrade <key>` keeps files and installed package files through one cold start. Memory and processes end. Use `--at next-resume` to schedule it without stopping the VM; `--json` returns the operation and its applied/scheduled result.
|
|
1076
|
+
|
|
1077
|
+
From 0.12.0 this package follows SDK 0.17.0. For an explicit blobless clone, use `shard ws exec <key> -- git clone --filter=blob:none -- https://github.com/python/cpython.git /home/user/cpython`. Current checkout files are fetched now; historical blobs fetch from origin later. Plain and shallow clone defaults remain unchanged.
|
|
1078
|
+
|
|
1079
|
+
### Sealed-file reads (0.12.0+)
|
|
1080
|
+
|
|
1081
|
+
Additive: `shard files read-many <workspace> --path /file --path /other --generation <generation>` prints a sealed-generation JSON envelope with exact base64 bytes. Exit 1 means explicit missing/refused paths; exit 0 means every requested path succeeded. It never wakes or resumes the workspace.
|
|
1082
|
+
|
|
1083
|
+
Use sealed-file reads for complete files from one suspended generation. If the generation changes, request a fresh read. See [lifecycle](https://docs.shardflux.dev/concepts/lifecycle) for the read contract.
|
|
1084
|
+
|
|
1085
|
+
### Parallel Git checkout (0.12.0+)
|
|
1086
|
+
|
|
1087
|
+
Set `parallelism` (1–16) to choose Git indexing threads and checkout workers for this clone. The `git_clone` tool forwards it; CLI callers use `git -c pack.threads=4 -c checkout.workers=4 clone ...` through `shard ws exec`.
|
|
1088
|
+
|
|
1089
|
+
### Treeless Git clones (0.12.0+)
|
|
1090
|
+
|
|
1091
|
+
Set `filter: "tree:0"` (Python: `filter="tree:0"`) to fetch the requested checkout and commit graph now, with historical trees and blobs fetched from origin as needed. CLI callers use `git clone --filter=tree:0` through `shard ws exec`. See [Git](https://docs.shardflux.dev/guides/agent-tools) for clone options.
|
|
1092
|
+
|
|
1093
|
+
## Explicit pixel completion (0.12.0+)
|
|
1094
|
+
|
|
1095
|
+
Pass the caller's predicate JSON in PREDICATE_JSON:
|
|
1096
|
+
|
|
1097
|
+
```sh
|
|
1098
|
+
shard ws computer key acme/demo Return --screenshot result.png --pixel-predicate "$PREDICATE_JSON"
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
An unmatched condition exits 1 and writes no successful screenshot.
|
|
1102
|
+
|
|
1103
|
+
Explicit predicates are enabled when supplied; no environment switch is required. Omitted predicates retain ordinary 250 ms settling.
|
|
1104
|
+
|
|
1105
|
+
Require an appended lossless PNG and omit `settle_ms`. Digests cover row-major RGBA8 bytes including cursor pixels. The exact matched full frame is encoded and returned. A condition already satisfied before input refuses the batch unless `allow_pre_satisfied` explicitly allows that semantics. Timeout returns no success image; input may already have run, so inspect input results before retrying. Use a known, semantically unambiguous result. A pixel condition cannot certify unpainted backend state or infer completion from focus or quiet.
|
|
1106
|
+
|
|
1107
|
+
## Exact text entry (0.12.0+)
|
|
1108
|
+
|
|
1109
|
+
`pasteText` replaces the clipboard with UTF-8 text and sends the chosen paste chord. Use `shift+Insert` for xterm. Ordinary `type` sends line breaks as Return. Start with all keys released. Clipboard replacement remains in effect after cancellation; see [computer use](https://docs.shardflux.dev/guides/computer-use) for action results.
|
|
1110
|
+
|
|
1111
|
+
Use `shard ws computer paste-text <key> - --paste-chord shift+Insert --screenshot after.png` with text on standard input. Leading-dash positional text goes after `--`.
|
|
1112
|
+
|
|
1113
|
+
## Initialized CPU services (0.12.0+)
|
|
1114
|
+
|
|
1115
|
+
Template services accept `initialized_cpu` with `compatibility`, `reset`, and `timeout_seconds`. Use `cpu-service-v1` for immutable initialization, or `cpu-service-fresh-inputs-v1` with `fresh_env_names` for values supplied at each open. Declare an unprivileged user and command readiness. Each open runs the reset script and its owned startup hooks before returning. Initialization-dependent and secret inputs use ordinary startup.
|
|
1116
|
+
|
|
1117
|
+
### Waiting agent reachability (0.13.0+)
|
|
1118
|
+
|
|
1119
|
+
Set `--reachable 30m` when opening a workspace to choose how long waiting agents stay reachable through connections they opened. See [reachability settings](https://docs.shardflux.dev/limits#cloud-agent-reachability). Incoming replies wake the parked workspace, which answers them and parks again. These wakes count as parked time; agent work counts as awake time.
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
## Cloud agents (0.13.0+)
|
|
1123
|
+
|
|
1124
|
+
Run `shard ./` in your project to start Claude Code or Codex, usable immediately while setup runs in the background.
|
|
1125
|
+
Your **machine** carries credentials, tools, dotfiles, logins and agent config; the project's **template** carries
|
|
1126
|
+
toolchains, dependencies, services and project secrets. Both are private filesystem templates and include secrets
|
|
1127
|
+
as ordinary files. Browser logins and Claude/Codex config belong to the machine. Set up tools and secrets normally inside the VM;
|
|
1128
|
+
your agent uses the same save/rollback commands there. Every agent starts from both. Save or roll back with `shard machine save|rollback` and
|
|
1129
|
+
`shard template save|rollback`; changed files reach running agents live, software at the next start.
|
|
1130
|
+
|
|
1131
|
+
`shard agents fork` or `f` in the command center copies the whole running state into an independent workspace and
|
|
1132
|
+
conversation. Open in your terminal, Claude Desktop or the Codex app, and the terminal Claude conversation in
|
|
1133
|
+
claude.ai or mobile. Session titles start with `[SF] `. Waiting agents stay reachable on their own connections;
|
|
1134
|
+
see the [settings and billing reference](https://docs.shardflux.dev/limits#cloud-agent-reachability).
|
|
1135
|
+
Claude needs one cloud sign-in per account; Codex uses your copied login.
|
|
1136
|
+
|
|
1137
|
+
Import project secrets from hsec, 1Password, Bitwarden or Keychain. Approve browser fills from the attached CLI or
|
|
1138
|
+
menu bar; remember a selected login for unattended work with automatic relay renewal. Bound secret values are
|
|
1139
|
+
redacted from command output and saved logs.
|
|
1140
|
+
|
|
1141
|
+
Imported secrets: stored encrypted with AWS KMS; only this project's workspaces can load it, and every load is logged.
|
|
1142
|
+
Vault fills: sent end to end from your vault to the workspace; Shardflux only relays encrypted bytes.
|
|
1143
|
+
Remembered credentials: stored encrypted with AWS KMS and filled straight into the page; agents never see it, and every use is logged.
|
|
1144
|
+
|
|
1145
|
+
[Start a cloud agent](https://docs.shardflux.dev/guides/cloud-agents) ·
|
|
1146
|
+
[Machine and template](https://docs.shardflux.dev/guides/machine-and-template) ·
|
|
1147
|
+
[Credentials](https://docs.shardflux.dev/guides/credentials)
|
|
1148
|
+
|
|
1149
|
+
`shard templates` remains the platform template build and management group.
|
package/dist/account-args.js
CHANGED
|
@@ -378,6 +378,8 @@ export const ACCOUNT_COMMANDS = [
|
|
|
378
378
|
options: { confirm: { type: 'string', value: '<email>', description: 'Your email address, typed out (required).' }, ...YES('the deletion') },
|
|
379
379
|
},
|
|
380
380
|
{ path: ['account', 'cancel-deletion'], summary: 'Cancel a scheduled account deletion', positionals: [], options: {} },
|
|
381
|
+
{ path: ['status'], summary: 'Show workspaces, agents and shared volumes', positionals: [], options: {} },
|
|
382
|
+
{ path: ['volumes', 'open'], summary: 'Create or reuse a shared volume by name', positionals: [{ name: 'name', required: true }], options: { 'quota-gib': { type: 'string', value: '<GiB>', description: 'New volume quota (default 1 GiB).' }, ...VOLUME_WAIT } },
|
|
381
383
|
// ---- project API key: egress and volumes ---------------------------------------------------------------
|
|
382
384
|
{
|
|
383
385
|
path: ['egress', 'get'],
|
package/dist/account-commands.js
CHANGED
|
@@ -1699,6 +1699,45 @@ export const ACCOUNT_HANDLERS = {
|
|
|
1699
1699
|
ctx.io.stderr.write('more: shard volumes list --all\n');
|
|
1700
1700
|
return 0;
|
|
1701
1701
|
},
|
|
1702
|
+
async status(ctx) {
|
|
1703
|
+
const cloud = ctx.cloud();
|
|
1704
|
+
const workspaces = [];
|
|
1705
|
+
const volumes = [];
|
|
1706
|
+
const agents = [];
|
|
1707
|
+
let agentsAvailable = true;
|
|
1708
|
+
const readAgents = async () => {
|
|
1709
|
+
try {
|
|
1710
|
+
let cursor = null;
|
|
1711
|
+
do {
|
|
1712
|
+
const page = await cloud.request('GET', '/v1/agents', { query: { limit: 200, ...(cursor ? { cursor } : {}) } });
|
|
1713
|
+
agents.push(...page.data);
|
|
1714
|
+
cursor = page.next_cursor;
|
|
1715
|
+
} while (cursor);
|
|
1716
|
+
}
|
|
1717
|
+
catch (err) {
|
|
1718
|
+
if (!(err instanceof ShardfluxApiError && err.status === 404))
|
|
1719
|
+
throw err;
|
|
1720
|
+
agentsAvailable = false;
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
await Promise.all([
|
|
1724
|
+
(async () => { for await (const w of cloud.workspaces.listAll({ lifetime: 'any', purpose: 'any' }))
|
|
1725
|
+
workspaces.push(w.data); })(),
|
|
1726
|
+
(async () => { for await (const v of cloud.volumes.listAll({ includeOrgShared: true }))
|
|
1727
|
+
volumes.push(v); })(), readAgents(),
|
|
1728
|
+
]);
|
|
1729
|
+
const agentText = agentsAvailable
|
|
1730
|
+
? agents.length ? table(['WORKSPACE', 'APP', 'STATE', 'TITLE'], agents.map((a) => [a.workspace_key, a.app, a.state, a.title])) : 'No agents.\n'
|
|
1731
|
+
: 'Agents are unavailable on this API.\n';
|
|
1732
|
+
out(ctx, `Workspaces\n${workspaces.length ? table(WORKSPACE_HEADERS, workspaces.map(workspaceRow)) : 'No workspaces.\n'}\nAgents\n${agentText}\nVolumes\n${volumes.length ? volumeTable(volumes) : 'No volumes.\n'}`, { workspaces, agents: agentsAvailable ? agents : null, volumes });
|
|
1733
|
+
return 0;
|
|
1734
|
+
},
|
|
1735
|
+
async 'volumes open'(ctx, values, [name]) {
|
|
1736
|
+
const quota = str(values['quota-gib']);
|
|
1737
|
+
const r = await waitedVolume(ctx, () => ctx.cloud().volumes.open({ name: name, ...(quota ? { quotaGib: parsePositiveInt(quota, 'quota-gib', 65536) } : {}), wait: volumeWait(ctx, values) }));
|
|
1738
|
+
out(ctx, `Volume ${r.volume.name} (${r.volume.id}) is ${r.volume.state}.\n${volumeDetail(r.volume)}`, r);
|
|
1739
|
+
return 0;
|
|
1740
|
+
},
|
|
1702
1741
|
async 'volumes create'(ctx, values, [name]) {
|
|
1703
1742
|
const quota = parsePositiveInt(need(values, 'quota-gib', '--quota-gib 20'), 'quota-gib', 100_000);
|
|
1704
1743
|
const r = await waitedVolume(ctx, () => ctx.cloud().volumes.create({ name: name, quotaGib: quota, ...(values['org-shared'] === true ? { orgShared: true } : {}), wait: volumeWait(ctx, values) }));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CellClient, PtySession } from '@shardflux/sdk';
|
|
2
|
+
import type { Ctx, Handler } from './commands.js';
|
|
3
|
+
/** `<slug>-<6 hex of sha256(identity)>`: the same repository gets the same key on every machine. */
|
|
4
|
+
export declare function disambiguatedSlug(slug: string, identity: string): string;
|
|
5
|
+
export declare function projectTemplate(ctx: Ctx, remote: string | null): Promise<string>;
|
|
6
|
+
export declare function agentPty(cell: CellClient, app: 'claude' | 'codex', agent: string, approvals: boolean, term: string, signal?: AbortSignal, project?: string, setupHash?: string, cwd?: string, ptyName?: string): Promise<PtySession>;
|
|
7
|
+
export declare const launchProject: Handler;
|
|
8
|
+
export declare const AGENT_HANDLERS: Record<string, Handler>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { agentReachability } from "./agent-reachability.js";
|
|
2
|
+
import { configureCredentialFill, watchCredentialPrompts } from "./credential-prompts.js";
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
5
|
+
import { UsageError } from "./errors.js";
|
|
6
|
+
import { startProjectSync } from "./project-sync.js";
|
|
7
|
+
import { AgentStateStore } from "./agent-state.js";
|
|
8
|
+
import { attachTerminal, cellConnector } from "./agent-terminal.js";
|
|
9
|
+
import { prepareProjectTemplate } from "./project-template-setup.js";
|
|
10
|
+
import { projectIdentity, projectLabels } from "./project-identity.js";
|
|
11
|
+
import { json } from "./format.js";
|
|
12
|
+
import { resolveWorkspace } from "./commands.js";
|
|
13
|
+
import { savedMachine } from "./filesystem-state.js";
|
|
14
|
+
import { startAgentConfigTunnels, syncAgentConfig } from "./agent-config-sync.js";
|
|
15
|
+
import { rememberGitAgent } from "./agent-git-commands.js";
|
|
16
|
+
import { projectWorkspacePath, seedAgentRepository, workspaceRepositoryPath } from "./agent-repository.js";
|
|
17
|
+
import { agentName } from "./agent-git.js";
|
|
18
|
+
import { cloudClaudeLogin, ensureCloudClaude, loginClaudeInWorkspace, requireClaudeTerminal } from "./claude-cloud-login.js";
|
|
19
|
+
const active = (session) => session.state === 'running' || session.state === 'starting';
|
|
20
|
+
/** `<slug>-<6 hex of sha256(identity)>`: the same repository gets the same key on every machine. */
|
|
21
|
+
export function disambiguatedSlug(slug, identity) {
|
|
22
|
+
return `${slug.slice(0, 89)}-${createHash('sha256').update(identity).digest('hex').slice(0, 6)}`;
|
|
23
|
+
}
|
|
24
|
+
const missing = (error) => error instanceof ShardfluxApiError && error.status === 404;
|
|
25
|
+
export async function projectTemplate(ctx, remote) {
|
|
26
|
+
if (!remote)
|
|
27
|
+
return 'coding';
|
|
28
|
+
try {
|
|
29
|
+
const binding = await ctx.cloud().request('GET', '/v1/project-templates', { query: { repo_remote: remote }, signal: ctx.signal });
|
|
30
|
+
if (typeof binding.template_slug !== 'string' || !binding.template_slug || !Number.isInteger(binding.version) || binding.version < 1)
|
|
31
|
+
throw new Error('project template response omitted a valid template_slug or version');
|
|
32
|
+
return `${binding.template_slug}@${binding.version}`;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (missing(error))
|
|
36
|
+
return 'coding';
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function agentPty(cell, app, agent, approvals, term, signal, project, setupHash, cwd, ptyName) {
|
|
41
|
+
const id = `shard-${ptyName || agent}-${app}`;
|
|
42
|
+
// Arrival and changed locks queue the same durable guest job even when the
|
|
43
|
+
// native agent is already alive. Setup never becomes its first user turn.
|
|
44
|
+
if (setupHash) {
|
|
45
|
+
const queued = await cell.exec.run(['sf-agent', 'setup-start', '--deps-hash', setupHash], { timeoutMs: 30_000, maxOutputBytes: 4096, signal, ...(cwd ? { cwd } : {}) });
|
|
46
|
+
if (queued.exitCode !== 0)
|
|
47
|
+
throw new Error(`project setup could not be queued: ${queued.stderr.trim() || `exit ${queued.exitCode}`}`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const session = await cell.pty.get(id);
|
|
51
|
+
if (active(session))
|
|
52
|
+
return session;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!missing(error))
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const launch = await cell.exec.run(['sf-agent', 'run', app, '--name', agent, '--detach', ...(ptyName ? ['--pty-name', ptyName] : []), ...(approvals ? ['--approvals'] : []), ...(setupHash ? ['--setup', '--setup-deps-hash', setupHash] : [])], {
|
|
59
|
+
env: { TERM: term, ...(project ? { SF_PROJECT_NAME: project } : {}) }, timeoutMs: 30_000, maxOutputBytes: 4096, signal, ...(cwd ? { cwd } : {}),
|
|
60
|
+
});
|
|
61
|
+
if (launch.exitCode !== 0)
|
|
62
|
+
throw new Error(`agent launch failed: ${launch.stderr.trim() || `exit ${launch.exitCode}`}`);
|
|
63
|
+
let result;
|
|
64
|
+
try {
|
|
65
|
+
result = JSON.parse(launch.stdout);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new Error('agent launcher returned invalid JSON');
|
|
69
|
+
}
|
|
70
|
+
if (!result || result.pty_id !== id || !Number.isInteger(result.pid) || result.pid <= 0)
|
|
71
|
+
throw new Error('agent launcher omitted the expected PTY id or process id');
|
|
72
|
+
const session = await cell.pty.get(id);
|
|
73
|
+
if (!active(session))
|
|
74
|
+
throw new Error(`agent exited before attach: ${session.state}`);
|
|
75
|
+
return session;
|
|
76
|
+
}
|
|
77
|
+
async function terminal(ctx, ws, app, agent, project, values, setupHash, cwd = workspaceRepositoryPath(ws)) {
|
|
78
|
+
const store = new AgentStateStore(ctx.store.dir);
|
|
79
|
+
if (!ws.ready)
|
|
80
|
+
await ws.wake({ timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal });
|
|
81
|
+
const cell = ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs });
|
|
82
|
+
const attach = !(ctx.json || values.detach === true);
|
|
83
|
+
// Laptop-local MCP servers are reachable while a terminal is attached (L6); --json/--detach start no tunnels.
|
|
84
|
+
const tunnels = attach ? await startAgentConfigTunnels(ctx, ws) : null;
|
|
85
|
+
try {
|
|
86
|
+
const credentialFill = await configureCredentialFill(ctx, ws);
|
|
87
|
+
const session = await agentPty(cell, app, agent, values.approvals === true, ctx.io.env.TERM || 'xterm-256color', ctx.signal, ws.labels.shard_project || ws.key.split('/')[0], setupHash, cwd, ws.labels.agent_pty_name);
|
|
88
|
+
store.save(project, app);
|
|
89
|
+
if (!attach) {
|
|
90
|
+
ctx.io.stdout.write(json({ workspace: ws.data, session }));
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
ctx.io.stderr.write(`Shardflux · ${ws.key}\nDetach: Ctrl-] then d\n`);
|
|
94
|
+
return await attachTerminal({
|
|
95
|
+
...(credentialFill ? { credentialPrompts: (ask, signal) => watchCredentialPrompts(ctx, ws, ask, signal) } : {}),
|
|
96
|
+
io: ctx.io, signal: ctx.signal, connect: cellConnector(cell, session.session_id),
|
|
97
|
+
wake: (signal) => ws.wake({ timeoutMs: ctx.wakeTimeoutMs, signal }),
|
|
98
|
+
invalidate: () => cell.tokens.invalidate(),
|
|
99
|
+
sleep: (ms, signal) => new Promise((resolve) => {
|
|
100
|
+
const done = () => { clearTimeout(timer); signal.removeEventListener('abort', done); resolve(); };
|
|
101
|
+
const timer = setTimeout(done, ms);
|
|
102
|
+
signal.addEventListener('abort', done, { once: true });
|
|
103
|
+
if (signal.aborted)
|
|
104
|
+
done();
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await tunnels?.stop();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function requireTerminal(ctx) {
|
|
113
|
+
if (ctx.json || !ctx.io.stdin.isTTY || !ctx.io.stdin.setRawMode || !ctx.io.stdin.on)
|
|
114
|
+
throw new UsageError('agent terminal requires a TTY; use "shard agents list --json" to list sessions');
|
|
115
|
+
}
|
|
116
|
+
/** L2 registers this exact command name; absence keeps the launch independent of its merge order. */
|
|
117
|
+
async function syncCredentials(ctx) {
|
|
118
|
+
const { HANDLERS } = await import("./handlers.js");
|
|
119
|
+
const sync = HANDLERS['agents credentials sync'];
|
|
120
|
+
if (sync) {
|
|
121
|
+
// The launcher reuses the registered sync command, keeping its metadata out of the terminal stream.
|
|
122
|
+
const code = await sync({ ...ctx, io: { ...ctx.io, stdout: { write: () => true } } }, {}, []);
|
|
123
|
+
if (code !== 0)
|
|
124
|
+
throw new Error('agent credentials sync failed');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export const launchProject = async (ctx, values, [path = './', explicitApp]) => {
|
|
128
|
+
if (explicitApp && explicitApp !== 'claude' && explicitApp !== 'codex')
|
|
129
|
+
throw new UsageError('agent app must be claude or codex');
|
|
130
|
+
let like = typeof values.like === 'string' ? await resolveWorkspace(ctx, values.like) : undefined;
|
|
131
|
+
const project = like ? { folder: path, slug: like.labels.shard_project || like.key.split('/')[0], remote: like.labels.repo_remote || null } : projectIdentity(path);
|
|
132
|
+
const projectKey = project.remote ?? (like ? project.slug : project.folder);
|
|
133
|
+
const store = new AgentStateStore(ctx.store.dir);
|
|
134
|
+
const app = explicitApp ?? store.app(projectKey, like?.labels.app === 'codex' ? 'codex' : 'claude');
|
|
135
|
+
const agent = typeof values.name === 'string' ? values.name : 'main';
|
|
136
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(agent))
|
|
137
|
+
throw new UsageError('agent name must match ^[a-z0-9][a-z0-9._-]{0,63}$');
|
|
138
|
+
if (agent !== 'main')
|
|
139
|
+
agentName(agent);
|
|
140
|
+
const labels = projectLabels(project, agent, app);
|
|
141
|
+
const cloud = ctx.cloud();
|
|
142
|
+
let existing = await cloud.workspaces.findByKey(`${project.slug}/${agent}`, { includeDeleted: false, signal: ctx.signal });
|
|
143
|
+
const owner = agent === 'main' ? existing : like ?? await cloud.workspaces.findByKey(`${project.slug}/main`, { includeDeleted: false, signal: ctx.signal });
|
|
144
|
+
if (owner && owner.labels.repo_remote !== undefined && owner.labels.repo_remote !== labels.repo_remote) {
|
|
145
|
+
// Two repositories with one name (acme/api, other/api): the later one gets the name plus a stable hash of its remote.
|
|
146
|
+
project.slug = disambiguatedSlug(project.slug, labels.repo_remote || project.folder);
|
|
147
|
+
labels.shard_project = project.slug;
|
|
148
|
+
existing = await cloud.workspaces.findByKey(`${project.slug}/${agent}`, { includeDeleted: false, signal: ctx.signal });
|
|
149
|
+
}
|
|
150
|
+
if (values.name !== undefined) {
|
|
151
|
+
if (existing)
|
|
152
|
+
throw new UsageError(`agent ${project.slug}/${agent} already exists`);
|
|
153
|
+
like ??= await cloud.workspaces.findByKey(`${project.slug}/main`, { includeDeleted: false, signal: ctx.signal }) ?? undefined;
|
|
154
|
+
if (!like)
|
|
155
|
+
throw new UsageError('a new agent needs the project main workspace; start the project first');
|
|
156
|
+
}
|
|
157
|
+
await syncCredentials(ctx);
|
|
158
|
+
const claudeReady = app === 'claude' ? await cloudClaudeLogin(ctx) : undefined;
|
|
159
|
+
if (app === 'claude' && !claudeReady)
|
|
160
|
+
requireClaudeTerminal(ctx);
|
|
161
|
+
if (!ctx.json && values.detach !== true)
|
|
162
|
+
requireTerminal(ctx);
|
|
163
|
+
// A `like` agent inherits the main workspace's template; only a project's own workspace prepares one (L7).
|
|
164
|
+
const prepared = like ? undefined : await prepareProjectTemplate(ctx, project, existing);
|
|
165
|
+
const ws = await cloud.workspaces.open({
|
|
166
|
+
...agentReachability(values),
|
|
167
|
+
key: `${project.slug}/${agent}`, ...(like ? { like: like.id } : { template: prepared.template }),
|
|
168
|
+
labels: { ...existing?.labels, ...labels, ...prepared?.labels, repo_path: projectWorkspacePath(project) }, agentLabel: ctx.agentLabel,
|
|
169
|
+
...(!existing && agent !== 'main' ? { retention: { delete_after_idle_days: 7 } } : {}),
|
|
170
|
+
wait: { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal },
|
|
171
|
+
});
|
|
172
|
+
if (app === 'claude' && !claudeReady)
|
|
173
|
+
await loginClaudeInWorkspace(ctx, ws);
|
|
174
|
+
if (!(await savedMachine(ctx))?.template)
|
|
175
|
+
await syncAgentConfig(ctx, ws);
|
|
176
|
+
// Extra/like agents return work through hidden refs; only the main owns local sync.
|
|
177
|
+
const sync = agent === 'main' && !like ? await startProjectSync(ctx, ws, project) : undefined;
|
|
178
|
+
try {
|
|
179
|
+
if (!sync) {
|
|
180
|
+
await ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs }).files.mkdir(workspaceRepositoryPath(ws), { parents: true });
|
|
181
|
+
await seedAgentRepository(ctx, ws, project.folder, { populate: true });
|
|
182
|
+
}
|
|
183
|
+
await rememberGitAgent(ctx, ws, project.folder);
|
|
184
|
+
return await terminal(ctx, ws, app, agent, projectKey, values, prepared?.setupHash, sync?.root);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await sync?.stop();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
export const AGENT_HANDLERS = {
|
|
191
|
+
async 'agents open'(ctx, values, [agent = 'main']) {
|
|
192
|
+
if (values.app !== undefined && values.app !== 'claude' && values.app !== 'codex')
|
|
193
|
+
throw new UsageError('--app must be claude or codex');
|
|
194
|
+
if (values.in !== undefined && values.in !== 'terminal') {
|
|
195
|
+
const { HANDLERS } = await import("./handlers.js");
|
|
196
|
+
const target = typeof values.in === 'string' ? HANDLERS[`agents open ${values.in}`] : undefined;
|
|
197
|
+
if (!target)
|
|
198
|
+
throw new UsageError('--in must be an available open target');
|
|
199
|
+
return target(ctx, values, [agent]);
|
|
200
|
+
}
|
|
201
|
+
await syncCredentials(ctx);
|
|
202
|
+
let ws;
|
|
203
|
+
if (agent.includes('/'))
|
|
204
|
+
ws = await ctx.cloud().workspace(agent, { create: false }).open();
|
|
205
|
+
else {
|
|
206
|
+
const project = projectIdentity('./');
|
|
207
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(agent))
|
|
208
|
+
throw new UsageError('agent must be a workspace key or an agent name');
|
|
209
|
+
ws = await ctx.cloud().workspace(`${project.slug}/${agent}`, { create: false }).open();
|
|
210
|
+
}
|
|
211
|
+
if (values.reachable !== undefined)
|
|
212
|
+
ws = await ctx.cloud().workspaces.open({ key: ws.key, ...agentReachability(values), wait: false });
|
|
213
|
+
const projectKey = typeof values.project === 'string' ? values.project : ws.labels.repo_remote || ws.labels.shard_project || ws.key;
|
|
214
|
+
const app = values.app ?? new AgentStateStore(ctx.store.dir).app(projectKey, ws.labels.app === 'codex' ? 'codex' : 'claude');
|
|
215
|
+
if (app === 'claude')
|
|
216
|
+
await ensureCloudClaude(ctx, ws);
|
|
217
|
+
if (!ctx.json && values.detach !== true)
|
|
218
|
+
requireTerminal(ctx);
|
|
219
|
+
if (ws.labels.shard_layer !== 'machine' && !(await savedMachine(ctx))?.template)
|
|
220
|
+
await syncAgentConfig(ctx, ws);
|
|
221
|
+
return terminal(ctx, ws, app, (ws.labels.agent ?? agent).split('/').at(-1), projectKey, values);
|
|
222
|
+
},
|
|
223
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ConfigIssue = {
|
|
2
|
+
item: string;
|
|
3
|
+
reason: string;
|
|
4
|
+
};
|
|
5
|
+
export type ConfigFile = {
|
|
6
|
+
content: Buffer;
|
|
7
|
+
mode: string;
|
|
8
|
+
};
|
|
9
|
+
export type ConfigFiles = Map<string, ConfigFile>;
|
|
10
|
+
export type ObjectValue = Record<string, unknown>;
|
|
11
|
+
export declare const object: (v: unknown) => ObjectValue;
|
|
12
|
+
export declare const text: (v: unknown) => string | undefined;
|
|
13
|
+
export declare const absent: (e: unknown) => boolean;
|
|
14
|
+
export declare function readOptional(file: string): Promise<Buffer | undefined>;
|
|
15
|
+
export declare function readObject(file: string): Promise<ObjectValue | undefined>;
|
|
16
|
+
/** Dereference skill symlinks, including ones shared from another local directory, but never traverse a cycle. */
|
|
17
|
+
export declare function collectTree(source: string, dest: string, files: ConfigFiles, issues: ConfigIssue[], parents?: Set<string>): Promise<void>;
|
|
18
|
+
export declare function collectUserFiles(home: string, issues: ConfigIssue[]): Promise<ConfigFiles>;
|
|
19
|
+
export declare function macBinary(bytes: Buffer): boolean;
|
|
20
|
+
/** Preserve v2 install metadata and user-scope records, and copy just the trees needed to load them on Linux. */
|
|
21
|
+
export declare function collectPlugins(home: string, remoteHome: string, files: ConfigFiles, issues: ConfigIssue[]): Promise<Set<string>>;
|
|
22
|
+
/** Only rewrite paths to files we actually send. Missing laptop paths are never made to look portable. */
|
|
23
|
+
export declare function rewritePaths(value: unknown, home: string, remoteHome: string, files: ConfigFiles, issues: ConfigIssue[], item: string): unknown;
|