@shardflux/cli 0.16.0 → 0.18.0-next.4
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 +67 -2
- package/README.md +47 -8
- package/dist/agent-commands.d.ts +9 -1
- package/dist/agent-commands.js +65 -16
- package/dist/agent-models.d.ts +61 -0
- package/dist/agent-models.js +79 -0
- package/dist/agent-state.d.ts +10 -0
- package/dist/agent-state.js +13 -0
- package/dist/agent-target-commands.d.ts +6 -1
- package/dist/agent-target-commands.js +113 -16
- package/dist/agent-targets.d.ts +17 -5
- package/dist/agent-targets.js +27 -11
- package/dist/args.d.ts +5 -1
- package/dist/args.js +49 -9
- package/dist/cli-proxy-forward.d.ts +45 -0
- package/dist/cli-proxy-forward.js +432 -0
- package/dist/cli-proxy-hub.d.ts +273 -0
- package/dist/cli-proxy-hub.js +749 -0
- package/dist/cli-proxy.d.ts +325 -0
- package/dist/cli-proxy.js +1016 -0
- package/dist/codex-cloud.d.ts +2 -1
- package/dist/codex-cloud.js +16 -6
- package/dist/command-center-actions.js +16 -1
- package/dist/conversation-local.js +2 -2
- package/dist/conversation-stash.js +4 -3
- package/dist/conversation-terminal.d.ts +5 -0
- package/dist/conversation-terminal.js +45 -6
- package/dist/conversations.d.ts +18 -2
- package/dist/conversations.js +12 -5
- package/dist/desktop-linux.d.ts +40 -0
- package/dist/desktop-linux.js +186 -0
- package/dist/device-approvals.d.ts +12 -0
- package/dist/device-approvals.js +75 -0
- package/dist/device-helper.d.ts +3 -1
- package/dist/device-helper.js +5 -2
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +1 -1
- package/dist/git-remote-bin.js +0 -0
- package/dist/handlers.js +3 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/linux-helper.d.ts +12 -0
- package/dist/linux-helper.js +88 -0
- package/dist/linux-terminal.d.ts +11 -0
- package/dist/linux-terminal.js +42 -0
- package/dist/main.js +30 -6
- package/dist/menubar.js +44 -4
- package/dist/onboarding.d.ts +2 -0
- package/dist/onboarding.js +59 -9
- package/dist/t3-asar.d.ts +10 -0
- package/dist/t3-asar.js +91 -0
- package/dist/t3-launch.d.ts +37 -0
- package/dist/t3-launch.js +133 -0
- package/dist/t3-linux.d.ts +68 -0
- package/dist/t3-linux.js +343 -0
- package/dist/t3-mode.d.ts +129 -0
- package/dist/t3-mode.js +676 -0
- package/dist/t3-phone.d.ts +17 -0
- package/dist/t3-phone.js +15 -0
- package/dist/t3-provider.d.ts +5 -3
- package/dist/t3-provider.js +12 -49
- package/dist/t3-thread-script.d.ts +18 -0
- package/dist/t3-thread-script.js +245 -0
- package/dist/terminal-onboarding.d.ts +11 -0
- package/dist/terminal-onboarding.js +75 -0
- package/dist/thread-on-computer.d.ts +2 -2
- package/dist/thread-on-computer.js +13 -7
- package/dist/update.d.ts +1 -1
- package/dist/update.js +28 -1
- package/dist/worktree-twin-engine.d.ts +31 -1
- package/dist/worktree-twin-engine.js +253 -40
- package/dist/worktree-twin.d.ts +17 -0
- package/dist/worktree-twin.js +147 -22
- package/package.json +7 -19
package/dist/codex-cloud.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { CodexCloudTask } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx, Handler } from './commands.js';
|
|
3
3
|
import type { Values } from './args.js';
|
|
4
|
+
import type { ModelChoice } from './agent-models.js';
|
|
4
5
|
import type { Conversation } from './conversations.js';
|
|
5
6
|
import type { InitialImage } from './initial-prompt.js';
|
|
6
7
|
/** Only provider-returned task ids enter the registered Codex deep-link route. */
|
|
7
8
|
export declare function openCodexThread(task: CodexCloudTask, env: Ctx['io']['env']): Promise<void>;
|
|
8
9
|
export declare function submitCloudInitialPrompt(ctx: Ctx, c: Conversation, prompt: string, workspaceKey: string): Promise<void>;
|
|
9
10
|
/** Prints the Cloud thread (and its link) and opens it in the Codex app; --json prints it instead. */
|
|
10
|
-
export declare function printCloudThread(ctx: Ctx, c: Conversation, task: CodexCloudTask, submitted?: boolean): Promise<void>;
|
|
11
|
+
export declare function printCloudThread(ctx: Ctx, c: Conversation, task: CodexCloudTask, submitted?: boolean, choice?: ModelChoice): Promise<void>;
|
|
11
12
|
/** How long the account's Codex remote device may take to come up before it counts as stuck. */
|
|
12
13
|
export declare const CODEX_STARTING_LIMIT_MS: number;
|
|
13
14
|
export type CodexConnectorView = {
|
package/dist/codex-cloud.js
CHANGED
|
@@ -2,7 +2,8 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { CodedError, UsageError } from "./errors.js";
|
|
3
3
|
import { json } from "./format.js";
|
|
4
4
|
import { syncAgentCredentials } from "./agent-credentials.js";
|
|
5
|
-
import { createConversation, resumeConversation } from "./conversations.js";
|
|
5
|
+
import { createConversation, getConversation, recordedChoice, resumeConversation } from "./conversations.js";
|
|
6
|
+
import { CHOICE_PENDING, choiceReport, hasChoice, modelChoice } from "./agent-models.js";
|
|
6
7
|
import { AgentStateStore } from "./agent-state.js";
|
|
7
8
|
import { ensureProjectGit } from "./project-git.js";
|
|
8
9
|
import { gitRunner } from "./agent-git.js";
|
|
@@ -33,16 +34,24 @@ export async function submitCloudInitialPrompt(ctx, c, prompt, workspaceKey) {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
/** Prints the Cloud thread (and its link) and opens it in the Codex app; --json prints it instead. */
|
|
36
|
-
export async function printCloudThread(ctx, c, task, submitted = false) { return output(ctx, c, task, submitted); }
|
|
37
|
-
async function output(ctx, c, task, submitted = false) {
|
|
37
|
+
export async function printCloudThread(ctx, c, task, submitted = false, choice) { return output(ctx, c, task, submitted, choice); }
|
|
38
|
+
async function output(ctx, c, task, submitted = false, choice) {
|
|
38
39
|
const url = threadLink(task);
|
|
40
|
+
// model_choice (0.17.0+): an admitted thread's record keeps the model and effort only when Cloud started it with them.
|
|
41
|
+
const applied = hasChoice(choice) ? await cloudChoiceApplied(ctx, c, choice) : true;
|
|
39
42
|
if (ctx.json) {
|
|
40
|
-
ctx.io.stdout.write(json({ conversation: c, cloud: task, url, ...(submitted ? { initial_prompt: { submitted: true } } : {}) }));
|
|
43
|
+
ctx.io.stdout.write(json({ conversation: c, cloud: task, url, ...(submitted ? { initial_prompt: { submitted: true } } : {}), ...(hasChoice(choice) ? { model_choice: choiceReport(choice, applied) } : {}) }));
|
|
41
44
|
return;
|
|
42
45
|
}
|
|
46
|
+
if (!applied)
|
|
47
|
+
ctx.io.stderr.write(`${CHOICE_PENDING}\n`);
|
|
43
48
|
ctx.io.stdout.write(`${c.key}\n${task.desktop_url}\n`);
|
|
44
49
|
await openCodexThread(task, ctx.io.env);
|
|
45
50
|
}
|
|
51
|
+
async function cloudChoiceApplied(ctx, c, choice) {
|
|
52
|
+
const kept = recordedChoice(await getConversation(ctx, c.id));
|
|
53
|
+
return kept.model === choice.model && kept.effort === choice.effort;
|
|
54
|
+
}
|
|
46
55
|
/** How long the account's Codex remote device may take to come up before it counts as stuck. */
|
|
47
56
|
export const CODEX_STARTING_LIMIT_MS = 5 * 60_000;
|
|
48
57
|
/** When a connector generation started: its UUIDv7 time. */
|
|
@@ -114,11 +123,12 @@ export async function launchCodexCloud(ctx, values, path, images = []) {
|
|
|
114
123
|
throw new UsageError('New Codex conversations open in Codex Cloud. Omit --in or use --in codex-app.');
|
|
115
124
|
if (values.like !== undefined)
|
|
116
125
|
throw new UsageError('Choose the project directory with --path; new Codex conversations use its latest machine template.');
|
|
126
|
+
const choice = modelChoice(values, 'codex');
|
|
117
127
|
const project = projectIdentity(path);
|
|
118
128
|
await ensureProjectGit(ctx, project.folder);
|
|
119
129
|
await gitRunner(project.folder, { ...process.env, ...ctx.io.env }, ctx.signal)(['rev-parse', 'HEAD']);
|
|
120
130
|
await ensureCodexCloud(ctx);
|
|
121
|
-
const created = await createConversation(ctx, path, 'codex', 'codex-cloud', typeof values.name === 'string' ? values.name : undefined);
|
|
131
|
+
const created = await createConversation(ctx, path, 'codex', 'codex-cloud', typeof values.name === 'string' ? values.name : undefined, choice);
|
|
122
132
|
const c = created.conversation;
|
|
123
133
|
// --computer: the conversation VM's desktop is on before Codex starts, so the thread gets the computer tools.
|
|
124
134
|
if (values.computer === true)
|
|
@@ -132,7 +142,7 @@ export async function launchCodexCloud(ctx, values, path, images = []) {
|
|
|
132
142
|
const submitted = typeof values.initialPrompt === 'string';
|
|
133
143
|
if (submitted)
|
|
134
144
|
await submitCloudInitialPrompt(ctx, c, values.initialPrompt, created.workspace.key);
|
|
135
|
-
await output(ctx, c, task, submitted);
|
|
145
|
+
await output(ctx, c, task, submitted, choice);
|
|
136
146
|
return 0;
|
|
137
147
|
}
|
|
138
148
|
export async function openCodexConversation(ctx, c) {
|
|
@@ -11,8 +11,9 @@ import { agentName as gitAgentName } from "./agent-git.js";
|
|
|
11
11
|
import { randomUUID } from 'node:crypto';
|
|
12
12
|
import { prepareAgentMachine } from "./agent-machine.js";
|
|
13
13
|
import { OperationFailedError } from '@shardflux/sdk';
|
|
14
|
-
import { launchConversationProject } from "./conversation-terminal.js";
|
|
14
|
+
import { launchConversationProject, launchProjectCommand } from "./conversation-terminal.js";
|
|
15
15
|
import { startThreadOn } from "./thread-on-computer.js";
|
|
16
|
+
import { launchT3 } from "./t3-launch.js";
|
|
16
17
|
/** L9 can register target-specific handlers after detecting the installed applications. */
|
|
17
18
|
export async function availableTargets(ctx) {
|
|
18
19
|
const { HANDLERS } = await import("./handlers.js");
|
|
@@ -35,6 +36,20 @@ export async function agentAction(ctx, action, values, args) {
|
|
|
35
36
|
}
|
|
36
37
|
export const CENTER_HANDLERS = {
|
|
37
38
|
async 'agents new'(ctx, values, [ref]) {
|
|
39
|
+
const fresh = !ref && values.name === undefined && values.like === undefined;
|
|
40
|
+
// T3 Code mode (0.17.0+): --in t3 starts the first message as a T3 thread (--on: on that computer's T3 server);
|
|
41
|
+
// --in auto opens where open_in says.
|
|
42
|
+
if (fresh && values.in === 't3')
|
|
43
|
+
return launchT3(ctx, values, str(values.path) ?? './', str(values.app));
|
|
44
|
+
if (fresh && values.in === 'auto') {
|
|
45
|
+
const next = { ...values };
|
|
46
|
+
delete next.in;
|
|
47
|
+
return launchProjectCommand(ctx, next, [str(values.path) ?? './', ...(str(values.app) ? [str(values.app)] : [])]);
|
|
48
|
+
}
|
|
49
|
+
if (values.in === 'auto') {
|
|
50
|
+
values = { ...values };
|
|
51
|
+
delete values.in;
|
|
52
|
+
}
|
|
38
53
|
// A thread on an existing conversation's computer (0.16.0+).
|
|
39
54
|
if (values.on !== undefined) {
|
|
40
55
|
if (ref || values.like !== undefined)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
-
import { getConversation, resolveConversation, conversationWorkspace, localConversations } from "./conversations.js";
|
|
2
|
+
import { conversationEnded, getConversation, resolveConversation, conversationWorkspace, localConversations } from "./conversations.js";
|
|
3
3
|
import { ensureConversationWorktree, devicePort } from "./device-bridge.js";
|
|
4
4
|
import { startProjectSync } from "./project-sync.js";
|
|
5
5
|
import { projectIdentity } from "./project-identity.js";
|
|
@@ -48,7 +48,7 @@ export const CONVERSATION_HANDLERS = {
|
|
|
48
48
|
try {
|
|
49
49
|
while (!ctx.signal.aborted) {
|
|
50
50
|
const next = await getConversation(ctx, c.id);
|
|
51
|
-
if (next
|
|
51
|
+
if (conversationEnded(next) || next.workspace_id !== c.workspace_id)
|
|
52
52
|
break;
|
|
53
53
|
await delay(1000, undefined, { signal: ctx.signal }).catch(() => undefined);
|
|
54
54
|
}
|
|
@@ -182,7 +182,7 @@ export async function mirrorProjectConversations(ctx, state = new ConversationMi
|
|
|
182
182
|
const c = all.find(c => c.id === record.conversation.id);
|
|
183
183
|
if (!c || record.handed_off || !c.harness_session_id)
|
|
184
184
|
continue;
|
|
185
|
-
const revision = JSON.stringify([c.workspace_id, c.harness_session_id, c.state, c.durable, c.last_activity_at, c.summary]);
|
|
185
|
+
const revision = JSON.stringify([c.workspace_id, c.harness_session_id, c.state, c.durable, c.last_activity_at, c.summary, c.ended_at]);
|
|
186
186
|
if (state.mirrored.get(c.id) === revision)
|
|
187
187
|
continue;
|
|
188
188
|
let retry = state.retries.get(c.id);
|
|
@@ -220,8 +220,9 @@ export async function mirrorProjectConversations(ctx, state = new ConversationMi
|
|
|
220
220
|
await mirrorConversation(ctx, { ...record, conversation: c }, source, skipped);
|
|
221
221
|
retry.failures = 0;
|
|
222
222
|
retry.retryAt = 0;
|
|
223
|
-
// An ended record may precede its VM tombstone; retry until the handoff receipt is written.
|
|
224
|
-
|
|
223
|
+
// An ended record may precede its VM tombstone; retry until the handoff receipt is written. A finished turn is
|
|
224
|
+
// `done` while its VM lives: mirrored once per revision, and again when it ends.
|
|
225
|
+
if (c.state !== 'archived' && !(c.state === 'done' && c.ended_at))
|
|
225
226
|
state.mirrored.set(c.id, revision);
|
|
226
227
|
}
|
|
227
228
|
catch (e) {
|
|
@@ -4,5 +4,10 @@ import type { Values } from './args.js';
|
|
|
4
4
|
import type { Conversation } from './conversations.js';
|
|
5
5
|
/** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
|
|
6
6
|
export declare function conversationTerminal(ctx: Ctx, ws: Workspace, c: Conversation, project: string, values: Values): Promise<number>;
|
|
7
|
+
/**
|
|
8
|
+
* `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
|
|
9
|
+
* Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
|
|
10
|
+
*/
|
|
11
|
+
export declare const launchProjectCommand: Handler;
|
|
7
12
|
/** Select a durable conversation, then use the harness's native terminal for all interaction. */
|
|
8
13
|
export declare const launchConversationProject: Handler;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { linuxTerminalSession } from "./linux-terminal.js";
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { AgentStateStore } from "./agent-state.js";
|
|
3
4
|
import { projectIdentity, projectLabels } from "./project-identity.js";
|
|
4
|
-
import { createConversation, conversationName, conversationWorkspace, conversationsUnavailable, listConversations, localConversations, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
|
|
5
|
+
import { createConversation, conversationName, conversationWorkspace, conversationsUnavailable, listConversations, localConversations, recordedChoice, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
|
|
6
|
+
import { modelChoice } from "./agent-models.js";
|
|
5
7
|
import { terminal } from "./agent-commands.js";
|
|
6
8
|
import { cloudClaudeLogin, loginClaudeInWorkspace, requireClaudeTerminal } from "./claude-cloud-login.js";
|
|
7
9
|
import { syncAgentCredentials } from "./agent-credentials.js";
|
|
@@ -16,6 +18,11 @@ import { agentReachability } from "./agent-reachability.js";
|
|
|
16
18
|
import { promptWithImages, readInitialImages, readInitialPrompt, uploadInitialImages } from "./initial-prompt.js";
|
|
17
19
|
import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
|
|
18
20
|
import { startThreadOn } from "./thread-on-computer.js";
|
|
21
|
+
import { openInChoices } from "./t3-mode.js";
|
|
22
|
+
import { launchT3 } from "./t3-launch.js";
|
|
23
|
+
import { openClaudeDesktopSession, remoteControlUrl } from "./agent-target-commands.js";
|
|
24
|
+
import { openUrl } from "./browser.js";
|
|
25
|
+
import { out } from "./commands.js";
|
|
19
26
|
/** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
|
|
20
27
|
export async function conversationTerminal(ctx, ws, c, project, values) {
|
|
21
28
|
if (c.route === 'codex-cloud')
|
|
@@ -23,10 +30,28 @@ export async function conversationTerminal(ctx, ws, c, project, values) {
|
|
|
23
30
|
await restoreConversationBinding(ctx, c, ws);
|
|
24
31
|
// A thread on another conversation's computer has its own PTY there, never the VM's own (0.16.0+).
|
|
25
32
|
const pty = ws.labels.shard_conversation && ws.labels.shard_conversation !== c.id ? conversationName(c) : undefined;
|
|
26
|
-
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 } : {}) });
|
|
33
|
+
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) });
|
|
27
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
|
|
37
|
+
* Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
|
|
38
|
+
*/
|
|
39
|
+
export const launchProjectCommand = async (ctx, values, [path = './', explicit]) => {
|
|
40
|
+
const named = explicit ?? (values.claude === true ? 'claude' : values.codex === true ? 'codex' : undefined);
|
|
41
|
+
if (values.resume !== true && values.review !== true) {
|
|
42
|
+
const project = projectIdentity(path);
|
|
43
|
+
const app = named === 'claude' || named === 'codex' ? named : new AgentStateStore(ctx.store.dir).app(project.remote ?? project.folder);
|
|
44
|
+
const where = process.platform === 'linux' && ctx.io.stdin.isTTY ? 'terminal' : openInChoices(ctx, project.slug)[app];
|
|
45
|
+
if (where === 't3')
|
|
46
|
+
return launchT3(ctx, values, path, app);
|
|
47
|
+
if (values.on === undefined && app === 'claude' && (where === 'claude-desktop' || where === 'web'))
|
|
48
|
+
return startConversation(ctx, values, [path, ...(explicit ? [explicit] : [])], where);
|
|
49
|
+
}
|
|
50
|
+
return launchConversationProject(ctx, values, [path, ...(explicit ? [explicit] : [])]);
|
|
51
|
+
};
|
|
28
52
|
/** Select a durable conversation, then use the harness's native terminal for all interaction. */
|
|
29
|
-
export const launchConversationProject =
|
|
53
|
+
export const launchConversationProject = (ctx, values, args) => startConversation(ctx, values, args);
|
|
54
|
+
async function startConversation(ctx, values, [path = './', explicit], openIn) {
|
|
30
55
|
if (values.on !== undefined) {
|
|
31
56
|
const app = explicit ?? (values.claude === true ? 'claude' : values.codex === true ? 'codex' : undefined);
|
|
32
57
|
return startThreadOn(ctx, values, path, app);
|
|
@@ -42,6 +67,8 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
|
|
|
42
67
|
const app = explicit === 'claude' || explicit === 'codex' ? explicit : store.app(project.remote ?? project.folder);
|
|
43
68
|
if (explicit && explicit !== app)
|
|
44
69
|
throw new UsageError('Choose claude or codex.');
|
|
70
|
+
// Checked before a VM is allocated; the new conversation's record carries it to its guest (0.17.0+).
|
|
71
|
+
const choice = modelChoice(values, app);
|
|
45
72
|
if (values.resume === true) {
|
|
46
73
|
// The native pickers are VM-local. C-K6's project picker must select the binding first.
|
|
47
74
|
const records = localConversations(ctx);
|
|
@@ -62,7 +89,7 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
|
|
|
62
89
|
const ws = await conversationWorkspace(ctx, next);
|
|
63
90
|
return conversationTerminal(ctx, ws, next, project.remote ?? project.folder, values);
|
|
64
91
|
}
|
|
65
|
-
if (app === 'codex')
|
|
92
|
+
if (app === 'codex' && !linuxTerminalSession(ctx, values))
|
|
66
93
|
return launchCodexCloud(ctx, values, path, images);
|
|
67
94
|
await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
|
|
68
95
|
const signedIn = app === 'claude' ? await cloudClaudeLogin(ctx) : true;
|
|
@@ -70,7 +97,7 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
|
|
|
70
97
|
requireClaudeTerminal(ctx);
|
|
71
98
|
let created;
|
|
72
99
|
try {
|
|
73
|
-
created = await createConversation(ctx, project.folder, app, 'terminal');
|
|
100
|
+
created = await createConversation(ctx, project.folder, app, 'terminal', undefined, choice);
|
|
74
101
|
}
|
|
75
102
|
catch (e) {
|
|
76
103
|
if (!conversationsUnavailable(e))
|
|
@@ -102,5 +129,17 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
|
|
|
102
129
|
if (!(await savedMachine(ctx))?.template)
|
|
103
130
|
await syncAgentConfig(ctx, ws);
|
|
104
131
|
await ensureMenubar(ctx.store.dir, ctx.apiUrl, new MenubarManager({ home: ctx.io.env.HOME, signal: ctx.signal }));
|
|
132
|
+
if (openIn) {
|
|
133
|
+
// The agent starts detached in its VM; the conversation then opens in Claude Desktop or claude.ai (Remote Control).
|
|
134
|
+
const quiet = { ...ctx, io: { ...ctx.io, stdout: { write: () => true } } };
|
|
135
|
+
await conversationTerminal(quiet, ws, c, project.remote ?? project.folder, { ...values, detach: true });
|
|
136
|
+
if (openIn === 'claude-desktop')
|
|
137
|
+
return openClaudeDesktopSession(ctx, ws, c);
|
|
138
|
+
const url = await remoteControlUrl(ctx, ws, c);
|
|
139
|
+
if (!ctx.json && !openUrl(url, ctx.io.env))
|
|
140
|
+
throw new Error(`could not open ${url}`);
|
|
141
|
+
out(ctx, `${url}\n`, { target: 'web', workspace: ws.data, conversation_id: c.id, url });
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
105
144
|
return conversationTerminal(ctx, ws, c, project.remote ?? project.folder, values);
|
|
106
|
-
}
|
|
145
|
+
}
|
package/dist/conversations.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Workspace, WorkspaceView } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx } from './commands.js';
|
|
3
|
+
import type { ModelChoice } from './agent-models.js';
|
|
3
4
|
export type Harness = 'claude' | 'codex';
|
|
4
5
|
export interface Conversation {
|
|
5
6
|
id: string;
|
|
@@ -17,6 +18,11 @@ export interface Conversation {
|
|
|
17
18
|
state: 'working' | 'needs_you' | 'unresolved' | 'done' | 'archived';
|
|
18
19
|
created_at: string;
|
|
19
20
|
ended_at: string | null;
|
|
21
|
+
landed?: {
|
|
22
|
+
how: string;
|
|
23
|
+
at: string;
|
|
24
|
+
ref: string;
|
|
25
|
+
} | null;
|
|
20
26
|
last_activity_at?: string;
|
|
21
27
|
durable?: {
|
|
22
28
|
branch_sha: string;
|
|
@@ -31,6 +37,9 @@ export interface Conversation {
|
|
|
31
37
|
base_branch?: string | null;
|
|
32
38
|
role?: string;
|
|
33
39
|
join_mode?: string | null;
|
|
40
|
+
/** The model and effort its harness starts with; null is the agent's own default (0.17.0+, APIs that keep them). */
|
|
41
|
+
model?: string | null;
|
|
42
|
+
effort?: string | null;
|
|
34
43
|
}
|
|
35
44
|
export interface LocalConversation {
|
|
36
45
|
api_url: string;
|
|
@@ -41,6 +50,11 @@ export interface LocalConversation {
|
|
|
41
50
|
/** The branch the conversation started from (0.16.0+), for APIs that do not return base_branch yet. */
|
|
42
51
|
base_branch?: string;
|
|
43
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
|
|
55
|
+
* while its VM still takes the next turn; on Codex Cloud, `done` ends the conversation.
|
|
56
|
+
*/
|
|
57
|
+
export declare const conversationEnded: (c: Pick<Conversation, "state" | "ended_at" | "route" | "landed">) => boolean;
|
|
44
58
|
export declare const conversationName: (c: Conversation) => string;
|
|
45
59
|
export declare function validateConversation(c: Conversation): Conversation;
|
|
46
60
|
export declare function localConversations(ctx: Pick<Ctx, 'store' | 'apiUrl'>): Record<string, LocalConversation>;
|
|
@@ -57,9 +71,11 @@ export declare function conversationWorkspace(ctx: Ctx, c: Conversation): Promis
|
|
|
57
71
|
*/
|
|
58
72
|
export declare function waitThreadReady(ctx: Ctx, c: Conversation, pollMs?: number): Promise<Conversation>;
|
|
59
73
|
export declare function restoreConversationBinding(ctx: Ctx, c: Conversation, ws: Workspace): Promise<void>;
|
|
60
|
-
export declare function createConversationRecord(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, allocated?: (workspace: WorkspaceView) => void, idempotencyKey?: string): Promise<Conversation>;
|
|
61
|
-
export declare function createConversation(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string): Promise<{
|
|
74
|
+
export declare function createConversationRecord(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, allocated?: (workspace: WorkspaceView) => void, idempotencyKey?: string, choice?: ModelChoice): Promise<Conversation>;
|
|
75
|
+
export declare function createConversation(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, choice?: ModelChoice): Promise<{
|
|
62
76
|
conversation: Conversation;
|
|
63
77
|
workspace: Workspace;
|
|
64
78
|
}>;
|
|
65
79
|
export declare function resumeConversation(ctx: Ctx, c: Conversation): Promise<Conversation>;
|
|
80
|
+
/** The model and effort a conversation's harness starts with, as its record keeps them. */
|
|
81
|
+
export declare const recordedChoice: (c: Pick<Conversation, "model" | "effort">) => ModelChoice;
|
package/dist/conversations.js
CHANGED
|
@@ -9,6 +9,11 @@ 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
|
+
/**
|
|
13
|
+
* Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
|
|
14
|
+
* while its VM still takes the next turn; on Codex Cloud, `done` ends the conversation.
|
|
15
|
+
*/
|
|
16
|
+
export const conversationEnded = (c) => !!c.ended_at || c.state === 'archived' || c.landed?.how === 'discarded' || (c.route === 'codex-cloud' && c.state === 'done');
|
|
12
17
|
export const conversationName = (c) => {
|
|
13
18
|
const name = c.key.split('/').at(-1);
|
|
14
19
|
if (!/^[a-z0-9][a-z0-9-]{0,47}$/.test(name) || c.branch !== `shard/${name}`)
|
|
@@ -116,7 +121,7 @@ export async function restoreConversationBinding(ctx, c, ws) {
|
|
|
116
121
|
if (result.exitCode !== 0)
|
|
117
122
|
throw new Error(`Conversation restore failed: ${result.stderr.trim()}`);
|
|
118
123
|
}
|
|
119
|
-
export async function createConversationRecord(ctx, folder, harness, route, name, allocated, idempotencyKey = randomUUID()) {
|
|
124
|
+
export async function createConversationRecord(ctx, folder, harness, route, name, allocated, idempotencyKey = randomUUID(), choice = {}) {
|
|
120
125
|
const project = projectIdentity(folder);
|
|
121
126
|
await ensureProjectGit(ctx, project.folder);
|
|
122
127
|
// Both the conversation API and its legacy workspace fallback need a usable local HEAD.
|
|
@@ -128,16 +133,16 @@ export async function createConversationRecord(ctx, folder, harness, route, name
|
|
|
128
133
|
const seed = await twinSeed(ctx.store.dir, project.folder, head, ctx.store.profile(ctx.apiUrl).project_id).catch(() => ({ commit: head, snapshot: false }));
|
|
129
134
|
const commit = seed.commit;
|
|
130
135
|
const response = await conversationClient(ctx).conversations.create({ repoRemote: project.remote ?? `file://${project.folder}`, harness, route, mode: 'new',
|
|
131
|
-
base: { commit, ...(branch ? { branch } : {}) }, ...(name ? { name } : {}), projectId: ctx.store.profile(ctx.apiUrl).project_id }, { idempotencyKey, signal: ctx.signal });
|
|
136
|
+
base: { commit, ...(branch ? { branch } : {}) }, ...(name ? { name } : {}), ...choice, projectId: ctx.store.profile(ctx.apiUrl).project_id }, { idempotencyKey, signal: ctx.signal });
|
|
132
137
|
const c = validateConversation({ ...response.conversation, restore: response.restore });
|
|
133
138
|
allocated?.(response.workspace);
|
|
134
139
|
rememberTwinSeed(ctx.store.dir, c.id, project.folder, seed, branch);
|
|
135
140
|
rememberConversation(ctx, c, project.folder, false, branch);
|
|
136
141
|
return c;
|
|
137
142
|
}
|
|
138
|
-
export async function createConversation(ctx, folder, harness, route, name) {
|
|
143
|
+
export async function createConversation(ctx, folder, harness, route, name, choice = {}) {
|
|
139
144
|
const project = projectIdentity(folder);
|
|
140
|
-
const c = await createConversationRecord(ctx, project.folder, harness, route, name);
|
|
145
|
+
const c = await createConversationRecord(ctx, project.folder, harness, route, name, undefined, undefined, choice);
|
|
141
146
|
try {
|
|
142
147
|
const ws = await conversationWorkspace(ctx, c);
|
|
143
148
|
// This is a one-time seed. A conversation never watches or writes the owner's checkout.
|
|
@@ -163,8 +168,10 @@ export async function createConversation(ctx, folder, harness, route, name) {
|
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
export async function resumeConversation(ctx, c) {
|
|
166
|
-
if (c
|
|
171
|
+
if (!conversationEnded(c))
|
|
167
172
|
return getConversation(ctx, c.id);
|
|
168
173
|
const response = await conversationClient(ctx).conversations.resume(c.id, { idempotencyKey: randomUUID(), signal: ctx.signal });
|
|
169
174
|
return validateConversation({ ...response.conversation, restore: response.restore });
|
|
170
175
|
}
|
|
176
|
+
/** The model and effort a conversation's harness starts with, as its record keeps them. */
|
|
177
|
+
export const recordedChoice = (c) => ({ ...(c.model ? { model: c.model } : {}), ...(c.effort ? { effort: c.effort } : {}) });
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import type { Ctx } from './commands.js';
|
|
3
|
+
declare const exec: typeof execFile.__promisify__;
|
|
4
|
+
export interface DesktopStatus {
|
|
5
|
+
installed: boolean;
|
|
6
|
+
version: string | null;
|
|
7
|
+
running: boolean;
|
|
8
|
+
pids: number[];
|
|
9
|
+
login: 'on' | 'off';
|
|
10
|
+
}
|
|
11
|
+
export declare function linuxDesktopPackage(arch?: NodeJS.Architecture): string | null;
|
|
12
|
+
export declare function graphicalSession(env: NodeJS.ProcessEnv): boolean;
|
|
13
|
+
export declare class LinuxDesktopManager {
|
|
14
|
+
#private;
|
|
15
|
+
readonly home: string;
|
|
16
|
+
readonly app: string;
|
|
17
|
+
constructor(options?: {
|
|
18
|
+
home?: string;
|
|
19
|
+
source?: () => string | null;
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
exec?: typeof exec;
|
|
22
|
+
spawn?: typeof spawn;
|
|
23
|
+
isProcess?: (pid: number, executable: string) => boolean;
|
|
24
|
+
});
|
|
25
|
+
status(dir: string): Promise<DesktopStatus>;
|
|
26
|
+
packagedVersion(): string | null;
|
|
27
|
+
login(on: boolean): Promise<void>;
|
|
28
|
+
stop(dir: string): Promise<DesktopStatus>;
|
|
29
|
+
refresh(dir: string, api: string): Promise<{
|
|
30
|
+
result: string;
|
|
31
|
+
from?: string | null;
|
|
32
|
+
to?: string | null;
|
|
33
|
+
}>;
|
|
34
|
+
start(dir: string, api: string, view?: string, runtime?: {
|
|
35
|
+
node: string;
|
|
36
|
+
script: string;
|
|
37
|
+
}): Promise<DesktopStatus>;
|
|
38
|
+
}
|
|
39
|
+
export declare function tryLinuxDesktop(ctx: Ctx): Promise<boolean>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, chmodSync, mkdtempSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { UsageError } from "./errors.js";
|
|
8
|
+
import { compareVersions } from '@shardflux/sdk';
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
export function linuxDesktopPackage(arch = process.arch) {
|
|
11
|
+
if (!['x64', 'arm64'].includes(arch))
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
return join(dirname(createRequire(import.meta.url).resolve(`@shardflux/desktop-linux-${arch}/package.json`)), 'app');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function graphicalSession(env) { return !!(env.DISPLAY || env.WAYLAND_DISPLAY); }
|
|
21
|
+
const desktopString = (text) => `"${text.replace(/(["`$\\])/g, '\\$1')}"`;
|
|
22
|
+
export class LinuxDesktopManager {
|
|
23
|
+
home;
|
|
24
|
+
app;
|
|
25
|
+
#source;
|
|
26
|
+
#env;
|
|
27
|
+
#exec;
|
|
28
|
+
#spawn;
|
|
29
|
+
#isProcess;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.home = options.home ?? homedir();
|
|
32
|
+
this.app = join(this.home, '.local', 'share', 'shardflux', 'desktop');
|
|
33
|
+
this.#source = options.source ?? linuxDesktopPackage;
|
|
34
|
+
this.#env = { ...process.env, ...options.env, HOME: this.home };
|
|
35
|
+
this.#exec = options.exec ?? exec;
|
|
36
|
+
this.#spawn = options.spawn ?? spawn;
|
|
37
|
+
this.#isProcess = options.isProcess ?? ((pid, executable) => { try {
|
|
38
|
+
if (readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0')[0] !== executable)
|
|
39
|
+
return false;
|
|
40
|
+
process.kill(pid, 0);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
} });
|
|
46
|
+
}
|
|
47
|
+
#version(path) {
|
|
48
|
+
try {
|
|
49
|
+
const manifest = JSON.parse(readFileSync(join(path, 'manifest.json'), 'utf8'));
|
|
50
|
+
return manifest.id === 'dev.shardflux.Desktop' && typeof manifest.version === 'string' ? manifest.version : null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async status(dir) {
|
|
57
|
+
const version = this.#version(this.app), pids = [];
|
|
58
|
+
try {
|
|
59
|
+
const pid = Number(readFileSync(join(dir, 'desktop-linux.pid'), 'utf8'));
|
|
60
|
+
// Recycled PIDs must never make us stop an unrelated process.
|
|
61
|
+
if (Number.isInteger(pid) && pid > 0 && this.#isProcess(pid, join(this.app, 'shardflux-desktop')))
|
|
62
|
+
pids.push(pid);
|
|
63
|
+
}
|
|
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' };
|
|
66
|
+
}
|
|
67
|
+
packagedVersion() { const path = this.#source(); return path ? this.#version(path) : null; }
|
|
68
|
+
async login(on) {
|
|
69
|
+
const file = join(this.home, '.config/autostart/shardflux.desktop');
|
|
70
|
+
if (!on) {
|
|
71
|
+
rmSync(file, { force: true });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
75
|
+
const entry = join(this.home, '.local/share/applications/dev.shardflux.Desktop.desktop');
|
|
76
|
+
if (!existsSync(entry))
|
|
77
|
+
throw new UsageError('Run shard menubar start first.');
|
|
78
|
+
cpSync(entry, file);
|
|
79
|
+
}
|
|
80
|
+
async stop(dir) {
|
|
81
|
+
const status = await this.status(dir);
|
|
82
|
+
if (status.running) {
|
|
83
|
+
await this.#exec(join(this.app, 'shardflux-desktop'), ['--control', 'stop'], { env: { ...this.#env, SHARDFLUX_CONFIG_DIR: dir }, timeout: 5_000 });
|
|
84
|
+
for (let i = 0; i < 30; i++) {
|
|
85
|
+
await new Promise(r => setTimeout(r, 100));
|
|
86
|
+
if (!(await this.status(dir)).running)
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if ((await this.status(dir)).running)
|
|
90
|
+
throw new UsageError('Shardflux is still stopping. Try shard menubar status.');
|
|
91
|
+
}
|
|
92
|
+
return this.status(dir);
|
|
93
|
+
}
|
|
94
|
+
async refresh(dir, api) {
|
|
95
|
+
const before = await this.status(dir), version = this.packagedVersion();
|
|
96
|
+
if (!before.installed || !version)
|
|
97
|
+
return { result: 'off' };
|
|
98
|
+
if ((compareVersions(before.version, version) ?? 0) >= 0)
|
|
99
|
+
return { result: 'current', from: before.version, to: version };
|
|
100
|
+
if (before.running) {
|
|
101
|
+
try {
|
|
102
|
+
const { stdout } = await this.#exec(join(this.app, 'shardflux-desktop'), ['--control', 'idle'], { env: this.#env, timeout: 5000 });
|
|
103
|
+
if (!JSON.parse(stdout).idle)
|
|
104
|
+
return { result: 'busy', from: before.version, to: version };
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return { result: 'busy', from: before.version, to: version };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const after = await this.start(dir, api);
|
|
111
|
+
return { result: 'updated', from: before.version, to: after.version };
|
|
112
|
+
}
|
|
113
|
+
async start(dir, api, view = 'onboarding', runtime = { node: process.execPath, script: process.argv[1] }) {
|
|
114
|
+
const source = this.#source(), version = source && this.#version(source);
|
|
115
|
+
if (!source || !version)
|
|
116
|
+
throw new UsageError('Install the window with npm i -g shardflux --include=optional, then run shard.');
|
|
117
|
+
if (!graphicalSession(this.#env))
|
|
118
|
+
throw new UsageError('Open shard from your desktop session to show the window.');
|
|
119
|
+
const current = await this.status(dir);
|
|
120
|
+
if (!current.version || (compareVersions(current.version, version) ?? -1) < 0) {
|
|
121
|
+
if (existsSync(this.app) && (lstatSync(this.app).isSymbolicLink() || !current.version))
|
|
122
|
+
throw new UsageError(`Refusing to replace an unrelated directory at ${this.app}.`);
|
|
123
|
+
// The loader probes every shared library before any installed copy is stopped or replaced.
|
|
124
|
+
await this.#exec('/usr/bin/ldd', [join(source, 'shardflux-desktop')], { timeout: 5_000 }).then(({ stdout }) => { if (stdout.includes('not found'))
|
|
125
|
+
throw new UsageError('The Shardflux window needs libwebkit2gtk-4.1-0 and libayatana-appindicator3-1. Install those desktop libraries, then run shard.'); });
|
|
126
|
+
mkdirSync(dirname(this.app), { recursive: true });
|
|
127
|
+
const staging = mkdtempSync(join(dirname(this.app), '.desktop-'));
|
|
128
|
+
const staged = join(staging, 'app'), backup = join(staging, 'previous');
|
|
129
|
+
try {
|
|
130
|
+
cpSync(source, staged, { recursive: true });
|
|
131
|
+
chmodSync(join(staged, 'shardflux-desktop'), 0o755);
|
|
132
|
+
if (current.running)
|
|
133
|
+
await this.stop(dir);
|
|
134
|
+
if (existsSync(this.app))
|
|
135
|
+
renameSync(this.app, backup);
|
|
136
|
+
try {
|
|
137
|
+
renameSync(staged, this.app);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (existsSync(backup))
|
|
141
|
+
renameSync(backup, this.app);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
rmSync(staging, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
150
|
+
const env = { ...this.#env, SHARDFLUX_CONFIG_DIR: dir, SHARDFLUX_API_URL: api, SHARDFLUX_NODE: runtime.node, SHARDFLUX_CLI: runtime.script };
|
|
151
|
+
const desktop = join(this.home, '.local/share/applications/dev.shardflux.Desktop.desktop');
|
|
152
|
+
mkdirSync(dirname(desktop), { recursive: true });
|
|
153
|
+
const args = ['/usr/bin/env', `SHARDFLUX_CONFIG_DIR=${dir}`, `SHARDFLUX_API_URL=${api}`, `SHARDFLUX_NODE=${runtime.node}`, `SHARDFLUX_CLI=${runtime.script}`, join(this.app, 'shardflux-desktop')];
|
|
154
|
+
writeFileSync(desktop, `[Desktop Entry]\nType=Application\nName=Shardflux\nComment=Your agents, a shortcut away\nExec=${args.map(desktopString).join(' ')}\nIcon=${join(this.app, 'ui/assets/shardflux-icon.png')}\nTerminal=false\nCategories=Development;\nStartupWMClass=dev.shardflux.Desktop\n`, { mode: 0o644 });
|
|
155
|
+
if ((await this.status(dir)).running)
|
|
156
|
+
await this.#exec(join(this.app, 'shardflux-desktop'), ['--control', view], { env, timeout: 5_000 });
|
|
157
|
+
else {
|
|
158
|
+
const child = this.#spawn(join(this.app, 'shardflux-desktop'), [view], { env, detached: true, stdio: 'ignore' });
|
|
159
|
+
await new Promise((resolve, reject) => { child.once('error', reject); child.once('spawn', resolve); });
|
|
160
|
+
child.unref();
|
|
161
|
+
for (let i = 0; i < 40; i++) {
|
|
162
|
+
if ((await this.status(dir)).running)
|
|
163
|
+
break;
|
|
164
|
+
await new Promise(r => setTimeout(r, 100));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const result = await this.status(dir);
|
|
168
|
+
if (!result.running)
|
|
169
|
+
throw new UsageError('The Shardflux window did not open. Run shard menubar start from your desktop session.');
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export async function tryLinuxDesktop(ctx) {
|
|
174
|
+
if (process.platform !== 'linux' || ctx.json || ctx.io.env.SHARDFLUX_DESKTOP_CHILD === '1' || !graphicalSession(ctx.io.env))
|
|
175
|
+
return false;
|
|
176
|
+
try {
|
|
177
|
+
const manager = new LinuxDesktopManager({ home: ctx.io.env.HOME, env: ctx.io.env });
|
|
178
|
+
await manager.start(ctx.store.dir, ctx.apiUrl);
|
|
179
|
+
await manager.login(true);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
ctx.io.stderr.write(`${err instanceof Error ? err.message : 'The Shardflux window could not open.'}\n`);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DeviceApproval, DeviceChoice } from './device-bridge.js';
|
|
2
|
+
import type { Ctx, Handler } from './commands.js';
|
|
3
|
+
type Pending = DeviceApproval & {
|
|
4
|
+
nonce: string;
|
|
5
|
+
pid: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function pendingDeviceApprovals(dir: string): Pending[];
|
|
8
|
+
export declare function answerDeviceApproval(dir: string, id: string, choice: DeviceChoice): void;
|
|
9
|
+
/** Local user-owned files carry approvals; cloud data can never answer its own request. */
|
|
10
|
+
export declare function askLocalDeviceApproval(ctx: Ctx, approval: DeviceApproval): Promise<DeviceChoice>;
|
|
11
|
+
export declare const DEVICE_HANDLERS: Record<string, Handler>;
|
|
12
|
+
export {};
|