@shardflux/cli 0.24.1 → 0.25.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 +30 -1
- package/README.md +37 -1
- package/dist/acp-journal.js +23 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +32 -4
- package/dist/board-args.js +18 -1
- package/dist/board-commands.d.ts +2 -2
- package/dist/board-commands.js +22 -5
- package/dist/conversation-commands.js +18 -2
- package/dist/conversations.d.ts +2 -1
- package/dist/device-folders.d.ts +8 -0
- package/dist/device-folders.js +14 -6
- package/dist/device-helper.js +13 -2
- package/dist/errors.js +2 -1
- package/dist/format.js +6 -2
- package/dist/handlers.js +2 -1
- package/dist/land.d.ts +116 -0
- package/dist/land.js +238 -0
- package/dist/main.js +7 -1
- package/dist/onboarding.d.ts +4 -4
- package/dist/router-commands.d.ts +11 -0
- package/dist/router-commands.js +1 -1
- package/dist/task-land-hub.py +150 -0
- package/dist/transcribe-commands.d.ts +7 -0
- package/dist/transcribe-commands.js +80 -0
- package/dist/worktree-twin-engine.d.ts +8 -0
- package/dist/worktree-twin-engine.js +28 -22
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Device requests reach the right computer (0.25.0+): the background helper tells the API which conversations are
|
|
6
|
+
local to this computer (started or opened here), so their requests come here first, then to the computer you used
|
|
7
|
+
last, never to whichever was seen last. A project link with a first task (`project_link` with `brief`) reads the
|
|
8
|
+
start's whole JSON output; before, it failed with "Unexpected token '}'" although the conversation started.
|
|
9
|
+
|
|
10
|
+
- `shard board` and `shard task show` show what a working task does now ("working (Fixing the login cookie)") and
|
|
11
|
+
what each working step does, next to the plain waiting line of a task that waits to start.
|
|
12
|
+
- `shard transcribe <file>` (0.25.0+, SDK 0.28.0) prints what a recording says (WebM/Opus or PCM WAV from the file;
|
|
13
|
+
raw PCM with `--format pcm16 --sample-rate`); `--words` adds each word's start and end, `--json` prints the
|
|
14
|
+
transcript.
|
|
5
15
|
- Faster conversation starts (0.24.1): the project's dependency fingerprint is read from Git's own file list instead of walking the whole folder, so ignored and untracked trees beside the project (agent worktrees, build output) no longer hold a start on **Getting your project…**. A folder with 1.2 million such files took 32 s per start; it now takes 0.3 s, with the same fingerprint.
|
|
6
16
|
- A folder without Git that `shard` makes a repository of (a first chat, `shard projects link`) starts with its files as
|
|
7
17
|
the first commit (0.25.0+; `.gitignore` respected, large media stay untracked), so a conversation starts from the
|
|
@@ -18,6 +28,26 @@
|
|
|
18
28
|
could no longer open. Key and sign-in stores (`~/.ssh`, `~/.aws`, `.env` files, private keys) stay here. Up to 64
|
|
19
29
|
files, 64 MiB in all, 16 MiB each; the command names what it brings and what stays, and `--json` adds `files` and
|
|
20
30
|
`files_left`.
|
|
31
|
+
- 0.25.0 (pending): Allowance hints respect unavailable overage; operation errors retain the service’s recovery message and retry delay.
|
|
32
|
+
|
|
33
|
+
- Landing that lands (0.25.0+): `shard task land <task>` merges a finished task's result (`shard/<task id>`, where its
|
|
34
|
+
master merged its workers' branches; when a master did not, their branches are merged into it here when they merge
|
|
35
|
+
cleanly) into the project's default branch on the project hub, a fast-forward or a clean merge commit, never by
|
|
36
|
+
force; brings it into your folder on this computer the way finished conversations land (your branch moves, your
|
|
37
|
+
uncommitted edits stay, rewritten files are backed up first); checks the commit is in the branch; and only then
|
|
38
|
+
records it on the board (`Landed in ~/code/best-todo · 3f2a1c9`). A conflict changes nothing and asks the task's
|
|
39
|
+
master to merge (`land_conflict`). `shard needs-you answer land_… land` does the same, and
|
|
40
|
+
`shard conversations land <id> merge` now lands the conversation's branch the same way before it records it (before,
|
|
41
|
+
both only recorded a landing). `--path <folder>` picks the folder; `--json` returns `{landed, result, hub, folder,
|
|
42
|
+
recorded, message}`.
|
|
43
|
+
|
|
44
|
+
- `shard conversations delete <id> --yes` (`shard c delete`, 0.25.0+, SDK 0.28.0) deletes a conversation everywhere at
|
|
45
|
+
once: its agent stops, its computer is deleted (a computer other conversations share stays for them), and its
|
|
46
|
+
messages, transcript and branch go with it, on every device. Without `--yes` it is refused before any request, as
|
|
47
|
+
every deletion is. `--json` prints the API's answer, `{id, deleted, deleted_at, operation}`; deleting it again answers
|
|
48
|
+
the same. The person who started it (or created the API key that did) and organization owners and admins delete
|
|
49
|
+
conversations. `shard conversations feed` shows a deleted conversation once as `<id> Deleted conversation deleted`.
|
|
50
|
+
- ACP input recovery (0.25.0): temporary guest-agent interruptions use bounded backoff with the same native input identity. The editor retains its running turn; permanent errors and unverified receipts still require loading the session.
|
|
21
51
|
|
|
22
52
|
- Tunnel recovery (0.24.1, SDK 0.27.3): a foreground reverse tunnel replaces its guest helper automatically when replay is lost. Existing TCP peers reset; fresh connections reach the new listener. SSH and device port-forward connections close cleanly for a new connection instead of looping on an unrecoverable cursor.
|
|
23
53
|
- Folders on this computer (0.25.0+): an agent can list a folder here (`dir_list`: names, kinds, sizes and Git
|
|
@@ -87,7 +117,6 @@
|
|
|
87
117
|
`codex_sign_in_required` and that line. The Codex remote device is never restarted for a sign-in (its Cloud threads
|
|
88
118
|
keep running), and one starting again with the new login reads as connecting.
|
|
89
119
|
|
|
90
|
-
|
|
91
120
|
- `shard conversations start` starts a conversation without a folder: from the project hub (`--repo`, `--branch`), or with `--home` a coordinator on your machine that spawns threads in the project's repositories. `--app`, `--model`, `--effort`, `--title`, `--project`; `--prompt` sends the first message, with `--file` attachments. Prints the conversation's id, key and state (`--json`: the conversation, its workspace and the message).
|
|
92
121
|
|
|
93
122
|
- `shard conversations send <id> <text> --file <path>` (repeatable, up to 20 files of 64 MiB each) puts each file in the conversation's computer before the message, which lists their paths, and prints `Attached <path>` for each. The text may be left out when files are attached, and unquoted words are sent as one message.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @shardflux/cli: `shard`
|
|
2
2
|
|
|
3
|
+
For error codes, retry timing, and plan recovery, see the [error reference](https://docs.shardflux.dev/reference/errors).
|
|
4
|
+
|
|
3
5
|
Historical template file lists: reads queue checksum-verified reconstruction when a cold list is absent from the SQL cache. TypeScript SDK 0.27.0+ / Python 0.23.0+ wait full Retry-After for retryable API 409 file_list_indexing on GET/HEAD within the admission timeout. Template objects and version history stay available.
|
|
4
6
|
|
|
5
7
|
Plan-derived API budgets (0.23.0+, SDK 0.27.0): `--json` error output includes `rate_limit` and `retry_after_seconds`; the text hint names the shared scope and delay. API request pacing follows the SDK admission timeout and preserves the request body. Reuse workspace credentials and reduce status polling when the returned budget asks for a delay.
|
|
@@ -705,6 +707,7 @@ shows the status; `shard version --check` asks now (`--json`: `{"cli", "sdk", "u
|
|
|
705
707
|
| `usage [--org <id>] [--estimate \| --series \| --workspace <id\|key>] [--from T] [--to T] [--granularity hour\|day] [--meter M]` | Usage summary for the current period: plan, meters and allowances. `--estimate`: the bill so far and projected allowance use; `--series`: usage over time; `--workspace`: one workspace's usage over time (0.5.0+). The summary includes the [overage](#usage-and-overage) block (0.5.1+). |
|
|
706
708
|
| `feedback [--yes] [--category C] [--workspace W] [--request-id ID] [--error-code CODE] [--command TEXT] [--from-file F\|-] <message...>` | Preview feedback; `--yes` sends after explicit user authorization (0.22.0+). See [Feedback](#feedback-preview-and-user-approval). |
|
|
707
709
|
| `route [--task ID] [--org O] [--project P] <text...>`, `ask [--task ID] [--org O] [--project P] <question...>` | The board's input bar (0.25.0+): where a line of input goes (its project or your home; a task, a question or a steer, split per task), and a quick question about your work, answered as it is written. |
|
|
710
|
+
| `conversations delete <id> --yes` (`c delete`) | Delete a conversation everywhere at once (0.25.0+): its agent stops, its computer is deleted, and its messages, transcript and branch go with it. `--json` prints `{id, deleted, deleted_at, operation}`. See [Conversations](#conversations). |
|
|
708
711
|
| `workspaces open <key> --template <slug> [--secret NAME]... [--input NAME=value]... [--input-file F] [--lifetime persistent\|session] [--mode processful\|file-first] [--cpu-millis N --memory-mib N --disk-gib N] [--allocation-mode fixed\|elastic] [--memory-held N] [--no-wait] [--timeout 5m] [--timing]` | Open by key. Creates the workspace on first use and reconnects or resumes it afterwards, never resetting it. Waits until it is ready (the template's start commands and services included) unless you pass `--no-wait`. `--secret` binds secret names (replacing the binding of an existing workspace). `--input` (0.4.0+) passes the template's text inputs (replacing those of an existing workspace); `--input-file` reads `NAME=value` lines or a JSON object. `--lifetime session`: the workspace is discarded when the session ends (`workspaces close` or the idle timeout), and the key then opens a new workspace. `--mode file-first` (0.5.0+) opens a [file-first workspace](#file-first-workspaces); the default is processful for a new key and the stored mode for an existing one. `--allocation-mode elastic` (0.8.0+): [elastic memory](#elastic-memory-080). |
|
|
709
712
|
| `workspaces list [--state S] [--prefix P] [--lifetime persistent\|session\|any] [--purpose standard\|template_draft\|template_test\|any] [--include-deleted] [--all] [--limit N] [--cursor C]` | One page, or every page with `--all`. The next cursor is printed on stderr. By default only persistent standard workspaces are listed. A file-first workspace's state reads `running (file-first)` (0.5.0+). |
|
|
710
713
|
| `workspaces get <id\|key>` | One workspace (deleted ones included), with the startup of its template's start commands and services (0.4.0+): a failed step shows its exit code, reason and output tail. Its mode, and a file-first workspace's tree revision (0.5.0+). Its memory (0.8.0+): `fixed 4096 MiB`, or `elastic: promised …, held …, plugged +… MiB`. The template version whose [immutable paths](#immutable-paths-080) it has mounted (0.8.0+): `immutable paths v3 (mounted read-only)`. |
|
|
@@ -1620,6 +1623,17 @@ Conversation JSON includes `join_mode` and the worktree used by reviewers and wo
|
|
|
1620
1623
|
|
|
1621
1624
|
`shard conversations resume <id>` continues a saved conversation in a fresh VM under the same identity (0.14.0+). `shard ./ --resume` uses this same path. `shard conversations reader <project-id>` opens a small read-only workspace for saved branches and transcripts; the Mac transcript mirror opens it automatically when no project VM is live. Adaptive idle reclaims the reader.
|
|
1622
1625
|
|
|
1626
|
+
`shard conversations delete <id> --yes` (0.25.0+) deletes a conversation everywhere at once: its agent stops, its
|
|
1627
|
+
computer is deleted (a computer other conversations share stays for them), and its messages, transcript and branch go
|
|
1628
|
+
with it, on every device. From then on it leaves every list and reads not found; `shard c feed` shows it once as
|
|
1629
|
+
`deleted`. The person who started it and organization owners and admins delete conversations. `--json` prints the
|
|
1630
|
+
API's answer: `{id, deleted, deleted_at, operation}`, where `operation` is the deletion of its computer.
|
|
1631
|
+
|
|
1632
|
+
```sh
|
|
1633
|
+
shard c delete conv_k3m5q7s2u4w6y3b5d7f2h4j6ln --yes
|
|
1634
|
+
# Deleted: conv_k3m5q7s2u4w6y3b5d7f2h4j6ln
|
|
1635
|
+
```
|
|
1636
|
+
|
|
1623
1637
|
### Route input and ask about your work (0.25.0+)
|
|
1624
1638
|
|
|
1625
1639
|
`shard route` reads a line of input the way the board's input bar does: which project it belongs to (or your home),
|
|
@@ -1646,6 +1660,24 @@ prints the API's object: the route (`kind`, `parts`, `task_ref`, `uncertain`,
|
|
|
1646
1660
|
`needs_work`, `sources`, …). An answer that needs work ends with "Make it a task?". Neither starts work or wakes a
|
|
1647
1661
|
computer.
|
|
1648
1662
|
|
|
1663
|
+
### Transcribe a recording (0.25.0+)
|
|
1664
|
+
|
|
1665
|
+
`shard transcribe <file>` prints what a recording says. `--words` adds each word with its start and end; `--json`
|
|
1666
|
+
prints the transcript (`text`, `words` with `w`, `start` and `end` in seconds, `language`, `duration`).
|
|
1667
|
+
|
|
1668
|
+
```sh
|
|
1669
|
+
shard transcribe note.webm
|
|
1670
|
+
# Take this sidebar, and move it over here, then make the header a bit taller, and change that color to blue.
|
|
1671
|
+
shard transcribe hello.wav --words
|
|
1672
|
+
# Move this button over here, then make that panel a little wider.
|
|
1673
|
+
# 0:00.00 0:00.18 Move
|
|
1674
|
+
# 0:00.18 0:00.44 this
|
|
1675
|
+
# …
|
|
1676
|
+
```
|
|
1677
|
+
|
|
1678
|
+
WebM/Opus and PCM WAV are read from the file; raw 16-bit mono PCM takes `--format pcm16 --sample-rate 16000`.
|
|
1679
|
+
`--language en` names the language when you know it. Up to 120 s of audio. Signed in, or with `SHARDFLUX_API_KEY`.
|
|
1680
|
+
|
|
1649
1681
|
### Permission requests (0.25.0+)
|
|
1650
1682
|
|
|
1651
1683
|
When a conversation's agent asks before it runs a tool (Claude Code, Codex or an ACP agent), the conversation is
|
|
@@ -1731,11 +1763,15 @@ changes the words (`--text`, and the task's master hears it), the title (`--titl
|
|
|
1731
1763
|
(`--clear`; `--no-clear` brings it back). `shard task steer <id> <words...>` sends steering words to the task's master,
|
|
1732
1764
|
`shard task rm <id>` deletes the task, and `shard task clear-done` clears every finished one. `shard task show <id>`
|
|
1733
1765
|
prints the task, its plan and each report: milestone, outcome and evidence (`tests 12/12 (pnpm test)`).
|
|
1766
|
+
`shard task land <id>` lands a finished task: its result (the branch `shard/<task id>`, where its master merged its
|
|
1767
|
+
workers' branches) goes into the project's default branch and into your folder on this computer the way finished
|
|
1768
|
+
conversations land there (your uncommitted edits stay), and the board shows it shipped once the commit is in the
|
|
1769
|
+
branch: `Landed in ~/code/best-todo · 3f2a1c9`.
|
|
1734
1770
|
|
|
1735
1771
|
**Needs you.** `shard needs-you` lists permission requests, questions, credential requests, finished work to land and
|
|
1736
1772
|
Later tasks to keep, oldest first. `shard needs-you answer <id>` answers any of them: `allow`, `always` (also adds the
|
|
1737
1773
|
always-allow rule for the project; `--everywhere` for every project) or `deny` for a permission request, `--pick` for an
|
|
1738
|
-
agent's question in one, `--text` for a question, `land
|
|
1774
|
+
agent's question in one, `--text` for a question, `land` (as `shard task land`), `keep` or `discard` for finished work, and `--keep <task-id>`
|
|
1739
1775
|
(or `keep`, `discard`) for Later tasks. The agent goes on as soon as you answer.
|
|
1740
1776
|
|
|
1741
1777
|
**Approvals.** The approval mode answers your agents' permission requests before they reach you: `ask` sends every one
|
package/dist/acp-journal.js
CHANGED
|
@@ -90,6 +90,28 @@ export class AcpJournal {
|
|
|
90
90
|
}
|
|
91
91
|
#wanted() { return !this.#signal.aborted && !this.rpc.closed && this.#paused === 0 && (this.rpc.busy || this.#cursor.prompt_id !== undefined); }
|
|
92
92
|
async #deliver(inputId, message) {
|
|
93
|
+
let failures = 0;
|
|
94
|
+
const started = performance.now();
|
|
95
|
+
while (true) {
|
|
96
|
+
this.#signal.throwIfAborted();
|
|
97
|
+
try {
|
|
98
|
+
return await this.#deliverOnce(inputId, message);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Guest restarts can refuse a new input command while its native
|
|
102
|
+
// supervisor keeps running. Its persisted receipt deduplicates this
|
|
103
|
+
// exact input id, including a command accepted before the refusal.
|
|
104
|
+
// An unverified receipt or a permanent error still stops delivery.
|
|
105
|
+
if (!(error instanceof ShardfluxApiError) || !error.retryable)
|
|
106
|
+
throw error;
|
|
107
|
+
const remaining = RECONNECT_BUDGET_MS - (performance.now() - started);
|
|
108
|
+
if (remaining <= 0)
|
|
109
|
+
throw new Error('ACP input reconnect timed out; load this session to continue.', { cause: error });
|
|
110
|
+
await delay(Math.min(reattachDelayMs(error, ++failures), remaining), undefined, { signal: this.#signal });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async #deliverOnce(inputId, message) {
|
|
93
115
|
const dir = `/home/user/.local/share/shardflux/acp/${this.#conversation}`;
|
|
94
116
|
const payload = Buffer.from(`{"mode":"input","input_id":${JSON.stringify(inputId)},"message":${message}}\n`);
|
|
95
117
|
if (payload.length <= ONE_SHOT_INPUT) {
|
|
@@ -133,7 +155,7 @@ export class AcpJournal {
|
|
|
133
155
|
finally {
|
|
134
156
|
await this.#cell.exec.cancel(session.session_id, 0).catch(() => undefined);
|
|
135
157
|
}
|
|
136
|
-
//
|
|
158
|
+
// Retries retain inputId: CVB persists its receipt before provider stdin.
|
|
137
159
|
}
|
|
138
160
|
start() {
|
|
139
161
|
this.#initial ??= this.#replay().then(() => { this.#ready = true; this.#follow(); }).catch(error => { this.rpc.close(error instanceof Error ? error : new Error(String(error))); throw error; });
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -14,7 +14,7 @@ import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
|
14
14
|
import { BOARD_COMMANDS } from "./board-args.js";
|
|
15
15
|
import { UsageError } from "./errors.js";
|
|
16
16
|
import { DEFAULT_CHOICE, MODEL_CHOICES } from "./agent-models.js";
|
|
17
|
-
export const CLI_VERSION = '0.
|
|
17
|
+
export const CLI_VERSION = '0.25.0';
|
|
18
18
|
export const GLOBAL_OPTIONS = {
|
|
19
19
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
20
20
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -156,7 +156,7 @@ const KEY_COMMANDS = [
|
|
|
156
156
|
{ path: ['conversations', 'feed'], summary: 'Print project conversations as they change', positionals: [], options: {
|
|
157
157
|
project: { type: 'string', value: '<id>', description: 'Project ID for a user session.' }, org: { type: 'string', value: '<id|slug>', description: 'Signed in: every project of this organization you work in (0.25.0+).' }, cursor: { type: 'string', value: '<cursor>', description: 'Continue after this feed cursor (--json prints it with each change).' }
|
|
158
158
|
},
|
|
159
|
-
description: 'Prints each conversation the moment it changes, until interrupted: "<id> <title> <state>", and why it needs you when it does; --json prints one {conversation, cursor} object per line. With an API key: its project. Signed in without a key: every project you work in, on one cursor (0.25.0+); --org follows one organization’s projects and uses your sign-in when you have one.',
|
|
159
|
+
description: 'Prints each conversation the moment it changes, until interrupted: "<id> <title> <state>", and why it needs you when it does; a deleted conversation shows once as "<id> Deleted conversation deleted" (0.25.0+); --json prints one {conversation, cursor} object per line. With an API key: its project. Signed in without a key: every project you work in, on one cursor (0.25.0+); --org follows one organization’s projects and uses your sign-in when you have one.',
|
|
160
160
|
examples: ['shard c feed', 'shard c feed --org acme --json'] },
|
|
161
161
|
{ path: ['conversations', 'resume'], summary: 'Resume a saved conversation in a fresh VM', positionals: [{ name: 'id', required: true }], options: {} },
|
|
162
162
|
{ path: ['conversations', 'reader'], summary: 'Open a read-only project space reader', positionals: [{ name: 'project_id', required: true }], options: {} },
|
|
@@ -194,8 +194,18 @@ const KEY_COMMANDS = [
|
|
|
194
194
|
{ path: ['conversations', 'events'], summary: 'Follow a conversation live: state, transcript, reply text and tool calls (0.25.0+)', positionals: [{ name: 'id', required: true }], options: {},
|
|
195
195
|
description: 'Streams the conversation until Ctrl-C, one line per event: state changes, new transcript lines and tool calls, with the reply written as the agent streams it. With --json: one event per line, {"type":"conversation"|"transcript"|"delta","id":…} with the conversation, the transcript lines or the ACP session updates.',
|
|
196
196
|
examples: ['shard c events conv_k3m5q7s2u4w6y3b5d7f2h4j6ln', 'shard c events conv_k3m5q7s2u4w6y3b5d7f2h4j6ln --json | jq -c \'select(.type=="delta")\''] },
|
|
197
|
-
{ path: ['conversations', 'land'], summary: '
|
|
197
|
+
{ path: ['conversations', 'land'], summary: 'Land a conversation’s work: merge lands it for real; pr, keep and discard record the decision', positionals: [{ name: 'id', required: true }, { name: 'action', required: true }], options: {
|
|
198
|
+
into: { type: 'string', value: '<ref>', description: 'pr: the PR URL; keep: the branch (merge always lands in the project’s default branch).' },
|
|
199
|
+
path: { type: 'string', value: '<folder>', description: 'merge: the folder to land in (default: where the conversation started on this computer).' }
|
|
200
|
+
},
|
|
201
|
+
description: 'merge (0.25.0+) merges the conversation’s branch into the project’s default branch on the project hub (a fast-forward, or a merge commit when it is clean), brings it into your folder on this computer the way finished conversations land (your uncommitted edits stay), checks the commit is in the branch, and only then records it. pr, keep and discard record what was decided.',
|
|
202
|
+
examples: ['shard conversations land conv_k3m5q7s2u4w6y3b5d7f2h4j6ln merge', 'shard conversations land conv_k3m5q7s2u4w6y3b5d7f2h4j6ln discard'] },
|
|
198
203
|
{ path: ['conversations', 'move'], summary: 'Move at the end of the turn', positionals: [{ name: 'id', required: true }, { name: 'to', required: true }], options: {} },
|
|
204
|
+
{ path: ['conversations', 'delete'], summary: 'Delete a conversation everywhere: its computer, messages, transcript and branch (0.25.0+)', positionals: [{ name: 'id', required: true }], options: {
|
|
205
|
+
yes: { type: 'boolean', description: 'Required: confirm the deletion.' }
|
|
206
|
+
},
|
|
207
|
+
description: 'Deletes the conversation on every device at once: its agent stops and its computer is deleted (a computer other conversations share stays for them), its messages, transcript and permission requests go with it, and its branch and transcript copy leave the project space. From then on it leaves every list and reads not found. The person who started it (or created the API key that did) or an organization owner or admin deletes it; deleting it again answers the same. --json prints {id, deleted, deleted_at, operation}: operation is the deletion of its computer, null when other conversations share it.',
|
|
208
|
+
examples: ['shard c delete conv_k3m5q7s2u4w6y3b5d7f2h4j6ln --yes', 'shard conversations delete conv_k3m5q7s2u4w6y3b5d7f2h4j6ln --yes --json'] },
|
|
199
209
|
{ path: ['conversations', 'landing'], summary: 'Show or set how finished conversations reach this checkout', positionals: [{ name: 'mode' }, { name: 'backup' }], options: {
|
|
200
210
|
path: { type: 'string', value: '<path>', description: 'Local project checkout (default the current directory).' }
|
|
201
211
|
},
|
|
@@ -1461,6 +1471,24 @@ const ROUTER_COMMANDS = [
|
|
|
1461
1471
|
},
|
|
1462
1472
|
examples: ['shard ask what is the healthz work doing right now', 'shard ask --task task_healthzhealthzhealthzhealt "is it done?"', 'shard ask "what is blocking the pricing page?" --json'],
|
|
1463
1473
|
},
|
|
1474
|
+
{
|
|
1475
|
+
path: ['transcribe'],
|
|
1476
|
+
summary: 'Turn a recording into text, with each word’s start and end (0.25.0+)',
|
|
1477
|
+
description: [
|
|
1478
|
+
'Sends the audio to Shardflux speech to text and prints what it says. WebM/Opus (a browser recording) and PCM WAV are recognized from the file; raw 16-bit mono PCM needs --format pcm16 and --sample-rate. Up to 120 s of audio.',
|
|
1479
|
+
'Uses your signed-in session, or SHARDFLUX_API_KEY when it is set. The audio is not kept.',
|
|
1480
|
+
'Prints the text; --words adds one line per word with its start and end. With --json: the transcript (text, words with w, start and end in seconds, language, duration, truncated).',
|
|
1481
|
+
].join('\n\n'),
|
|
1482
|
+
positionals: [{ name: 'file', required: true }],
|
|
1483
|
+
options: {
|
|
1484
|
+
format: { type: 'string', value: '<webm|wav|pcm16>', description: 'The audio format (default: from the file).' },
|
|
1485
|
+
'sample-rate': { type: 'string', value: '<hz>', description: 'Samples per second of raw pcm16 audio (8000 to 48000).' },
|
|
1486
|
+
language: { type: 'string', value: '<code>', description: 'ISO 639-1 code of the language spoken (default: detected).' },
|
|
1487
|
+
words: { type: 'boolean', description: 'Print each word with its start and end.' },
|
|
1488
|
+
org: { type: 'string', value: '<id|slug>', description: 'Organization to transcribe in when you are in several (default: the saved one, shard orgs use, else your only organization).' },
|
|
1489
|
+
},
|
|
1490
|
+
examples: ['shard transcribe note.webm', 'shard transcribe meeting.wav --words', 'shard transcribe mic.pcm --format pcm16 --sample-rate 16000 --json'],
|
|
1491
|
+
},
|
|
1464
1492
|
];
|
|
1465
1493
|
/** The project a drive or models command acts in (0.25.0+). */
|
|
1466
1494
|
const PROJECT_SCOPE = {
|
|
@@ -1927,7 +1955,7 @@ export const EXIT_CODE_HELP = [
|
|
|
1927
1955
|
/** The top help's sections: every command's group (first word) is in exactly one (args.test.ts checks). */
|
|
1928
1956
|
export const HELP_SECTIONS = [
|
|
1929
1957
|
{ title: 'Account (a user session: shard signup or shard auth login)', groups: ['signup', 'verify', 'auth', 'setup', 'env', 'orgs', 'projects', 'api-keys', 'members', 'invitations', 'billing', 'audit', 'account'] },
|
|
1930
|
-
{ title: 'Cloud agents (shard ./ starts one; a project API key)', groups: ['conversations', 'board', 'task', 'needs-you', 'approvals', 'route', 'ask', 'drive', 'models', 'move', 'machine', 'template', 'agents', 'settings', 'accounts', 'device', 'cli-proxy', 'ssh-proxy'] },
|
|
1958
|
+
{ title: 'Cloud agents (shard ./ starts one; a project API key)', groups: ['conversations', 'board', 'task', 'needs-you', 'approvals', 'route', 'ask', 'transcribe', 'drive', 'models', 'move', 'machine', 'template', 'agents', 'settings', 'accounts', 'device', 'cli-proxy', 'ssh-proxy'] },
|
|
1931
1959
|
{ title: 'Workspaces for your app (a project API key: SHARDFLUX_API_KEY, else the one shard setup saved)', groups: ['login', 'whoami', 'status', 'usage', 'workspaces', 'files', 'operations', 'executions', 'secrets', 'credentials', 'volumes', 'egress'] },
|
|
1932
1960
|
{ title: 'Templates for your app’s workspaces (not for cloud agents; publish and archive use the session, the rest the API key)', groups: ['templates'] },
|
|
1933
1961
|
{ title: 'Other', groups: ['menubar', 'update', 'feedback', 'version'] },
|
package/dist/board-args.js
CHANGED
|
@@ -80,6 +80,22 @@ export const BOARD_COMMANDS = [
|
|
|
80
80
|
description: 'Your words go to the agent that runs the task, which adjusts its plan. The task keeps its own words.',
|
|
81
81
|
examples: ['shard task steer task_h7g4d7tqyv2pz3a2npyo4rpgzq use postgres instead'],
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
path: ['task', 'land'],
|
|
85
|
+
summary: 'Land a finished task: its result into your folder and the project’s default branch (0.25.0+)',
|
|
86
|
+
positionals: [{ name: 'id', required: true }],
|
|
87
|
+
options: { path: { type: 'string', value: '<folder>', description: 'The folder to land in (default: the project’s folder on this computer, if any).' } },
|
|
88
|
+
description: [
|
|
89
|
+
'A task has one result: the branch shard/<task id>, where its master merged its workers’ branches. Landing merges it',
|
|
90
|
+
'into the project’s default branch on the project hub (a fast-forward, or a merge commit when the merge is clean),',
|
|
91
|
+
'then brings it into your folder on this computer the way finished conversations land there: your branch moves to the',
|
|
92
|
+
'new commits, your uncommitted edits stay, and every file it rewrites is backed up first (shard conversations landing',
|
|
93
|
+
'backups). Only after the commit is checked to be in the branch is the landing recorded on the board, so the task',
|
|
94
|
+
'reads shipped. A conflict changes nothing: the task’s master is asked to merge and the task asks to land again.',
|
|
95
|
+
'--json returns {landed, result, hub, folder, recorded, message}.',
|
|
96
|
+
].join('\n'),
|
|
97
|
+
examples: ['shard task land task_h7g4d7tqyv2pz3a2npyo4rpgzq', 'shard task land task_h7g4d7tqyv2pz3a2npyo4rpgzq --path ~/code/best-todo --json'],
|
|
98
|
+
},
|
|
83
99
|
{
|
|
84
100
|
path: ['task', 'clear-done'],
|
|
85
101
|
summary: 'Clear every finished task from the board (0.25.0+)',
|
|
@@ -117,7 +133,8 @@ export const BOARD_COMMANDS = [
|
|
|
117
133
|
' cq_… --text <answer>: the agent gets it as its next message',
|
|
118
134
|
' ask_… --text <answer>: the task’s master gets it',
|
|
119
135
|
' cred_… deny (the value goes through the credential request, sealed for its computer)',
|
|
120
|
-
' land_… land (the
|
|
136
|
+
' land_… land (as shard task land: the result into your folder and the default branch), keep (the',
|
|
137
|
+
' branch stays) or discard',
|
|
121
138
|
' prune_… --keep <task-id> for each task to keep, keep for all of them, or discard to clear them',
|
|
122
139
|
'The agent goes on as soon as you answer.',
|
|
123
140
|
].join('\n'),
|
package/dist/board-commands.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ import type { Handler } from './commands.js';
|
|
|
10
10
|
/** A task as people read it: its title once it has one, else your words, on one line of at most `max` characters. */
|
|
11
11
|
export declare function taskLabel(t: Pick<Task, 'title' | 'text'>, max?: number): string;
|
|
12
12
|
/**
|
|
13
|
-
* One task as `shard board` lists it: id, state (with why it waits to start
|
|
14
|
-
* title or words (a follow-up ↳, a suggestion marked), project.
|
|
13
|
+
* One task as `shard board` lists it: id, state (with why it waits to start, "queued (Getting its computer ready)", or
|
|
14
|
+
* what it does now, "working (Fixing the login cookie)"), title or words (a follow-up ↳, a suggestion marked), project.
|
|
15
15
|
*/
|
|
16
16
|
export declare function taskLine(t: Task): string;
|
|
17
17
|
/** One item that waits on you: its id, what is asked, then the command, the suggested answers or the branch behind it. */
|
package/dist/board-commands.js
CHANGED
|
@@ -3,6 +3,7 @@ import { detailLine, pickAnswers } from "./conversation-commands.js";
|
|
|
3
3
|
import { NotFoundError, UsageError } from "./errors.js";
|
|
4
4
|
import { time } from "./format.js";
|
|
5
5
|
import { progress } from "./progress.js";
|
|
6
|
+
import { landDeps, landTask, landedText } from "./land.js";
|
|
6
7
|
const BUCKETS = ['now', 'next', 'later'];
|
|
7
8
|
const BUCKET_TITLES = { now: 'Now', next: 'Next', later: 'Later' };
|
|
8
9
|
const MODES = ['ask', 'routine', 'trusted'];
|
|
@@ -16,12 +17,17 @@ const oneLine = (s, max) => {
|
|
|
16
17
|
export function taskLabel(t, max = 80) {
|
|
17
18
|
return oneLine(t.title?.trim() || t.text, max);
|
|
18
19
|
}
|
|
20
|
+
/** Why a task waits to start, or what a working one does now: "Getting its computer ready", "Fixing the login cookie". */
|
|
21
|
+
function stateNote(t, max) {
|
|
22
|
+
const note = t.waiting?.message.trim() || (t.state === 'working' ? t.activity?.trim() : '') || '';
|
|
23
|
+
return note ? ` (${oneLine(note, max)})` : '';
|
|
24
|
+
}
|
|
19
25
|
/**
|
|
20
|
-
* One task as `shard board` lists it: id, state (with why it waits to start
|
|
21
|
-
* title or words (a follow-up ↳, a suggestion marked), project.
|
|
26
|
+
* One task as `shard board` lists it: id, state (with why it waits to start, "queued (Getting its computer ready)", or
|
|
27
|
+
* what it does now, "working (Fixing the login cookie)"), title or words (a follow-up ↳, a suggestion marked), project.
|
|
22
28
|
*/
|
|
23
29
|
export function taskLine(t) {
|
|
24
|
-
const state = `${t.state}${
|
|
30
|
+
const state = `${t.state}${stateNote(t, 60)}`;
|
|
25
31
|
const label = `${t.parent_task_id ? '↳ ' : ''}${taskLabel(t)}${t.suggested ? ' (suggested)' : ''}`;
|
|
26
32
|
return `${t.id}\t${state}\t${label}\t${t.project_id}${t.home ? ' (home)' : ''}`;
|
|
27
33
|
}
|
|
@@ -111,14 +117,14 @@ export function taskDetailText(d) {
|
|
|
111
117
|
const t = d.task;
|
|
112
118
|
const titled = Boolean(t.title?.trim());
|
|
113
119
|
const lines = [titled ? oneLine(t.title, 200) : oneLine(t.text.split('\n').find((l) => l.trim()) ?? t.text, 200)];
|
|
114
|
-
lines.push([`${t.state}${
|
|
120
|
+
lines.push([`${t.state}${stateNote(t, 120)}`, t.bucket, `${t.project_id}${t.home ? ' (home)' : ''}`, ...(t.suggested ? ['suggested'] : []), ...(t.deleted_at ? ['deleted'] : t.cleared_at ? ['cleared'] : [])].join('\t'));
|
|
115
121
|
// Your words exactly, when the first line does not already show them whole.
|
|
116
122
|
if (titled || t.text.trim() !== lines[0])
|
|
117
123
|
lines.push('', t.text);
|
|
118
124
|
if (t.plan) {
|
|
119
125
|
lines.push('', `Plan: ${t.plan.summary}`);
|
|
120
126
|
for (const s of t.plan.steps)
|
|
121
|
-
lines.push(` ${s.id}\t${s.state}\t${oneLine(s.text, 100)}${s.evidence.length ? `\t${s.evidence.join(', ')}` : ''}`);
|
|
127
|
+
lines.push(` ${s.id}\t${s.state}\t${oneLine(s.text, 100)}${s.now?.trim() ? `\t(${oneLine(s.now, 80)})` : ''}${s.evidence.length ? `\t${s.evidence.join(', ')}` : ''}`);
|
|
122
128
|
}
|
|
123
129
|
if (t.conversations.length)
|
|
124
130
|
lines.push('', `Conversations: ${t.conversations.map((c) => `${c.id} (${c.role}${c.step_id ? ` ${c.step_id}` : ''})`).join(', ')}`);
|
|
@@ -332,6 +338,11 @@ export const BOARD_HANDLERS = {
|
|
|
332
338
|
out(ctx, `Steered: ${taskLabel(task)}\n`, { task });
|
|
333
339
|
return 0;
|
|
334
340
|
},
|
|
341
|
+
async 'task land'(ctx, values, [id]) {
|
|
342
|
+
const o = await landTask(landDeps(ctx), id, { path: str(values.path) });
|
|
343
|
+
out(ctx, landedText(o), o);
|
|
344
|
+
return 0;
|
|
345
|
+
},
|
|
335
346
|
async 'task clear-done'(ctx, values) {
|
|
336
347
|
const cleared = await boardClient(ctx).tasks.clearDone(projectScope(values), { signal: ctx.signal });
|
|
337
348
|
out(ctx, cleared.length ? `Cleared ${cleared.length} finished task${cleared.length === 1 ? '' : 's'}.\n` : 'No finished tasks to clear.\n', { cleared });
|
|
@@ -345,6 +356,12 @@ export const BOARD_HANDLERS = {
|
|
|
345
356
|
async 'needs-you answer'(ctx, values, [id, word]) {
|
|
346
357
|
const input = { word, text: str(values.text), picks: strs(values.pick), keep: strs(values.keep), everywhere: values.everywhere === true };
|
|
347
358
|
const cloud = boardClient(ctx);
|
|
359
|
+
// Land merges here (shard task land) and records itself after; the board only takes keep and discard.
|
|
360
|
+
if (itemKind(id) === 'land' && word?.toLowerCase() === 'land') {
|
|
361
|
+
const o = await landTask(landDeps(ctx), id.replace(/^land_/, ''), {});
|
|
362
|
+
out(ctx, landedText(o), o);
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
348
365
|
// Checked first, so a wrong combination is a usage error before any request.
|
|
349
366
|
let answer = needsAnswer(id, input);
|
|
350
367
|
if (answerNeedsItem(id, input)) {
|
|
@@ -9,6 +9,7 @@ import { modelChoice } from "./agent-models.js";
|
|
|
9
9
|
import { storedTitlePrefix } from "./client-settings.js";
|
|
10
10
|
import { PtyOutputCursor, passiveUnavailable, sleepingAttach } from "./agent-terminal.js";
|
|
11
11
|
import { progress } from "./progress.js";
|
|
12
|
+
import { landConversation, landDeps, landedText } from "./land.js";
|
|
12
13
|
import { UUID } from "./args.js";
|
|
13
14
|
import { findOrg } from "./account-scope.js";
|
|
14
15
|
export function conversationClient(ctx) {
|
|
@@ -224,8 +225,9 @@ export const CONVERSATION_HANDLERS = {
|
|
|
224
225
|
const { client, organizationId } = await listScope(ctx, values);
|
|
225
226
|
let cursor = str(values.cursor);
|
|
226
227
|
for await (const c of client.conversations.watch({ projectId: str(values.project), organizationId, cursor, signal: ctx.signal, onCursor: next => { cursor = next; } })) {
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
// A deleted conversation comes once, its content cleared: its line says deleted (0.25.0+).
|
|
229
|
+
const q = questionLine(c), line = c.deleted_at ? `${c.id}\tDeleted conversation\tdeleted` : `${c.id}\t${displayTitle(c)}\t${c.state}${q ? `\t${q}` : ''}`;
|
|
230
|
+
ctx.io.stdout.write(ctx.json ? JSON.stringify({ conversation: c, cursor }) + '\n' : `${line}\n`);
|
|
229
231
|
}
|
|
230
232
|
return 0;
|
|
231
233
|
},
|
|
@@ -360,6 +362,12 @@ export const CONVERSATION_HANDLERS = {
|
|
|
360
362
|
async 'conversations land'(ctx, values, [id, action]) {
|
|
361
363
|
if (!['merge', 'pr', 'keep', 'discard'].includes(action))
|
|
362
364
|
throw new UsageError('Choose merge, pr, keep or discard.');
|
|
365
|
+
// merge lands for real (land.ts) and records only after the commit is in the branch; the others record a decision.
|
|
366
|
+
if (action === 'merge') {
|
|
367
|
+
const o = await landConversation(landDeps(ctx), id, { path: str(values.path) });
|
|
368
|
+
out(ctx, landedText(o), o);
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
363
371
|
const r = await conversationClient(ctx).conversations.land(id, action, { into: str(values.into), signal: ctx.signal });
|
|
364
372
|
out(ctx, `${displayTitle(r)}: ${r.landed.how}\n`, r);
|
|
365
373
|
return 0;
|
|
@@ -370,4 +378,12 @@ export const CONVERSATION_HANDLERS = {
|
|
|
370
378
|
return 0;
|
|
371
379
|
},
|
|
372
380
|
async 'conversations move'(ctx, _values, [id, to]) { const r = await conversationClient(ctx).conversations.move(id, to, { signal: ctx.signal }); out(ctx, `Move scheduled for the end of the turn.\n`, r); return 0; },
|
|
381
|
+
// Everywhere at once, its computer included (0.25.0+); refused without --yes, as every deletion is. --json: the API's answer.
|
|
382
|
+
async 'conversations delete'(ctx, values, [id]) {
|
|
383
|
+
if (values.yes !== true)
|
|
384
|
+
throw new UsageError('refusing to delete without --yes (the conversation, its computer, messages, transcript and branch are deleted everywhere)');
|
|
385
|
+
const r = await conversationClient(ctx).conversations.delete(id, { signal: ctx.signal });
|
|
386
|
+
out(ctx, `Deleted: ${r.id}\n`, r);
|
|
387
|
+
return 0;
|
|
388
|
+
},
|
|
373
389
|
};
|
package/dist/conversations.d.ts
CHANGED
|
@@ -17,7 +17,8 @@ export interface Conversation {
|
|
|
17
17
|
worktree_path: string;
|
|
18
18
|
branch: string;
|
|
19
19
|
base_commit: string;
|
|
20
|
-
|
|
20
|
+
/** `deleted`: the one feed delivery of a deleted conversation (0.25.0+). */
|
|
21
|
+
state: 'working' | 'needs_you' | 'unresolved' | 'done' | 'archived' | 'deleted';
|
|
21
22
|
created_at: string;
|
|
22
23
|
ended_at: string | null;
|
|
23
24
|
landed?: {
|
package/dist/device-folders.d.ts
CHANGED
|
@@ -42,6 +42,14 @@ type Starter = (ctx: Ctx, folder: string, harness: 'claude' | 'codex', brief: st
|
|
|
42
42
|
* prompt. The brief goes in on standard input, never in a command line another user can read.
|
|
43
43
|
*/
|
|
44
44
|
export declare const startFromFolder: Starter;
|
|
45
|
+
/**
|
|
46
|
+
* The conversation `shard agents new --json` started, from its whole output: one pretty-printed JSON document
|
|
47
|
+
* (format.ts json), never its last line (staging 2026-10-16: the last line is `}`).
|
|
48
|
+
*/
|
|
49
|
+
export declare function startedConversation(stdout: string): {
|
|
50
|
+
id: string;
|
|
51
|
+
project_id: string;
|
|
52
|
+
};
|
|
45
53
|
/**
|
|
46
54
|
* `folder` becomes one of your projects: the repository its Git origin names (else the folder itself), in your
|
|
47
55
|
* project on this API. It is selected for background preparation, so its files are in the cloud before the next chat,
|
package/dist/device-folders.js
CHANGED
|
@@ -148,10 +148,11 @@ export const startFromFolder = (ctx, folder, harness, brief) => {
|
|
|
148
148
|
env: { ...process.env, ...ctx.io.env, SHARDFLUX_CONFIG_DIR: ctx.store.dir, SHARDFLUX_API_URL: ctx.apiUrl, CI: '1' },
|
|
149
149
|
});
|
|
150
150
|
const out = [], err = [];
|
|
151
|
+
let outBytes = 0;
|
|
151
152
|
const stop = () => child.kill('SIGTERM');
|
|
152
153
|
const timer = setTimeout(stop, 300_000);
|
|
153
154
|
ctx.signal.addEventListener('abort', stop, { once: true });
|
|
154
|
-
child.stdout.on('data', (b) => { if (
|
|
155
|
+
child.stdout.on('data', (b) => { if ((outBytes += b.length) <= 8 << 20)
|
|
155
156
|
out.push(b); });
|
|
156
157
|
child.stderr.on('data', (b) => { if (err.length < 64)
|
|
157
158
|
err.push(b); });
|
|
@@ -163,11 +164,7 @@ export const startFromFolder = (ctx, folder, harness, brief) => {
|
|
|
163
164
|
try {
|
|
164
165
|
if (code !== 0)
|
|
165
166
|
throw new Error(lastLine(Buffer.concat(err).toString('utf8')) || `The conversation did not start (exit ${code}).`);
|
|
166
|
-
|
|
167
|
-
const id = started.conversation?.id ?? started.workspace?.labels?.shard_conversation;
|
|
168
|
-
if (!id)
|
|
169
|
-
throw new Error('The conversation did not start.');
|
|
170
|
-
resolveStart({ id, project_id: started.conversation?.project_id ?? started.workspace?.project_id ?? '' });
|
|
167
|
+
resolveStart(startedConversation(text));
|
|
171
168
|
}
|
|
172
169
|
catch (error) {
|
|
173
170
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -176,6 +173,17 @@ export const startFromFolder = (ctx, folder, harness, brief) => {
|
|
|
176
173
|
child.stdin.end(brief);
|
|
177
174
|
});
|
|
178
175
|
};
|
|
176
|
+
/**
|
|
177
|
+
* The conversation `shard agents new --json` started, from its whole output: one pretty-printed JSON document
|
|
178
|
+
* (format.ts json), never its last line (staging 2026-10-16: the last line is `}`).
|
|
179
|
+
*/
|
|
180
|
+
export function startedConversation(stdout) {
|
|
181
|
+
const started = JSON.parse(stdout);
|
|
182
|
+
const id = started.conversation?.id ?? started.workspace?.labels?.shard_conversation;
|
|
183
|
+
if (!id)
|
|
184
|
+
throw new Error('The conversation did not start.');
|
|
185
|
+
return { id, project_id: started.conversation?.project_id ?? started.workspace?.project_id ?? '' };
|
|
186
|
+
}
|
|
179
187
|
function lastLine(stderr) {
|
|
180
188
|
const text = stderr.trim();
|
|
181
189
|
try {
|
package/dist/device-helper.js
CHANGED
|
@@ -150,7 +150,18 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
150
150
|
// Registered for every project an agent may ask from (deviceProjects); one poll covers them all where the API
|
|
151
151
|
// lists by device (0.25.0+), else one per project.
|
|
152
152
|
const registered = new Map();
|
|
153
|
-
let all, deviceWide = true;
|
|
153
|
+
let all, deviceWide = true, links = true;
|
|
154
|
+
// This device's local conversations of a project (started here or opened here): their requests come here first.
|
|
155
|
+
const linked = (projectId) => Object.values(localConversations(ctx)).filter(r => !r.handed_off && (!projectId || r.project_id === projectId))
|
|
156
|
+
.sort((a, b) => (b.conversation.created_at ?? '').localeCompare(a.conversation.created_at ?? '')).slice(0, 500).map(r => r.conversation.id);
|
|
157
|
+
const beat = (projectId) => conversationClient(ctx).deviceRequests.heartbeat({ ...(projectId ? { projectId } : {}), deviceId, leaseSeconds: 30, ...(links ? { conversationIds: linked(projectId) } : {}) })
|
|
158
|
+
.catch((e) => {
|
|
159
|
+
// An API before device links (0.25.0+ clients) refuses the list: register without it from then on.
|
|
160
|
+
if (!links || !(e instanceof ShardfluxApiError && [400, 422].includes(e.status)))
|
|
161
|
+
throw e;
|
|
162
|
+
links = false;
|
|
163
|
+
return conversationClient(ctx).deviceRequests.heartbeat({ ...(projectId ? { projectId } : {}), deviceId, leaseSeconds: 30 });
|
|
164
|
+
});
|
|
154
165
|
const take = (rows) => {
|
|
155
166
|
for (const r of rows) {
|
|
156
167
|
if (r.device_id !== deviceId || working.has(r.id) || working.size >= 8)
|
|
@@ -167,7 +178,7 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
167
178
|
for (const projectId of projects) {
|
|
168
179
|
if (Date.now() - (registered.get(projectId) ?? 0) < 10_000)
|
|
169
180
|
continue;
|
|
170
|
-
await
|
|
181
|
+
await beat(projectId).then(() => registered.set(projectId, Date.now()), (e) => {
|
|
171
182
|
// A project this sign-in no longer reaches is skipped until the next listing, never the others.
|
|
172
183
|
if (!(e instanceof ShardfluxApiError && [403, 404].includes(e.status)))
|
|
173
184
|
throw e;
|
package/dist/errors.js
CHANGED
|
@@ -266,7 +266,8 @@ export function describeError(err) {
|
|
|
266
266
|
// details.reason: the operation error's reason (e.g. the scheduler's no_ready_hosts), printed after the message.
|
|
267
267
|
const opDetails = err.operation.error?.details;
|
|
268
268
|
const reason = typeof opDetails === 'object' && opDetails !== null ? opDetails.reason : undefined;
|
|
269
|
-
const
|
|
269
|
+
const retryAfter = typeof opDetails === 'object' && opDetails !== null ? opDetails.retry_after_seconds : undefined;
|
|
270
|
+
const details = { ...(err.errorCode ? { error_code: err.errorCode } : {}), ...(typeof reason === 'string' ? { reason } : {}), ...(typeof retryAfter === 'number' && Number.isFinite(retryAfter) && retryAfter > 0 ? { retry_after_seconds: retryAfter } : {}) };
|
|
270
271
|
// A suspend-when-idle canceled workspace_active (0.7.0+): the workspace was in use and keeps running.
|
|
271
272
|
const message = err.workspaceActive ? `${err.message}: the workspace was in use, so the suspend-when-idle was canceled; it keeps running` : err.message;
|
|
272
273
|
return { code: 'operation_failed', message, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}), operation: err.operation };
|
package/dist/format.js
CHANGED
|
@@ -448,7 +448,7 @@ const hoursText = (h) => String(Math.round(h * 100) / 100);
|
|
|
448
448
|
* while the GPU is stopped (cap_state gpu_blocked), why and what brings it back. Workspaces keep running meanwhile.
|
|
449
449
|
* Nothing without `gpu` (plans without GPU-hours).
|
|
450
450
|
*/
|
|
451
|
-
function gpuLines(gpu, resetsAt) {
|
|
451
|
+
function gpuLines(gpu, resetsAt, overageAvailable) {
|
|
452
452
|
if (!isObject(gpu) || typeof gpu.used_hours !== 'number')
|
|
453
453
|
return [];
|
|
454
454
|
const included = typeof gpu.included_hours === 'number' ? gpu.included_hours : null;
|
|
@@ -470,6 +470,10 @@ function gpuLines(gpu, resetsAt) {
|
|
|
470
470
|
rows.push(['GPU', `stopped (spend_cap_reached): workspaces keep running; raise the spend cap (shard billing overage cap <dollars>), or wait for the reset${resets}`]);
|
|
471
471
|
break;
|
|
472
472
|
default:
|
|
473
|
+
if (!overageAvailable) {
|
|
474
|
+
rows.push(['GPU', 'stopped (allowance_used): workspaces keep running; choose a paid plan (shard billing plans); overage is unavailable on the current plan']);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
473
477
|
rows.push(['GPU', `stopped (allowance_used): workspaces keep running; turn on overage (shard billing overage on --cap <dollars>), upgrade (shard billing plans), or wait for the reset${resets}`]);
|
|
474
478
|
}
|
|
475
479
|
return rows;
|
|
@@ -507,7 +511,7 @@ export function usageText(summary) {
|
|
|
507
511
|
head.push(['retained state', 'used up: new workspaces and forks are refused (delete workspaces you no longer need, or upgrade)']);
|
|
508
512
|
}
|
|
509
513
|
const resetsAt = isObject(s.period) ? [s.period.resets_at, s.period.end].find((t) => typeof t === 'string') : undefined;
|
|
510
|
-
head.push(...gpuLines(s.gpu, resetsAt));
|
|
514
|
+
head.push(...gpuLines(s.gpu, resetsAt, !(isObject(s.spend_cap) && s.spend_cap.available === false)));
|
|
511
515
|
for (const [k, v] of Object.entries(s))
|
|
512
516
|
if (!USAGE_KNOWN.has(k) && (typeof v !== 'object' || v === null))
|
|
513
517
|
head.push([k.replace(/_/g, ' '), scalar(v)]);
|
package/dist/handlers.js
CHANGED
|
@@ -5,6 +5,7 @@ import { CONVERSATION_HANDLERS } from "./conversation-commands.js";
|
|
|
5
5
|
import { BOARD_HANDLERS } from "./board-commands.js";
|
|
6
6
|
import { TRANSCRIPT_HANDLERS } from "./conversation-transcript.js";
|
|
7
7
|
import { ROUTER_HANDLERS } from "./router-commands.js";
|
|
8
|
+
import { TRANSCRIBE_HANDLERS } from "./transcribe-commands.js";
|
|
8
9
|
/**
|
|
9
10
|
* Every command's handler, keyed by its path ("workspaces open", "auth login"): the project-key commands of
|
|
10
11
|
* commands.ts and the account plane of account-commands.ts (0.5.0). args.test.ts checks that it matches COMMANDS.
|
|
@@ -31,4 +32,4 @@ import { MACHINE_SETUP_HANDLERS } from "./machine-setup.js";
|
|
|
31
32
|
import { SETTINGS_HANDLERS } from "./client-settings.js";
|
|
32
33
|
import { PROJECT_PREPARATION_HANDLERS } from "./project-preparation.js";
|
|
33
34
|
import { DRIVE_HANDLERS } from "./drive-commands.js";
|
|
34
|
-
export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...PROJECT_LINK_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...BOARD_HANDLERS, ...TRANSCRIPT_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...MACHINE_SETUP_HANDLERS, ...SETTINGS_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS, ...ROUTER_HANDLERS, ...DRIVE_HANDLERS };
|
|
35
|
+
export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...PROJECT_LINK_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...BOARD_HANDLERS, ...TRANSCRIPT_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...MACHINE_SETUP_HANDLERS, ...SETTINGS_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS, ...ROUTER_HANDLERS, ...TRANSCRIBE_HANDLERS, ...DRIVE_HANDLERS };
|