@shardflux/cli 0.18.0-next.5 → 0.18.1
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 +76 -8
- package/README.md +93 -34
- package/dist/account-args.js +5 -3
- package/dist/account-commands.d.ts +4 -1
- package/dist/account-commands.js +104 -14
- package/dist/account-pool.d.ts +31 -0
- package/dist/account-pool.js +313 -0
- package/dist/agent-commands.js +7 -2
- package/dist/agent-setup.d.ts +38 -0
- package/dist/agent-setup.js +116 -0
- package/dist/agent-target-commands.js +20 -4
- package/dist/agent-targets.d.ts +11 -0
- package/dist/agent-targets.js +22 -4
- package/dist/args.d.ts +3 -1
- package/dist/args.js +52 -7
- package/dist/claude-cloud-login.d.ts +11 -0
- package/dist/claude-cloud-login.js +49 -39
- package/dist/cli-proxy-hub.d.ts +26 -1
- package/dist/cli-proxy-hub.js +60 -4
- package/dist/command-center-actions.js +31 -5
- package/dist/commands.d.ts +3 -0
- package/dist/commands.js +7 -3
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +14 -3
- package/dist/conversation-terminal.d.ts +10 -0
- package/dist/conversation-terminal.js +22 -0
- package/dist/conversations.d.ts +17 -1
- package/dist/conversations.js +35 -0
- package/dist/desktop-linux.js +6 -5
- package/dist/filesystem-commands.d.ts +12 -2
- package/dist/filesystem-commands.js +46 -18
- package/dist/git-remote-bin.js +0 -0
- package/dist/handlers.js +2 -1
- package/dist/input.d.ts +4 -2
- package/dist/input.js +6 -3
- package/dist/main.js +19 -8
- package/dist/menubar.js +1 -1
- package/dist/t3-asar.d.ts +12 -2
- package/dist/t3-asar.js +14 -5
- package/dist/t3-launch.js +7 -2
- package/dist/t3-linux.d.ts +12 -13
- package/dist/t3-linux.js +81 -87
- package/dist/t3-mode.d.ts +4 -3
- package/dist/t3-mode.js +15 -14
- package/dist/t3-version.d.ts +8 -0
- package/dist/t3-version.js +42 -0
- package/package.json +21 -7
|
@@ -23,6 +23,7 @@ import { launchT3 } from "./t3-launch.js";
|
|
|
23
23
|
import { openClaudeDesktopSession, remoteControlUrl } from "./agent-target-commands.js";
|
|
24
24
|
import { openUrl } from "./browser.js";
|
|
25
25
|
import { out } from "./commands.js";
|
|
26
|
+
import { progress } from "./progress.js";
|
|
26
27
|
/** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
|
|
27
28
|
export async function conversationTerminal(ctx, ws, c, project, values) {
|
|
28
29
|
if (c.route === 'codex-cloud')
|
|
@@ -32,6 +33,27 @@ export async function conversationTerminal(ctx, ws, c, project, values) {
|
|
|
32
33
|
const pty = ws.labels.shard_conversation && ws.labels.shard_conversation !== c.id ? conversationName(c) : undefined;
|
|
33
34
|
return terminal(ctx, ws, c.harness, conversationName(c), project, values, undefined, c.worktree_path, { id: c.id, session: c.harness_session_id, title: c.title, ...(pty ? { pty } : {}), choice: recordedChoice(c) });
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* A conversation and its VM, for an app to open (Claude Desktop, claude.ai, T3 Code, the menu bar). A conversation
|
|
38
|
+
* whose VM was removed after it finished restores first (0.18.1+): a fresh VM from its saved branch and transcript, the
|
|
39
|
+
* same conversation and its session resumed. A terminal conversation's session starts detached, as a new one does, so
|
|
40
|
+
* the app reaches it at once; the guest starts the others.
|
|
41
|
+
*/
|
|
42
|
+
export async function reopenConversation(ctx, c, values) {
|
|
43
|
+
if (!c.ended_at || c.route === 'codex-cloud')
|
|
44
|
+
return { conversation: c, ws: await conversationWorkspace(ctx, c) };
|
|
45
|
+
const restored = await resumeConversation(ctx, c);
|
|
46
|
+
const ws = await conversationWorkspace(ctx, restored);
|
|
47
|
+
if (restored.route !== 'terminal') {
|
|
48
|
+
progress(ctx, 'Restored');
|
|
49
|
+
return { conversation: restored, ws };
|
|
50
|
+
}
|
|
51
|
+
if (!(await savedMachine(ctx))?.template)
|
|
52
|
+
await syncAgentConfig(ctx, ws);
|
|
53
|
+
const quiet = { ...ctx, io: { ...ctx.io, stdout: { write: () => true } } };
|
|
54
|
+
await conversationTerminal(quiet, ws, restored, ws.labels.repo_remote || ws.labels.shard_project || ws.key, { ...values, detach: true });
|
|
55
|
+
return { conversation: restored, ws };
|
|
56
|
+
}
|
|
35
57
|
/**
|
|
36
58
|
* `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
|
|
37
59
|
* Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
|
package/dist/conversations.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Workspace, WorkspaceView } from '@shardflux/sdk';
|
|
1
|
+
import type { CellClient, Workspace, WorkspaceView } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx } from './commands.js';
|
|
3
3
|
import type { ModelChoice } from './agent-models.js';
|
|
4
4
|
export type Harness = 'claude' | 'codex';
|
|
@@ -40,6 +40,15 @@ export interface Conversation {
|
|
|
40
40
|
/** The model and effort its harness starts with; null is the agent's own default (0.17.0+, APIs that keep them). */
|
|
41
41
|
model?: string | null;
|
|
42
42
|
effort?: string | null;
|
|
43
|
+
/** The account its thread runs on and the mode it started in (0.18.0+, APIs with the account pool). */
|
|
44
|
+
account?: {
|
|
45
|
+
id: string | null;
|
|
46
|
+
label: string | null;
|
|
47
|
+
provider: Harness;
|
|
48
|
+
mode: 'direct' | 'pooled';
|
|
49
|
+
} | null;
|
|
50
|
+
/** Why it needs you, while `state` is needs_you; null otherwise (0.18.0+, APIs that return it). */
|
|
51
|
+
question?: string | null;
|
|
43
52
|
}
|
|
44
53
|
export interface LocalConversation {
|
|
45
54
|
api_url: string;
|
|
@@ -56,6 +65,13 @@ export interface LocalConversation {
|
|
|
56
65
|
*/
|
|
57
66
|
export declare const conversationEnded: (c: Pick<Conversation, "state" | "ended_at" | "route" | "landed">) => boolean;
|
|
58
67
|
export declare const conversationName: (c: Conversation) => string;
|
|
68
|
+
/**
|
|
69
|
+
* The managed PTY a conversation's harness runs in (0.18.1+). The guest records it per conversation
|
|
70
|
+
* (sf-agent's conversation runtime record), which also keeps the PTY a fork inherited from its parent; without a
|
|
71
|
+
* record it is the one the guest names after the conversation, `shard-<name>-<harness>`. Never the VM key's name: a
|
|
72
|
+
* conversation's VM is `<project>/conv-<name>`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function conversationPtyId(cell: Pick<CellClient, 'exec'>, c: Conversation, signal?: AbortSignal): Promise<string>;
|
|
59
75
|
export declare function validateConversation(c: Conversation): Conversation;
|
|
60
76
|
export declare function localConversations(ctx: Pick<Ctx, 'store' | 'apiUrl'>): Record<string, LocalConversation>;
|
|
61
77
|
export declare function rememberConversation(ctx: Pick<Ctx, 'store' | 'apiUrl'>, c: Conversation, folder: string, handedOff?: boolean, baseBranch?: string): void;
|
package/dist/conversations.js
CHANGED
|
@@ -9,6 +9,7 @@ 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";
|
|
12
13
|
/**
|
|
13
14
|
* Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
|
|
14
15
|
* while its VM still takes the next turn; on Codex Cloud, `done` ends the conversation.
|
|
@@ -20,6 +21,37 @@ export const conversationName = (c) => {
|
|
|
20
21
|
throw new Error('Conversation returned an invalid branch or name');
|
|
21
22
|
return name;
|
|
22
23
|
};
|
|
24
|
+
/** The guest's managed PTY id: `shard-<name>-<app>` (services/guest-agent/internal/agents PtyID). */
|
|
25
|
+
const MANAGED_PTY = /^shard-[a-z0-9][a-z0-9._-]{0,63}-(claude|codex)$/;
|
|
26
|
+
/**
|
|
27
|
+
* The managed PTY a conversation's harness runs in (0.18.1+). The guest records it per conversation
|
|
28
|
+
* (sf-agent's conversation runtime record), which also keeps the PTY a fork inherited from its parent; without a
|
|
29
|
+
* record it is the one the guest names after the conversation, `shard-<name>-<harness>`. Never the VM key's name: a
|
|
30
|
+
* conversation's VM is `<project>/conv-<name>`.
|
|
31
|
+
*/
|
|
32
|
+
export async function conversationPtyId(cell, c, signal) {
|
|
33
|
+
const derived = `shard-${conversationName(c)}-${c.harness}`;
|
|
34
|
+
let stdout;
|
|
35
|
+
try {
|
|
36
|
+
const r = await cell.exec.run(['cat', `/home/user/.local/share/shardflux/conversation-runtime/${c.id}.json`], { timeoutMs: 10_000, maxOutputBytes: 4096, ...(signal ? { signal } : {}) });
|
|
37
|
+
if (r.exitCode !== 0)
|
|
38
|
+
return derived;
|
|
39
|
+
stdout = r.stdout;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (signal?.aborted)
|
|
43
|
+
throw error;
|
|
44
|
+
return derived;
|
|
45
|
+
}
|
|
46
|
+
let recorded;
|
|
47
|
+
try {
|
|
48
|
+
recorded = JSON.parse(stdout)?.pty_id;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return derived;
|
|
52
|
+
}
|
|
53
|
+
return typeof recorded === 'string' && MANAGED_PTY.test(recorded) && recorded.endsWith(`-${c.harness}`) ? recorded : derived;
|
|
54
|
+
}
|
|
23
55
|
export function validateConversation(c) {
|
|
24
56
|
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
57
|
throw new Error('Conversation response omitted its identity or binding');
|
|
@@ -120,6 +152,9 @@ export async function restoreConversationBinding(ctx, c, ws) {
|
|
|
120
152
|
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
153
|
if (result.exitCode !== 0)
|
|
122
154
|
throw new Error(`Conversation restore failed: ${result.stderr.trim()}`);
|
|
155
|
+
// A conversation whose VM was removed now runs in a fresh one, from its saved branch and transcript (0.18.1+).
|
|
156
|
+
if (c.restore.kind === 'resume')
|
|
157
|
+
progress(ctx, 'Restored');
|
|
123
158
|
}
|
|
124
159
|
export async function createConversationRecord(ctx, folder, harness, route, name, allocated, idempotencyKey = randomUUID(), choice = {}) {
|
|
125
160
|
const project = projectIdentity(folder);
|
package/dist/desktop-linux.js
CHANGED
|
@@ -53,7 +53,7 @@ export class LinuxDesktopManager {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
status(dir) {
|
|
57
57
|
const version = this.#version(this.app), pids = [];
|
|
58
58
|
try {
|
|
59
59
|
const pid = Number(readFileSync(join(dir, 'desktop-linux.pid'), 'utf8'));
|
|
@@ -62,20 +62,21 @@ export class LinuxDesktopManager {
|
|
|
62
62
|
pids.push(pid);
|
|
63
63
|
}
|
|
64
64
|
catch { /* no running app */ }
|
|
65
|
-
return { installed: version !== null, version, running: pids.length > 0, pids, login: existsSync(join(this.home, '.config/autostart/shardflux.desktop')) ? 'on' : 'off' };
|
|
65
|
+
return Promise.resolve({ installed: version !== null, version, running: pids.length > 0, pids, login: existsSync(join(this.home, '.config/autostart/shardflux.desktop')) ? 'on' : 'off' });
|
|
66
66
|
}
|
|
67
67
|
packagedVersion() { const path = this.#source(); return path ? this.#version(path) : null; }
|
|
68
|
-
|
|
68
|
+
login(on) {
|
|
69
69
|
const file = join(this.home, '.config/autostart/shardflux.desktop');
|
|
70
70
|
if (!on) {
|
|
71
71
|
rmSync(file, { force: true });
|
|
72
|
-
return;
|
|
72
|
+
return Promise.resolve();
|
|
73
73
|
}
|
|
74
74
|
mkdirSync(dirname(file), { recursive: true });
|
|
75
75
|
const entry = join(this.home, '.local/share/applications/dev.shardflux.Desktop.desktop');
|
|
76
76
|
if (!existsSync(entry))
|
|
77
|
-
|
|
77
|
+
return Promise.reject(new UsageError('Run shard menubar start first.'));
|
|
78
78
|
cpSync(entry, file);
|
|
79
|
+
return Promise.resolve();
|
|
79
80
|
}
|
|
80
81
|
async stop(dir) {
|
|
81
82
|
const status = await this.status(dir);
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import type { AgentFilesystem } from '@shardflux/sdk';
|
|
2
|
-
import type { Handler } from './commands.js';
|
|
1
|
+
import type { AgentFilesystem, Workspace } from '@shardflux/sdk';
|
|
2
|
+
import type { Ctx, Handler } from './commands.js';
|
|
3
3
|
/** One line per status: what is in use and what is saving or failed, its platform base and setup cache. */
|
|
4
4
|
export declare function filesystemLine(state: AgentFilesystem & {
|
|
5
5
|
cache_scope?: string;
|
|
6
6
|
}): string;
|
|
7
|
+
/** The save's one line about software under the home that went into the image (0.18.0+), as the in-VM shard says it. */
|
|
8
|
+
export declare function movedLine(state: AgentFilesystem, level: 'machine' | 'template'): string;
|
|
9
|
+
/** Creates or resumes the machine VM of the saved machine version; the first one gets this computer's agent config. */
|
|
10
|
+
/** The account's machine VM, created or resumed; `sync: false` skips the first-open config sync (an account sign-in). */
|
|
11
|
+
export declare function openMachine(ctx: Ctx, state?: AgentFilesystem, opts?: {
|
|
12
|
+
sync?: boolean;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
state: AgentFilesystem;
|
|
15
|
+
ws: Workspace;
|
|
16
|
+
}>;
|
|
7
17
|
export declare const FILESYSTEM_HANDLERS: Record<string, Handler>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codingAgentShell } from "./agent-setup.js";
|
|
1
2
|
import { waitTimeout } from "./commands.js";
|
|
2
3
|
import { UsageError } from "./errors.js";
|
|
3
4
|
import { projectIdentity, normalizeRemote } from "./project-identity.js";
|
|
@@ -37,9 +38,17 @@ async function saved(ctx, values, save, level) {
|
|
|
37
38
|
const state = await save({ signal: ctx.signal, ...(wait ? { wait } : {}) });
|
|
38
39
|
if (ctx.json)
|
|
39
40
|
return output(ctx, state);
|
|
40
|
-
|
|
41
|
+
const moved = movedLine(state, level);
|
|
42
|
+
ctx.io.stdout.write(wait ? `${level}: version ${state.version} saved${moved}\n` : state.pending ? `${filesystemLine(state)}; shard ${level} shows when it is ready${moved}\n` : `${filesystemLine(state)}${moved}\n`);
|
|
41
43
|
return 0;
|
|
42
44
|
}
|
|
45
|
+
/** The save's one line about software under the home that went into the image (0.18.0+), as the in-VM shard says it. */
|
|
46
|
+
export function movedLine(state, level) {
|
|
47
|
+
const moved = state.moved_to_image ?? [];
|
|
48
|
+
if (moved.length === 0)
|
|
49
|
+
return '';
|
|
50
|
+
return `; ${moved.length} large tool folder${moved.length === 1 ? '' : 's'} went into the ${level} image (${moved.join(', ')}); they keep working at the same paths`;
|
|
51
|
+
}
|
|
43
52
|
const rollbackTo = (value) => {
|
|
44
53
|
if (value === undefined)
|
|
45
54
|
return undefined;
|
|
@@ -48,34 +57,53 @@ const rollbackTo = (value) => {
|
|
|
48
57
|
throw new UsageError('--to must be a saved version number');
|
|
49
58
|
return n;
|
|
50
59
|
};
|
|
60
|
+
/** Creates or resumes the machine VM of the saved machine version; the first one gets this computer's agent config. */
|
|
61
|
+
/** The account's machine VM, created or resumed; `sync: false` skips the first-open config sync (an account sign-in). */
|
|
62
|
+
export async function openMachine(ctx, state, opts = {}) {
|
|
63
|
+
const cloud = ctx.cloud();
|
|
64
|
+
state ??= await cloud.machine.get({ signal: ctx.signal });
|
|
65
|
+
const key = `machine/${state.organization_id}/v${state.version}`;
|
|
66
|
+
const existing = await cloud.workspaces.findByKey(key, { includeDeleted: false, signal: ctx.signal });
|
|
67
|
+
if (!existing)
|
|
68
|
+
progress(ctx, `Creating your machine VM (${key}); the first open takes about a minute…`);
|
|
69
|
+
else if (existing.data.observed_state !== 'running')
|
|
70
|
+
progress(ctx, `Resuming your machine VM (${key})…`);
|
|
71
|
+
const ws = await cloud.workspaces.open({ key, template: state.template ? `${state.template.slug}@${state.template.version}` : 'coding',
|
|
72
|
+
labels: { ...existing?.labels, shard_layer: 'machine', shard_machine: state.organization_id, repo_path: '/home/user', app: 'claude', agent: 'machine' },
|
|
73
|
+
wait: { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal } });
|
|
74
|
+
if (state.version === 0 && opts.sync !== false) {
|
|
75
|
+
progress(ctx, 'Syncing your Claude and Codex config into the machine…');
|
|
76
|
+
await syncAgentConfig(ctx, ws);
|
|
77
|
+
}
|
|
78
|
+
return { state, ws };
|
|
79
|
+
}
|
|
51
80
|
export const FILESYSTEM_HANDLERS = {
|
|
52
81
|
async machine(ctx) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
if (ctx.json || !ctx.io.stdin.isTTY)
|
|
82
|
+
const state = await ctx.cloud().machine.get({ signal: ctx.signal });
|
|
83
|
+
// Without a terminal (or in a coding agent's shell, 0.17.1+) there is nobody to type to the machine agent: the status.
|
|
84
|
+
if (ctx.json || !ctx.io.stdin.isTTY || codingAgentShell(ctx.io.env))
|
|
56
85
|
return output(ctx, state);
|
|
57
|
-
const
|
|
58
|
-
const existing = await cloud.workspaces.findByKey(key, { includeDeleted: false, signal: ctx.signal });
|
|
59
|
-
if (!existing)
|
|
60
|
-
progress(ctx, `Creating your machine VM (${key}); the first open takes about a minute…`);
|
|
61
|
-
else if (existing.data.observed_state !== 'running')
|
|
62
|
-
progress(ctx, `Resuming your machine VM (${key})…`);
|
|
63
|
-
const ws = await cloud.workspaces.open({ key, template: state.template ? `${state.template.slug}@${state.template.version}` : 'coding',
|
|
64
|
-
labels: { ...existing?.labels, shard_layer: 'machine', shard_machine: state.organization_id, repo_path: '/home/user', app: 'claude', agent: 'machine' },
|
|
65
|
-
wait: { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal } });
|
|
66
|
-
if (state.version === 0) {
|
|
67
|
-
progress(ctx, 'Syncing your Claude and Codex config into the machine…');
|
|
68
|
-
await syncAgentConfig(ctx, ws);
|
|
69
|
-
}
|
|
86
|
+
const { ws } = await openMachine(ctx, state);
|
|
70
87
|
return AGENT_HANDLERS['agents open'](ctx, {}, [ws.key]);
|
|
71
88
|
},
|
|
89
|
+
/** `shard machine open` (0.17.1+): the machine VM without the agent terminal, for an agent that builds the machine. */
|
|
90
|
+
async 'machine open'(ctx) {
|
|
91
|
+
const { state, ws } = await openMachine(ctx);
|
|
92
|
+
if (ctx.json)
|
|
93
|
+
ctx.io.stdout.write(JSON.stringify({ workspace: ws.data, key: ws.key, machine: state, next: [`shard ws exec ${ws.key} -- bash -lc '<install commands>'`, `shard files write ${ws.key} <path> --from -`, 'shard machine save --wait'] }) + '\n');
|
|
94
|
+
else {
|
|
95
|
+
ctx.io.stdout.write(`${ws.key}\n`);
|
|
96
|
+
progress(ctx, `Install with shard ws exec ${ws.key} -- bash -lc '…', write files with shard files write ${ws.key} <path> --from -, then shard machine save --wait.`);
|
|
97
|
+
}
|
|
98
|
+
return 0;
|
|
99
|
+
},
|
|
72
100
|
async 'machine save'(ctx, values) {
|
|
73
101
|
const cloud = ctx.cloud();
|
|
74
102
|
const state = await cloud.machine.get({ signal: ctx.signal });
|
|
75
103
|
const ws = await cloud.workspaces.findByKey(`machine/${state.organization_id}/v${state.version}`, { signal: ctx.signal }) ??
|
|
76
104
|
(state.workspace_id ? await cloud.workspaces.get(state.workspace_id) : null);
|
|
77
105
|
if (!ws)
|
|
78
|
-
throw new UsageError('Open
|
|
106
|
+
throw new UsageError('Open your machine and change it first, then run shard machine save', 'shard machine in a terminal, or shard machine open --json for an agent (0.17.1+)');
|
|
79
107
|
return saved(ctx, values, options => cloud.machine.save({ workspaceId: ws.id }, options), 'machine');
|
|
80
108
|
},
|
|
81
109
|
async 'machine rollback'(ctx, values) {
|
package/dist/git-remote-bin.js
CHANGED
|
File without changes
|
package/dist/handlers.js
CHANGED
|
@@ -20,4 +20,5 @@ import { TWIN_HANDLERS } from "./worktree-twin-engine.js";
|
|
|
20
20
|
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
|
+
export const 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 };
|
package/dist/input.d.ts
CHANGED
|
@@ -13,8 +13,10 @@ export interface ErrWriter {
|
|
|
13
13
|
}
|
|
14
14
|
export declare class Input {
|
|
15
15
|
#private;
|
|
16
|
-
constructor(stdin: StdinLike, stderr: ErrWriter
|
|
17
|
-
|
|
16
|
+
constructor(stdin: StdinLike, stderr: ErrWriter, opts?: {
|
|
17
|
+
agentShell?: boolean;
|
|
18
|
+
});
|
|
19
|
+
/** True when standard input is a terminal `shard` can prompt on (and not a coding agent's shell). */
|
|
18
20
|
get interactive(): boolean;
|
|
19
21
|
/** The next line of piped input without its line ending, or null at the end of input. */
|
|
20
22
|
line(): Promise<string | null>;
|
package/dist/input.js
CHANGED
|
@@ -16,18 +16,21 @@ const MAX_PIPED_BYTES = 64 * 1024;
|
|
|
16
16
|
export class Input {
|
|
17
17
|
#stdin;
|
|
18
18
|
#stderr;
|
|
19
|
+
/** In a coding agent's shell nobody types at a prompt, even on a terminal (0.17.1+). */
|
|
20
|
+
#agentShell;
|
|
19
21
|
#it = null;
|
|
20
22
|
#decoder = new TextDecoder('utf-8');
|
|
21
23
|
#buf = '';
|
|
22
24
|
#eof = false;
|
|
23
25
|
#read = 0;
|
|
24
|
-
constructor(stdin, stderr) {
|
|
26
|
+
constructor(stdin, stderr, opts = {}) {
|
|
25
27
|
this.#stdin = stdin;
|
|
26
28
|
this.#stderr = stderr;
|
|
29
|
+
this.#agentShell = opts.agentShell === true;
|
|
27
30
|
}
|
|
28
|
-
/** True when standard input is a terminal `shard` can prompt on. */
|
|
31
|
+
/** True when standard input is a terminal `shard` can prompt on (and not a coding agent's shell). */
|
|
29
32
|
get interactive() {
|
|
30
|
-
return this.#stdin.isTTY === true;
|
|
33
|
+
return this.#stdin.isTTY === true && !this.#agentShell;
|
|
31
34
|
}
|
|
32
35
|
async #more() {
|
|
33
36
|
if (this.#eof)
|
package/dist/main.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { homedir, hostname } from 'node:os';
|
|
14
14
|
import { statSync } from 'node:fs';
|
|
15
15
|
import { SDK_VERSION, Shardflux, ShardfluxAccount, isSessionToken } from '@shardflux/sdk';
|
|
16
|
-
import { CLI_VERSION, CENTER_COMMAND, ONBOARDING_COMMAND, PROJECT_COMMAND,
|
|
16
|
+
import { CLI_VERSION, CENTER_COMMAND, GUIDE_COMMAND, ONBOARDING_COMMAND, PROJECT_COMMAND, parseCommandLine, resolveApiUrl, resolveWake, resolveWakeTimeout } from "./args.js";
|
|
17
17
|
import { commandCenter } from "./command-center.js";
|
|
18
18
|
import { launchProjectCommand } from "./conversation-terminal.js";
|
|
19
19
|
import { runAcp } from "./acp.js";
|
|
@@ -21,6 +21,8 @@ import { registerInstalledT3 } from "./t3-provider.js";
|
|
|
21
21
|
import { refreshForwarderAgent } from "./cli-proxy.js";
|
|
22
22
|
import { runAcpLogin } from "./acp-login.js";
|
|
23
23
|
import { terminalOnboarding } from "./terminal-onboarding.js";
|
|
24
|
+
import { codingAgentShell, setupGuideText } from "./agent-setup.js";
|
|
25
|
+
import { agentSetupGuide } from "./account-commands.js";
|
|
24
26
|
import { tryLinuxDesktop } from "./desktop-linux.js";
|
|
25
27
|
import { startLinuxHelper } from "./linux-helper.js";
|
|
26
28
|
import { runDeviceHelper } from "./device-helper.js";
|
|
@@ -40,7 +42,7 @@ import { CLI_DISTRIBUTION, NOTICE_WAIT_MS, UpdateCheck, noticeOf, updateCheckDis
|
|
|
40
42
|
const KEY_SHAPE = /^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/;
|
|
41
43
|
// Printable (no C0 controls or DEL), as the API's AgentLabel pattern.
|
|
42
44
|
const isLabel = (s) => s.length >= 1 && s.length <= 100 && ![...s].some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f);
|
|
43
|
-
const LOGIN_HINT = 'shard auth login --
|
|
45
|
+
const LOGIN_HINT = 'shard auth login --browser';
|
|
44
46
|
const STEP_UP_HINT = `confirm your password, then run the command again (valid 10 minutes): printf '%s\\n' "$PASSWORD" | shard auth step-up (add --code <totp> with two-factor authentication)`;
|
|
45
47
|
/** A sleep that ends early when the signal aborts (so Ctrl-C never waits out a poll interval). */
|
|
46
48
|
export function abortableSleep(signal) {
|
|
@@ -427,9 +429,12 @@ export async function run(argv, io, opts = {}) {
|
|
|
427
429
|
io.stderr.write(redact(text, ...secrets));
|
|
428
430
|
return code;
|
|
429
431
|
};
|
|
432
|
+
// A coding agent's shell (0.17.1+) is no terminal to attach to or prompt on, even when it is a PTY.
|
|
433
|
+
const agentShell = codingAgentShell(io.env);
|
|
434
|
+
const tty = io.stdin.isTTY === true && io.stdout.isTTY !== false && !agentShell;
|
|
430
435
|
let parsed;
|
|
431
436
|
try {
|
|
432
|
-
parsed = parseCommandLine(argv, { tty
|
|
437
|
+
parsed = parseCommandLine(argv, { tty, isProjectPath: (path) => { try {
|
|
433
438
|
return statSync(path).isDirectory();
|
|
434
439
|
}
|
|
435
440
|
catch {
|
|
@@ -449,10 +454,11 @@ export async function run(argv, io, opts = {}) {
|
|
|
449
454
|
io.stdout.write(wantsJson ? json({ cli: CLI_VERSION, sdk: SDK_VERSION }) : `shard ${CLI_VERSION} (@shardflux/sdk ${SDK_VERSION})\n`);
|
|
450
455
|
return EXIT.ok;
|
|
451
456
|
}
|
|
452
|
-
|
|
457
|
+
// shard ./ attaches Claude Code or Codex to this terminal: without one (or in a coding agent's shell) it says how to
|
|
458
|
+
// start the conversation instead of starting one nobody can see (0.17.1+: a usage error, before 0.17.1 its help).
|
|
459
|
+
if (parsed.command === PROJECT_COMMAND && !tty && !parsed.global.json) {
|
|
453
460
|
await startupHousekeeping({ io, store, signal });
|
|
454
|
-
|
|
455
|
-
return EXIT.ok;
|
|
461
|
+
return report(new UsageError('shard ./ opens Claude Code or Codex in a terminal, and this shell has none.', 'ask your user to run shard ./ in their own terminal, or start the conversation detached: shard ./ --json --prompt "<task>" (shard ./ codex --json ... for Codex)'));
|
|
456
462
|
}
|
|
457
463
|
command = parsed.command.path.join(' ');
|
|
458
464
|
let ctx;
|
|
@@ -544,7 +550,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
544
550
|
timing: showTiming,
|
|
545
551
|
traces,
|
|
546
552
|
store,
|
|
547
|
-
input: new Input(io.stdin, io.stderr),
|
|
553
|
+
input: new Input(io.stdin, io.stderr, { agentShell }),
|
|
548
554
|
distribution,
|
|
549
555
|
runtime: opts.runtime ?? null,
|
|
550
556
|
async clientVersions() {
|
|
@@ -606,7 +612,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
606
612
|
else {
|
|
607
613
|
const saved = store.profile(apiUrl).session;
|
|
608
614
|
if (!saved)
|
|
609
|
-
throw new AuthConfigError(`Not signed in to ${apiUrl}.`, `sign in: ${LOGIN_HINT} (new here? shard
|
|
615
|
+
throw new AuthConfigError(`Not signed in to ${apiUrl}.`, `sign in: ${LOGIN_HINT} (new here? shard signup; setting Shardflux up for someone: shard setup --guide)`);
|
|
610
616
|
cred.sessionSource = 'file';
|
|
611
617
|
cred.email = saved.user?.email ?? null;
|
|
612
618
|
secrets.push(saved.token);
|
|
@@ -689,6 +695,11 @@ export async function run(argv, io, opts = {}) {
|
|
|
689
695
|
}
|
|
690
696
|
stopHelper = startLinuxHelper(ctx);
|
|
691
697
|
};
|
|
698
|
+
// Bare shard without a terminal (0.17.1+): the setup guide, read from local state only (never waits).
|
|
699
|
+
if (parsed.command === GUIDE_COMMAND) {
|
|
700
|
+
io.stdout.write(setupGuideText(agentSetupGuide(ctx)));
|
|
701
|
+
return EXIT.ok;
|
|
702
|
+
}
|
|
692
703
|
const handler = parsed.command.path[0] === 'acp' ? (ctx) => parsed.values.login === true ? runAcpLogin(ctx) : runAcp(ctx) : parsed.command.path[0] === 'device-helper' ? (ctx) => runDeviceHelper(ctx, { service: parsed.values.service === true }) : command === 'menubar refresh' ? runMenubarRefresh : parsed.command === PROJECT_COMMAND ? launchProjectCommand : parsed.command === ONBOARDING_COMMAND ? async (ctx, values) => await tryLinuxDesktop(ctx) ? 0 : terminalOnboarding(ctx, values, { startHelper }) : parsed.command === CENTER_COMMAND ? commandCenter : HANDLERS[parsed.command.path.join(' ')];
|
|
693
704
|
if (!handler)
|
|
694
705
|
return report(new UsageError(`unknown command "${parsed.command.path.join(' ')}"`));
|
package/dist/menubar.js
CHANGED
|
@@ -342,7 +342,7 @@ export function refreshMenubarInBackground(o) {
|
|
|
342
342
|
return false;
|
|
343
343
|
const manifest = (dir) => { try {
|
|
344
344
|
const m = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8'));
|
|
345
|
-
return m.id === 'dev.shardflux.Desktop' ? m.version : null;
|
|
345
|
+
return m.id === 'dev.shardflux.Desktop' && typeof m.version === 'string' ? m.version : null;
|
|
346
346
|
}
|
|
347
347
|
catch {
|
|
348
348
|
return null;
|
package/dist/t3-asar.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
/** The server CLI inside the archive: what Electron runs as Node to issue T3 sessions. */
|
|
2
2
|
export declare const T3_SERVER_ENTRY = "apps/server/dist/bin.mjs";
|
|
3
|
-
/** The
|
|
4
|
-
export
|
|
3
|
+
/** The archive's package.json: whether it is T3 Code's, and its release version. */
|
|
4
|
+
export interface T3Package {
|
|
5
|
+
t3: boolean;
|
|
6
|
+
version: string | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The archive's package.json (null when it has none): T3 Code's when its `name` is `t3code` (as 0.0.45 and the
|
|
10
|
+
* nightlies ship it) or its `productName` is T3 Code's, and its release version (e.g. `0.0.46-nightly.20261010.2908`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function asarPackage(archive: string): T3Package | null;
|
|
13
|
+
/** The app's release version from its package.json, or null. */
|
|
14
|
+
export declare const asarVersion: (archive: string) => string | null;
|
|
5
15
|
/**
|
|
6
16
|
* Whether the server registers the ACP Registry driver. A version number alone is insufficient (0.0.45 lacks it while
|
|
7
17
|
* upstream main had it). Builds put the driver in `bin.mjs` itself, or (nightlies since October 2026) in a chunk the
|
package/dist/t3-asar.js
CHANGED
|
@@ -61,13 +61,22 @@ function read(a, path) {
|
|
|
61
61
|
const bytes = Buffer.alloc(size);
|
|
62
62
|
return readSync(a.fd, bytes, 0, size, offset) === size ? bytes : undefined;
|
|
63
63
|
}
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The archive's package.json (null when it has none): T3 Code's when its `name` is `t3code` (as 0.0.45 and the
|
|
66
|
+
* nightlies ship it) or its `productName` is T3 Code's, and its release version (e.g. `0.0.46-nightly.20261010.2908`).
|
|
67
|
+
*/
|
|
68
|
+
export function asarPackage(archive) {
|
|
69
|
+
return fact(archive, 'app', a => {
|
|
70
|
+
const bytes = read(a, 'package.json');
|
|
71
|
+
if (!bytes)
|
|
72
|
+
return null;
|
|
73
|
+
const doc = JSON.parse(bytes.toString());
|
|
74
|
+
return { t3: doc.name === 't3code' || (typeof doc.productName === 'string' && /^T3 Code(?: \(.+\))?$/.test(doc.productName)),
|
|
75
|
+
version: typeof doc.version === 'string' && RELEASE.test(doc.version) ? doc.version : null };
|
|
69
76
|
}, null) ?? null;
|
|
70
77
|
}
|
|
78
|
+
/** The app's release version from its package.json, or null. */
|
|
79
|
+
export const asarVersion = (archive) => asarPackage(archive)?.version ?? null;
|
|
71
80
|
/**
|
|
72
81
|
* Whether the server registers the ACP Registry driver. A version number alone is insufficient (0.0.45 lacks it while
|
|
73
82
|
* upstream main had it). Builds put the driver in `bin.mjs` itself, or (nightlies since October 2026) in a chunk the
|
package/dist/t3-launch.js
CHANGED
|
@@ -9,6 +9,7 @@ import { conversationWorkspace, getConversation } from "./conversations.js";
|
|
|
9
9
|
import { resolveOn } from "./thread-on-computer.js";
|
|
10
10
|
import { ensureCloudClaude } from "./claude-cloud-login.js";
|
|
11
11
|
import { modelChoice } from "./agent-models.js";
|
|
12
|
+
import { conversationPooled } from "./account-pool.js";
|
|
12
13
|
/**
|
|
13
14
|
* A `t3_*` refusal raised once the workspace, conversation or thread exists names them (details.workspace_key,
|
|
14
15
|
* conversation_id, t3_thread_id), so a client opens what exists instead of starting it again.
|
|
@@ -124,10 +125,14 @@ export async function openT3Conversation(ctx, conversationId, values = {}) {
|
|
|
124
125
|
return 0;
|
|
125
126
|
}
|
|
126
127
|
// Its VM as T3's SSH environment, with the checks every `agents open t3` makes: --reachable and the cloud Claude login.
|
|
127
|
-
|
|
128
|
+
// A conversation whose VM was removed restores into a fresh one first (0.18.1+).
|
|
129
|
+
const { reopenConversation } = await import("./conversation-terminal.js");
|
|
130
|
+
let { ws } = await reopenConversation(ctx, conversation, values);
|
|
128
131
|
if (values.reachable !== undefined)
|
|
129
132
|
ws = await ctx.cloud().workspaces.open({ key: ws.key, ...agentReachability(values), wait: false });
|
|
130
|
-
|
|
133
|
+
// A pooled thread (0.18.0+) runs on the account hub, not on the account's cloud Claude login.
|
|
134
|
+
if (!conversationPooled(conversation))
|
|
135
|
+
await ensureCloudClaude(ctx, ws);
|
|
131
136
|
const { openDesktopWorkspace } = await import("./agent-target-commands.js");
|
|
132
137
|
return openDesktopWorkspace(ctx, ws, 't3', install.version ?? undefined, false, undefined, install, conversationId);
|
|
133
138
|
}
|
package/dist/t3-linux.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
type Env = Record<string, string | undefined>;
|
|
2
|
-
export type T3LinuxFormat = 'deb' | 'directory' | 'appimage' | 'flatpak'
|
|
2
|
+
export type T3LinuxFormat = 'deb' | 'directory' | 'appimage' | 'flatpak';
|
|
3
3
|
export interface T3LinuxApp {
|
|
4
4
|
format: T3LinuxFormat;
|
|
5
5
|
/** The desktop entry's file name (the id `gtk-launch` takes); null for an AppImage found by its file name. */
|
|
6
6
|
desktop_id: string | null;
|
|
7
7
|
/** What starts it: the entry's Exec without field codes, or the AppImage. */
|
|
8
8
|
exec: string[];
|
|
9
|
-
/** The app's resources/app.asar on this computer (deb, other unpacked installs, flatpak); null for an AppImage
|
|
9
|
+
/** The app's resources/app.asar on this computer (deb, other unpacked installs, flatpak); null for an AppImage. */
|
|
10
10
|
asar: string | null;
|
|
11
|
-
/** Flatpak: the app id and the app's files folder on this computer (mounted at /app in its sandbox). */
|
|
11
|
+
/** Flatpak (unverified layout): the app id and the app's files folder on this computer (mounted at /app in its sandbox). */
|
|
12
12
|
flatpak?: {
|
|
13
13
|
id: string;
|
|
14
14
|
files: string;
|
|
@@ -20,7 +20,7 @@ export interface T3LinuxInstall {
|
|
|
20
20
|
version: string | null;
|
|
21
21
|
linux: T3LinuxApp;
|
|
22
22
|
}
|
|
23
|
-
/** The launcher T3 (
|
|
23
|
+
/** The launcher T3 (from v0.0.46-preview.20261008.2800) writes to `<T3 home>/bin/t3` for its bundled CLI; for an AppImage it mounts the image itself. */
|
|
24
24
|
export declare const T3_LAUNCHER_MARKER = "Written by T3 Code: runs the desktop app's bundled t3 CLI.";
|
|
25
25
|
/**
|
|
26
26
|
* The folders scanned on Linux, in XDG precedence: the data home's and each data dir's `applications`, snapd's and
|
|
@@ -35,8 +35,8 @@ export declare function desktopExec(value: string): string[] | undefined;
|
|
|
35
35
|
export declare function linuxT3Apps(dirs: string[], env: Env): T3LinuxInstall[];
|
|
36
36
|
/**
|
|
37
37
|
* Whether this install runs the Shardflux provider (ACP mode): the server in its app.asar has the ACP Registry driver
|
|
38
|
-
* (deb, unpacked installs, flatpak). An AppImage is never mounted to look: its
|
|
39
|
-
*
|
|
38
|
+
* (deb, unpacked installs, flatpak). An AppImage is never mounted to look: its sessions come from T3's own launcher, so
|
|
39
|
+
* it is ACP from the first build that writes one (t3WritesLauncher); earlier builds start threads over SSH.
|
|
40
40
|
*/
|
|
41
41
|
export declare function linuxSupportsAcp(app: T3LinuxInstall): boolean;
|
|
42
42
|
/**
|
|
@@ -48,6 +48,11 @@ export declare function linuxServerCli(app: T3LinuxInstall, t3Home: string): {
|
|
|
48
48
|
command: string;
|
|
49
49
|
args: string[];
|
|
50
50
|
} | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* The environment T3 is opened with: the caller's, without what would make its Electron run as plain Node
|
|
53
|
+
* (ELECTRON_RUN_AS_NODE: `t3code` would exit at once while the open still looked fine) or detach its console.
|
|
54
|
+
*/
|
|
55
|
+
export declare function t3OpenEnv(env: Env): Env;
|
|
51
56
|
/**
|
|
52
57
|
* How T3 is opened (or brought to the front, as a second launch of T3 hands over to the running one): `gtk-launch`
|
|
53
58
|
* with its desktop entry when there is one, else its own program, started detached.
|
|
@@ -58,11 +63,5 @@ export declare function linuxOpenCommands(app: T3LinuxInstall, env: Env): Array<
|
|
|
58
63
|
detached: boolean;
|
|
59
64
|
}>;
|
|
60
65
|
/** Opens T3 Code on Linux. Nothing here ever stops a T3 process: a launcher that hangs is left alone. */
|
|
61
|
-
export declare function openLinuxT3(
|
|
62
|
-
/**
|
|
63
|
-
* When T3 last started, as the filesystem records it: the program's access time once it is later than its modification
|
|
64
|
-
* time (dpkg installs both at the build time; relatime updates the access time on the first start, then daily). Null
|
|
65
|
-
* before the first start and on noatime mounts.
|
|
66
|
-
*/
|
|
67
|
-
export declare function linuxLastUsed(path: string): string | null;
|
|
66
|
+
export declare function openLinuxT3(callerEnv: Env, app: T3LinuxInstall): Promise<void>;
|
|
68
67
|
export {};
|