@shardflux/cli 0.18.0 → 0.19.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 +96 -1
- package/README.md +28 -1
- package/dist/account-args.js +1 -0
- package/dist/agent-commands.d.ts +1 -0
- package/dist/agent-commands.js +15 -5
- package/dist/agent-config-files.d.ts +16 -2
- package/dist/agent-config-files.js +48 -61
- package/dist/agent-config-sync.d.ts +12 -1
- package/dist/agent-config-sync.js +228 -54
- package/dist/agent-repository.d.ts +39 -9
- package/dist/agent-repository.js +309 -92
- package/dist/agent-state.d.ts +3 -0
- package/dist/agent-state.js +13 -0
- package/dist/agent-target-commands.d.ts +2 -0
- package/dist/agent-target-commands.js +55 -18
- package/dist/args.d.ts +1 -1
- package/dist/args.js +16 -4
- package/dist/command-center-actions.js +31 -5
- package/dist/conversation-commands.js +6 -1
- package/dist/conversation-router.js +7 -11
- package/dist/conversation-stash.js +64 -18
- package/dist/conversation-terminal.d.ts +11 -1
- package/dist/conversation-terminal.js +28 -7
- package/dist/conversations.d.ts +17 -3
- package/dist/conversations.js +58 -6
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/files-mirror.d.ts +1 -9
- package/dist/files-mirror.js +19 -3
- package/dist/format.js +37 -2
- package/dist/handlers.js +3 -1
- package/dist/local-sessions.d.ts +60 -0
- package/dist/local-sessions.js +445 -0
- package/dist/main.js +12 -0
- package/dist/move.d.ts +44 -0
- package/dist/move.js +248 -0
- package/dist/moved-sessions.d.ts +33 -0
- package/dist/moved-sessions.js +91 -0
- package/dist/onboarding.d.ts +18 -3
- package/dist/onboarding.js +72 -46
- package/dist/project-sync-manifest.d.ts +31 -3
- package/dist/project-sync-manifest.js +192 -108
- package/dist/project-sync-transfer.d.ts +50 -4
- package/dist/project-sync-transfer.js +404 -106
- package/dist/project-sync.js +30 -33
- package/dist/seed-phases.d.ts +29 -0
- package/dist/seed-phases.js +83 -0
- package/dist/t3-launch.js +3 -1
- package/dist/t3-mode.d.ts +7 -2
- package/dist/t3-mode.js +39 -5
- package/dist/thread-on-computer.js +4 -0
- package/dist/worktree-twin.d.ts +2 -6
- package/dist/worktree-twin.js +16 -51
- package/package.json +5 -5
package/dist/conversations.js
CHANGED
|
@@ -4,11 +4,13 @@ import { AgentStateStore } from "./agent-state.js";
|
|
|
4
4
|
import { gitRunner } from "./agent-git.js";
|
|
5
5
|
import { projectIdentity } from "./project-identity.js";
|
|
6
6
|
import { ensureProjectGit } from "./project-git.js";
|
|
7
|
-
import { seedAgentRepository } from "./agent-repository.js";
|
|
7
|
+
import { preflightProjectSeed, seedAgentRepository } from "./agent-repository.js";
|
|
8
8
|
import { UsageError } from "./errors.js";
|
|
9
9
|
import { conversationClient } from "./conversation-commands.js";
|
|
10
10
|
import { recordSeedSnapshot } from "./files-mirror.js";
|
|
11
11
|
import { rememberTwinSeed, sendTwinSeed, twinCheckoutArgv, twinSeed } from "./worktree-twin.js";
|
|
12
|
+
import { progress } from "./progress.js";
|
|
13
|
+
import { seedPhase, startProgress } from "./seed-phases.js";
|
|
12
14
|
/**
|
|
13
15
|
* Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
|
|
14
16
|
* while its VM still takes the next turn; on Codex Cloud, `done` ends the conversation.
|
|
@@ -20,6 +22,37 @@ export const conversationName = (c) => {
|
|
|
20
22
|
throw new Error('Conversation returned an invalid branch or name');
|
|
21
23
|
return name;
|
|
22
24
|
};
|
|
25
|
+
/** The guest's managed PTY id: `shard-<name>-<app>` (services/guest-agent/internal/agents PtyID). */
|
|
26
|
+
const MANAGED_PTY = /^shard-[a-z0-9][a-z0-9._-]{0,63}-(claude|codex)$/;
|
|
27
|
+
/**
|
|
28
|
+
* The managed PTY a conversation's harness runs in (0.18.1+). The guest records it per conversation
|
|
29
|
+
* (sf-agent's conversation runtime record), which also keeps the PTY a fork inherited from its parent; without a
|
|
30
|
+
* record it is the one the guest names after the conversation, `shard-<name>-<harness>`. Never the VM key's name: a
|
|
31
|
+
* conversation's VM is `<project>/conv-<name>`.
|
|
32
|
+
*/
|
|
33
|
+
export async function conversationPtyId(cell, c, signal) {
|
|
34
|
+
const derived = `shard-${conversationName(c)}-${c.harness}`;
|
|
35
|
+
let stdout;
|
|
36
|
+
try {
|
|
37
|
+
const r = await cell.exec.run(['cat', `/home/user/.local/share/shardflux/conversation-runtime/${c.id}.json`], { timeoutMs: 10_000, maxOutputBytes: 4096, ...(signal ? { signal } : {}) });
|
|
38
|
+
if (r.exitCode !== 0)
|
|
39
|
+
return derived;
|
|
40
|
+
stdout = r.stdout;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (signal?.aborted)
|
|
44
|
+
throw error;
|
|
45
|
+
return derived;
|
|
46
|
+
}
|
|
47
|
+
let recorded;
|
|
48
|
+
try {
|
|
49
|
+
recorded = JSON.parse(stdout)?.pty_id;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return derived;
|
|
53
|
+
}
|
|
54
|
+
return typeof recorded === 'string' && MANAGED_PTY.test(recorded) && recorded.endsWith(`-${c.harness}`) ? recorded : derived;
|
|
55
|
+
}
|
|
23
56
|
export function validateConversation(c) {
|
|
24
57
|
if (!c || !/^conv_[a-z0-9]{26}$/.test(c.id) || !c.project_id || !c.workspace_id || !c.worktree_path?.startsWith('/home/user/') || /[\0\r\n]/.test(c.worktree_path) || !['claude', 'codex'].includes(c.harness))
|
|
25
58
|
throw new Error('Conversation response omitted its identity or binding');
|
|
@@ -120,10 +153,15 @@ export async function restoreConversationBinding(ctx, c, ws) {
|
|
|
120
153
|
const result = await cell.exec.run(['sf-agent', 'conversation', 'restore', '--args', '{"start":false}'], { cwd: c.worktree_path, env: { SF_CONVERSATION_ID: c.id }, timeoutMs: 120_000, signal: ctx.signal });
|
|
121
154
|
if (result.exitCode !== 0)
|
|
122
155
|
throw new Error(`Conversation restore failed: ${result.stderr.trim()}`);
|
|
156
|
+
// A conversation whose VM was removed now runs in a fresh one, from its saved branch and transcript (0.18.1+).
|
|
157
|
+
if (c.restore.kind === 'resume')
|
|
158
|
+
progress(ctx, 'Restored');
|
|
123
159
|
}
|
|
124
|
-
export async function createConversationRecord(ctx, folder, harness, route, name, allocated, idempotencyKey = randomUUID(), choice = {}) {
|
|
160
|
+
export async function createConversationRecord(ctx, folder, harness, route, name, allocated, idempotencyKey = randomUUID(), choice = {}, movedIn) {
|
|
125
161
|
const project = projectIdentity(folder);
|
|
126
162
|
await ensureProjectGit(ctx, project.folder);
|
|
163
|
+
// A start refuses before a VM is allocated, never after (0.19.0+).
|
|
164
|
+
await preflightProjectSeed(project.folder, ctx.signal);
|
|
127
165
|
// Both the conversation API and its legacy workspace fallback need a usable local HEAD.
|
|
128
166
|
await conversationClient(ctx).conversations.list({ limit: 1 });
|
|
129
167
|
const git = gitRunner(project.folder, { ...process.env, ...ctx.io.env }, ctx.signal);
|
|
@@ -131,32 +169,46 @@ export async function createConversationRecord(ctx, folder, harness, route, name
|
|
|
131
169
|
const branch = await git(['symbolic-ref', '--quiet', '--short', 'HEAD']).catch(() => undefined);
|
|
132
170
|
// Uncommitted edits ride along as the twin snapshot the conversation starts from (worktree-twin.ts).
|
|
133
171
|
const seed = await twinSeed(ctx.store.dir, project.folder, head, ctx.store.profile(ctx.apiUrl).project_id).catch(() => ({ commit: head, snapshot: false }));
|
|
172
|
+
seedPhase('local_snapshot');
|
|
134
173
|
const commit = seed.commit;
|
|
135
174
|
const response = await conversationClient(ctx).conversations.create({ repoRemote: project.remote ?? `file://${project.folder}`, harness, route, mode: 'new',
|
|
136
|
-
base: { commit, ...(branch ? { branch } : {}) }, ...(name ? { name } : {}), ...choice, projectId: ctx.store.profile(ctx.apiUrl).project_id
|
|
175
|
+
base: { commit, ...(branch ? { branch } : {}) }, ...(name ? { name } : {}), ...choice, projectId: ctx.store.profile(ctx.apiUrl).project_id,
|
|
176
|
+
...(movedIn ? { harnessSessionId: movedIn.session_id, ...(movedIn.title ? { title: movedIn.title.slice(0, 200) } : {}) } : {}) }, { idempotencyKey, signal: ctx.signal });
|
|
137
177
|
const c = validateConversation({ ...response.conversation, restore: response.restore });
|
|
138
178
|
allocated?.(response.workspace);
|
|
139
179
|
rememberTwinSeed(ctx.store.dir, c.id, project.folder, seed, branch);
|
|
140
180
|
rememberConversation(ctx, c, project.folder, false, branch);
|
|
141
181
|
return c;
|
|
142
182
|
}
|
|
143
|
-
export async function createConversation(ctx, folder, harness, route, name, choice = {}) {
|
|
183
|
+
export async function createConversation(ctx, folder, harness, route, name, choice = {}, movedIn) {
|
|
144
184
|
const project = projectIdentity(folder);
|
|
145
|
-
|
|
185
|
+
seedPhase('start');
|
|
186
|
+
// The launcher's phase line (SHARDFLUX_PROGRESS_FILE, 0.19.0+): a computer starts, then the seed's own marks move it on.
|
|
187
|
+
startProgress(harness, ctx.io.env);
|
|
188
|
+
const c = await createConversationRecord(ctx, project.folder, harness, route, name, undefined, undefined, choice, movedIn);
|
|
189
|
+
seedPhase('conversation_create');
|
|
146
190
|
try {
|
|
147
191
|
const ws = await conversationWorkspace(ctx, c);
|
|
192
|
+
seedPhase('vm_running');
|
|
193
|
+
// A moved-in session's transcript is in place before the repository: its guest resumes it once the seed is done.
|
|
194
|
+
await movedIn?.beforeSeed(ws, c);
|
|
148
195
|
// This is a one-time seed. A conversation never watches or writes the owner's checkout.
|
|
149
196
|
await seedAgentRepository(ctx, ws, project.folder, { root: c.worktree_path, populate: true });
|
|
150
197
|
const cell = ws.cell();
|
|
198
|
+
seedPhase('seed_done');
|
|
151
199
|
await sendTwinSeed(ctx.store.dir, cell, c, project.folder, ctx.signal);
|
|
200
|
+
seedPhase('twin_seed');
|
|
152
201
|
const result = await cell.exec.run(twinCheckoutArgv(ctx.store.dir, c), { timeoutMs: 30_000, signal: ctx.signal });
|
|
153
202
|
if (result.exitCode !== 0)
|
|
154
203
|
throw new Error(`Conversation branch could not be prepared: ${result.stderr.trim()}`);
|
|
204
|
+
seedPhase('branch_checkout');
|
|
155
205
|
await recordSeedSnapshot(ws, c, ctx.signal);
|
|
206
|
+
seedPhase('seed_snapshot');
|
|
156
207
|
return { conversation: c, workspace: ws };
|
|
157
208
|
}
|
|
158
209
|
catch (error) {
|
|
159
|
-
|
|
210
|
+
// A failed move leaves nothing behind: its session can move again (the API counts only a live VM's move).
|
|
211
|
+
if (route === 'codex-cloud' || movedIn) {
|
|
160
212
|
try {
|
|
161
213
|
await ctx.cloud().workspaces.delete(c.workspace_id);
|
|
162
214
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export declare class GitError extends HintedError {
|
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* A refusal `shard` detects itself, with a stable code, its facts in `details` and the next step as `hint` (exit 1),
|
|
40
|
-
* e.g. `
|
|
40
|
+
* e.g. `t3_not_installed` (0.17.0+) when T3 Code is missing.
|
|
41
41
|
*/
|
|
42
42
|
export declare class CodedError extends HintedError {
|
|
43
43
|
name: string;
|
package/dist/errors.js
CHANGED
|
@@ -54,7 +54,7 @@ export class GitError extends HintedError {
|
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* A refusal `shard` detects itself, with a stable code, its facts in `details` and the next step as `hint` (exit 1),
|
|
57
|
-
* e.g. `
|
|
57
|
+
* e.g. `t3_not_installed` (0.17.0+) when T3 Code is missing.
|
|
58
58
|
*/
|
|
59
59
|
export class CodedError extends HintedError {
|
|
60
60
|
name = 'CodedError';
|
package/dist/files-mirror.d.ts
CHANGED
|
@@ -146,15 +146,7 @@ export declare class FilesMirror {
|
|
|
146
146
|
already: boolean;
|
|
147
147
|
}>;
|
|
148
148
|
}
|
|
149
|
-
|
|
150
|
-
* The seed snapshot (sh; $1 repository, $2 conversation name, $3 hub): HEAD plus the working tree the conversation was
|
|
151
|
-
* seeded with, committed in the conversation's VM with a temporary index (never on this computer), kept per worktree
|
|
152
|
-
* as refs/worktree/shard/seed and in the hub as refs/shard/seed/<name>. Mirrors leave out what it already contains,
|
|
153
|
-
* so the user's own uncommitted edits never show as the conversation's change. Threads on one computer are worktrees
|
|
154
|
-
* of one repository (0.16.0+), so the seed is per worktree; the shared refs/shard/seed of earlier seeds is read, and
|
|
155
|
-
* still written, only in the repository's main worktree.
|
|
156
|
-
*/
|
|
157
|
-
export declare const SEED_SNAPSHOT = "set -eu\ncd \"$1\"\nmain=0; [ \"$(git rev-parse --absolute-git-dir)\" = \"$(git rev-parse --path-format=absolute --git-common-dir)\" ] && main=1\nif git rev-parse -q --verify refs/worktree/shard/seed >/dev/null; then seed=$(git rev-parse refs/worktree/shard/seed)\nelif [ \"$main\" = 1 ] && git rev-parse -q --verify refs/shard/seed >/dev/null; then seed=$(git rev-parse refs/shard/seed); else\nidx=$(mktemp); trap 'rm -f \"$idx\"' EXIT\ncp \"$(git rev-parse --path-format=absolute --git-path index)\" \"$idx\" 2>/dev/null || { rm -f \"$idx\"; GIT_INDEX_FILE=\"$idx\" git read-tree HEAD; }\nGIT_INDEX_FILE=\"$idx\" git add -A\ntree=$(GIT_INDEX_FILE=\"$idx\" git write-tree)\nif [ \"$tree\" = \"$(git rev-parse 'HEAD^{tree}')\" ]; then seed=$(git rev-parse HEAD); else\nseed=$(GIT_AUTHOR_NAME=Shardflux GIT_AUTHOR_EMAIL=seed@shardflux.dev GIT_COMMITTER_NAME=Shardflux GIT_COMMITTER_EMAIL=seed@shardflux.dev git commit-tree \"$tree\" -p HEAD -m \"Shardflux: the working tree this conversation started from\"); fi\ngit update-ref refs/worktree/shard/seed \"$seed\"\nif [ \"$main\" = 1 ]; then git update-ref refs/shard/seed \"$seed\"; fi\nfi\nif [ -d \"$3\" ]; then git -c core.hooksPath=/dev/null push -q \"$3\" \"+$seed:refs/shard/seed/$2\" >/dev/null 2>&1 || true; fi\nprintf '%s\n' \"$seed\"\n";
|
|
149
|
+
export declare const SEED_SNAPSHOT = "set -eu\ncd \"$1\"\nmain=0; [ \"$(git rev-parse --absolute-git-dir)\" = \"$(git rev-parse --path-format=absolute --git-common-dir)\" ] && main=1\nif git rev-parse -q --verify refs/worktree/shard/seed >/dev/null; then seed=$(git rev-parse refs/worktree/shard/seed)\nelif [ \"$main\" = 1 ] && git rev-parse -q --verify refs/shard/seed >/dev/null; then seed=$(git rev-parse refs/shard/seed); else\nidx=$(mktemp); trap 'rm -f \"$idx\"' EXIT\ncp \"$(git rev-parse --path-format=absolute --git-path index)\" \"$idx\" 2>/dev/null || { rm -f \"$idx\"; GIT_INDEX_FILE=\"$idx\" git read-tree HEAD; }\nGIT_INDEX_FILE=\"$idx\" git add -A\ntree=$(GIT_INDEX_FILE=\"$idx\" git write-tree)\nif [ \"$tree\" = \"$(git rev-parse 'HEAD^{tree}')\" ]; then seed=$(git rev-parse HEAD); else\nseed=$(GIT_AUTHOR_NAME=Shardflux GIT_AUTHOR_EMAIL=seed@shardflux.dev GIT_COMMITTER_NAME=Shardflux GIT_COMMITTER_EMAIL=seed@shardflux.dev git commit-tree \"$tree\" -p HEAD -m \"Shardflux: the working tree this conversation started from\"); fi\ngit update-ref refs/worktree/shard/seed \"$seed\"\nif [ \"$main\" = 1 ]; then git update-ref refs/shard/seed \"$seed\"; fi\nfi\npush='set -u\nseed=$1; name=$2; hub=$3; cd \"$4\"\npush=1; if [ \"$(git config --bool remote.origin.promisor 2>/dev/null || true)\" = true ]; then push=0\nfor c in \"$seed\" \"$seed^1\"; do o=$(git rev-parse -q --verify \"$c^{commit}\" 2>/dev/null) && git --git-dir=\"$hub\" cat-file -e \"$o^{commit}\" 2>/dev/null && push=1 && break; done; fi\nif [ -d \"$hub\" ] && [ \"$push\" = 1 ]; then git -c core.hooksPath=/dev/null push -q \"$hub\" \"+$seed:refs/shard/seed/$name\" >/dev/null 2>&1 || true; fi\n'\nif [ \"${4:-}\" = background ]; then\n if command -v setsid >/dev/null 2>&1; then (setsid sh -c \"$push\" shard-seed-push \"$seed\" \"$2\" \"$3\" \"$PWD\" >/dev/null 2>&1 </dev/null &)\n else (sh -c \"$push\" shard-seed-push \"$seed\" \"$2\" \"$3\" \"$PWD\" >/dev/null 2>&1 </dev/null &); fi\nelse sh -c \"$push\" shard-seed-push \"$seed\" \"$2\" \"$3\" \"$PWD\"; fi\nprintf '%s\n' \"$seed\"\n";
|
|
158
150
|
/** Best effort: without a snapshot the folder also lists the start state's own edits. */
|
|
159
151
|
export declare function recordSeedSnapshot(ws: Pick<Workspace, 'cell'>, c: Pick<Conversation, 'worktree_path' | 'branch'>, signal?: AbortSignal): Promise<string | null>;
|
|
160
152
|
/** A ready VM of the project (never woken), else the project's space reader, woken when needed. */
|
package/dist/files-mirror.js
CHANGED
|
@@ -675,8 +675,19 @@ export class FilesMirror {
|
|
|
675
675
|
* as refs/worktree/shard/seed and in the hub as refs/shard/seed/<name>. Mirrors leave out what it already contains,
|
|
676
676
|
* so the user's own uncommitted edits never show as the conversation's change. Threads on one computer are worktrees
|
|
677
677
|
* of one repository (0.16.0+), so the seed is per worktree; the shared refs/shard/seed of earlier seeds is read, and
|
|
678
|
-
* still written, only in the repository's main worktree.
|
|
678
|
+
* still written, only in the repository's main worktree. With $4 = background (0.19.1+) the hub push runs detached
|
|
679
|
+
* in the VM and the script returns once the seed is recorded: the hub is on the project's network volume, where the
|
|
680
|
+
* push took 6-10 s on every start (staging, coding 37), on the way to the first prompt.
|
|
679
681
|
*/
|
|
682
|
+
// It is embedded in single quotes below: never a single quote in it. A blobless partial clone (0.19.0+) pushes only
|
|
683
|
+
// when the hub already has the seed history; otherwise the push would fetch every historic blob first. Its background
|
|
684
|
+
// backfill completes the repository for later pushes.
|
|
685
|
+
const SEED_PUSH = `set -u
|
|
686
|
+
seed=$1; name=$2; hub=$3; cd "$4"
|
|
687
|
+
push=1; if [ "$(git config --bool remote.origin.promisor 2>/dev/null || true)" = true ]; then push=0
|
|
688
|
+
for c in "$seed" "$seed^1"; do o=$(git rev-parse -q --verify "$c^{commit}" 2>/dev/null) && git --git-dir="$hub" cat-file -e "$o^{commit}" 2>/dev/null && push=1 && break; done; fi
|
|
689
|
+
if [ -d "$hub" ] && [ "$push" = 1 ]; then git -c core.hooksPath=/dev/null push -q "$hub" "+$seed:refs/shard/seed/$name" >/dev/null 2>&1 || true; fi
|
|
690
|
+
`;
|
|
680
691
|
export const SEED_SNAPSHOT = `set -eu
|
|
681
692
|
cd "$1"
|
|
682
693
|
main=0; [ "$(git rev-parse --absolute-git-dir)" = "$(git rev-parse --path-format=absolute --git-common-dir)" ] && main=1
|
|
@@ -691,13 +702,18 @@ seed=$(GIT_AUTHOR_NAME=Shardflux GIT_AUTHOR_EMAIL=seed@shardflux.dev GIT_COMMITT
|
|
|
691
702
|
git update-ref refs/worktree/shard/seed "$seed"
|
|
692
703
|
if [ "$main" = 1 ]; then git update-ref refs/shard/seed "$seed"; fi
|
|
693
704
|
fi
|
|
694
|
-
|
|
705
|
+
push='${SEED_PUSH}'
|
|
706
|
+
if [ "\${4:-}" = background ]; then
|
|
707
|
+
if command -v setsid >/dev/null 2>&1; then (setsid sh -c "$push" shard-seed-push "$seed" "$2" "$3" "$PWD" >/dev/null 2>&1 </dev/null &)
|
|
708
|
+
else (sh -c "$push" shard-seed-push "$seed" "$2" "$3" "$PWD" >/dev/null 2>&1 </dev/null &); fi
|
|
709
|
+
else sh -c "$push" shard-seed-push "$seed" "$2" "$3" "$PWD"; fi
|
|
695
710
|
printf '%s\n' "$seed"
|
|
696
711
|
`;
|
|
697
712
|
/** Best effort: without a snapshot the folder also lists the start state's own edits. */
|
|
698
713
|
export async function recordSeedSnapshot(ws, c, signal) {
|
|
699
714
|
try {
|
|
700
|
-
|
|
715
|
+
// The hub push runs detached in the VM: it is on the project volume and never on the way to the first prompt.
|
|
716
|
+
const r = await ws.cell().exec.run(['sh', '-c', SEED_SNAPSHOT, 'shard-seed', c.worktree_path, c.branch.replace(/^shard\//, ''), HUB_PATH, 'background'], { timeoutMs: 120_000, maxOutputBytes: 4096, ...(signal ? { signal } : {}) });
|
|
701
717
|
return r.exitCode === 0 ? r.stdout.trim() : null;
|
|
702
718
|
}
|
|
703
719
|
catch (error) {
|
package/dist/format.js
CHANGED
|
@@ -376,7 +376,7 @@ function objectTable(rows, columns) {
|
|
|
376
376
|
const cols = columns.filter(([k]) => objs.some((o) => k in o));
|
|
377
377
|
return table(cols.map(([, h]) => h), objs.map((o) => cols.map(([k]) => (k === 'percent_used' && typeof o[k] === 'number' ? `${o[k]}%` : scalar(o[k])))));
|
|
378
378
|
}
|
|
379
|
-
const USAGE_KNOWN = new Set(['plan', 'period', 'measurement', 'allowance_exhausted', 'exhausted_reason', 'egress_override', 'meters', 'allowances', 'exhausted', 'alert_thresholds', 'spend_cap']);
|
|
379
|
+
const USAGE_KNOWN = new Set(['plan', 'period', 'measurement', 'allowance_exhausted', 'exhausted_reason', 'egress_override', 'meters', 'allowances', 'exhausted', 'alert_thresholds', 'spend_cap', 'gpu']);
|
|
380
380
|
/** Minor units as money (the overage block): `$3.00` for usd, else `3.00 EUR`. A fractional minor amount (a sub-cent rate) keeps its digits. */
|
|
381
381
|
export function capMoney(minor, currency) {
|
|
382
382
|
if (typeof minor !== 'number')
|
|
@@ -432,6 +432,39 @@ function spendCapText(cap) {
|
|
|
432
432
|
}
|
|
433
433
|
return out;
|
|
434
434
|
}
|
|
435
|
+
/** Hours for people: at most two decimals (`25`, `3.25`, `0.2`). */
|
|
436
|
+
const hoursText = (h) => String(Math.round(h * 100) / 100);
|
|
437
|
+
/**
|
|
438
|
+
* The GPU-hours lines of the summary (0.19.0; `gpu`, SDK 0.25.0): used of included with the overage past them, then,
|
|
439
|
+
* while the GPU is stopped (cap_state gpu_blocked), why and what brings it back. Workspaces keep running meanwhile.
|
|
440
|
+
* Nothing without `gpu` (plans without GPU-hours).
|
|
441
|
+
*/
|
|
442
|
+
function gpuLines(gpu, resetsAt) {
|
|
443
|
+
if (!isObject(gpu) || typeof gpu.used_hours !== 'number')
|
|
444
|
+
return [];
|
|
445
|
+
const included = typeof gpu.included_hours === 'number' ? gpu.included_hours : null;
|
|
446
|
+
let used = included === null ? `${hoursText(gpu.used_hours)} used (the plan includes none)` : `${hoursText(gpu.used_hours)} of ${hoursText(included)} used`;
|
|
447
|
+
const over = typeof gpu.overage_hours === 'number' ? gpu.overage_hours : 0;
|
|
448
|
+
if (included !== null && over > 0) {
|
|
449
|
+
const billed = typeof gpu.billed_overage_hours === 'number' ? gpu.billed_overage_hours : 0;
|
|
450
|
+
used += `, ${hoursText(over)} past the allowance${billed > 0 ? ` (${hoursText(billed)} billed as overage)` : ''}`;
|
|
451
|
+
}
|
|
452
|
+
const rows = [['GPU-hours', used]];
|
|
453
|
+
if (gpu.cap_state !== 'gpu_blocked')
|
|
454
|
+
return rows;
|
|
455
|
+
const resets = resetsAt ? ` (the period resets ${time(resetsAt)})` : '';
|
|
456
|
+
switch (gpu.blocked_reason) {
|
|
457
|
+
case 'overage_paused':
|
|
458
|
+
rows.push(['GPU', 'stopped (overage_paused): workspaces keep running; pay the past-due invoice (shard billing portal --open) and the GPU renders again']);
|
|
459
|
+
break;
|
|
460
|
+
case 'spend_cap_reached':
|
|
461
|
+
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}`]);
|
|
462
|
+
break;
|
|
463
|
+
default:
|
|
464
|
+
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}`]);
|
|
465
|
+
}
|
|
466
|
+
return rows;
|
|
467
|
+
}
|
|
435
468
|
/**
|
|
436
469
|
* The usage summary (GET /v1/organizations/{id}/usage/summary): plan, period, measurement
|
|
437
470
|
* freshness, then meters and allowances as tables (cap state `overage` past an allowance while opt-in overage is on),
|
|
@@ -464,6 +497,8 @@ export function usageText(summary) {
|
|
|
464
497
|
if (Array.isArray(s.allowances) && s.allowances.some((a) => isObject(a) && a.cap_state === 'storage_blocked')) {
|
|
465
498
|
head.push(['retained state', 'used up: new workspaces and forks are refused (delete workspaces you no longer need, or upgrade)']);
|
|
466
499
|
}
|
|
500
|
+
const resetsAt = isObject(s.period) ? [s.period.resets_at, s.period.end].find((t) => typeof t === 'string') : undefined;
|
|
501
|
+
head.push(...gpuLines(s.gpu, resetsAt));
|
|
467
502
|
for (const [k, v] of Object.entries(s))
|
|
468
503
|
if (!USAGE_KNOWN.has(k) && (typeof v !== 'object' || v === null))
|
|
469
504
|
head.push([k.replace(/_/g, ' '), scalar(v)]);
|
|
@@ -1019,7 +1054,7 @@ export function egressVersionTable(rows) {
|
|
|
1019
1054
|
return table(['VERSION', 'KIND', 'POLICY', 'BY', 'CREATED'], rows.map((v) => [String(v.version), v.kind, v.kind === 'cleared' ? '-' : egressPolicyLine({ mode: v.mode ?? 'allow_all', rules: v.rules, cidrs: v.cidrs }), `${v.created_by.type}:${v.created_by.id}`, time(v.created_at)]));
|
|
1020
1055
|
}
|
|
1021
1056
|
// ---- Overage settings (0.5.1; shard billing overage) -----------------------------------------------------------
|
|
1022
|
-
const RATE_UNITS = { ram_gib_hours: 'RAM GiB-hour', cpu_hours: 'CPU-hour' };
|
|
1057
|
+
const RATE_UNITS = { ram_gib_hours: 'RAM GiB-hour', cpu_hours: 'CPU-hour', gpu_hours: 'GPU-hour' };
|
|
1023
1058
|
/**
|
|
1024
1059
|
* The spend policy's overage settings for people (GET/PUT /v1/organizations/{id}/spend-policy): the state, the spend
|
|
1025
1060
|
* cap and its bounds, the rates, and the next step. `saved` adds the version the change was saved as.
|
package/dist/handlers.js
CHANGED
|
@@ -21,4 +21,6 @@ import { CODEX_CLOUD_HANDLERS } from "./codex-cloud.js";
|
|
|
21
21
|
import { THREAD_HANDLERS } from "./thread-on-computer.js";
|
|
22
22
|
import { CLI_PROXY_HANDLERS } from "./cli-proxy.js";
|
|
23
23
|
import { ACCOUNT_POOL_HANDLERS } from "./account-pool.js";
|
|
24
|
-
|
|
24
|
+
import { MOVE_HANDLERS } from "./move.js";
|
|
25
|
+
import { AGENT_CONFIG_HANDLERS } from "./agent-config-sync.js";
|
|
26
|
+
export const HANDLERS = { ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_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, ...UPDATE_HANDLERS, 'agents config-sync': AGENT_CONFIG_HANDLERS['agents config-sync'] };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type SessionHarness = 'claude' | 'codex';
|
|
2
|
+
export interface LocalSession {
|
|
3
|
+
harness: SessionHarness;
|
|
4
|
+
session_id: string;
|
|
5
|
+
/** The transcript (Claude) or rollout (Codex) file. */
|
|
6
|
+
file: string;
|
|
7
|
+
/** The session's working directory, as its transcript records it. */
|
|
8
|
+
cwd: string;
|
|
9
|
+
title: string;
|
|
10
|
+
updated_at: string;
|
|
11
|
+
bytes: number;
|
|
12
|
+
/** Where it was started: Claude's entrypoint (cli, claude-desktop, sdk-cli…) or Codex's originator (codex-tui…). */
|
|
13
|
+
source: string;
|
|
14
|
+
}
|
|
15
|
+
export interface SessionState {
|
|
16
|
+
/** A process has the session open right now. */
|
|
17
|
+
running: boolean;
|
|
18
|
+
/** working: a turn is in progress (never cut off); idle: between turns, or no process is running it. */
|
|
19
|
+
turn: 'working' | 'idle';
|
|
20
|
+
pid?: number;
|
|
21
|
+
}
|
|
22
|
+
type Env = Record<string, string | undefined>;
|
|
23
|
+
type Rec = Record<string, unknown>;
|
|
24
|
+
export declare const isSessionId: (id: string) => boolean;
|
|
25
|
+
export declare const claudeConfigDir: (env: Env) => string;
|
|
26
|
+
export declare const codexHome: (env: Env) => string;
|
|
27
|
+
/** Claude Code's folder name under projects/ for a working directory (measured: a 268-character path → 200 + "-1qkjv5"). */
|
|
28
|
+
export declare function claudeProjectName(cwd: string): string;
|
|
29
|
+
/** Records appended after `offset` (whole lines only). */
|
|
30
|
+
export declare function recordsAfter(file: string, offset: number): Rec[];
|
|
31
|
+
/** Whether the transcript's last turn is still running, from its records (see the module comment). */
|
|
32
|
+
export declare function claudeTurnFromRecords(records: Rec[]): 'working' | 'idle';
|
|
33
|
+
export declare function codexTurnFromRecords(records: Rec[]): 'working' | 'idle';
|
|
34
|
+
/** A project's sessions on this computer, newest first: those started in the folder or below it. */
|
|
35
|
+
export declare function listLocalSessions(folder: string, env: Env, options?: {
|
|
36
|
+
harness?: SessionHarness;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}): LocalSession[];
|
|
39
|
+
/** One session by id, wherever it was started. */
|
|
40
|
+
export declare function findLocalSession(id: string, env: Env): LocalSession | null;
|
|
41
|
+
/** Whether a turn is running now. A session no process has open is between turns, whatever its last record says. */
|
|
42
|
+
export declare function sessionState(s: Pick<LocalSession, 'harness' | 'session_id' | 'file'>, env: Env, now?: number): SessionState;
|
|
43
|
+
/** Whether someone typed in the local copy after it moved: a new prompt or turn after `offset`. */
|
|
44
|
+
export declare function continuedAfter(s: Pick<LocalSession, 'harness' | 'file'>, offset: number): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The transcript as whole lines, cut after its last complete record: a harness may be appending a record while a
|
|
47
|
+
* move reads it (between turns that is metadata, never part of a turn).
|
|
48
|
+
*/
|
|
49
|
+
export declare function completeRecords(bytes: Buffer): Buffer;
|
|
50
|
+
/**
|
|
51
|
+
* The session's records with its working directory moved from `from` to `to`: Claude's and Codex's `cwd` fields (and
|
|
52
|
+
* Codex's workdir and workspace roots). Lines that do not mention `from` keep their exact bytes; message text,
|
|
53
|
+
* tool output and signed thinking are never changed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function rewriteSessionPaths(bytes: Buffer, from: string, to: string): Buffer;
|
|
56
|
+
/** Where the session's transcript lives under a home folder, relative to it (the VM's and the project space's layout). */
|
|
57
|
+
export declare function homeRelativeTranscript(s: Pick<LocalSession, 'harness' | 'session_id' | 'file'>, cwd: string): string;
|
|
58
|
+
/** The transcript file is a regular file: never follow a link out of the harness's store. */
|
|
59
|
+
export declare function readTranscript(file: string): Buffer;
|
|
60
|
+
export {};
|