@shardflux/cli 0.16.0 → 0.17.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 +62 -2
- package/README.md +43 -5
- 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 +117 -16
- package/dist/agent-targets.d.ts +2 -0
- package/dist/agent-targets.js +10 -4
- 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/device-approvals.d.ts +12 -0
- package/dist/device-approvals.js +73 -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/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 +29 -6
- package/dist/onboarding.d.ts +2 -0
- package/dist/onboarding.js +53 -5
- package/dist/t3-launch.d.ts +37 -0
- package/dist/t3-launch.js +133 -0
- package/dist/t3-mode.d.ts +121 -0
- package/dist/t3-mode.js +654 -0
- package/dist/t3-phone.d.ts +17 -0
- package/dist/t3-phone.js +15 -0
- 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 +12 -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 +3 -3
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { CLI_VERSION } from "./args.js";
|
|
6
|
+
import { writePrivateFile } from "./credentials.js";
|
|
7
|
+
import { runDeviceHelper } from "./device-helper.js";
|
|
8
|
+
export const HELPER_UNIT = 'shardflux-helper.service';
|
|
9
|
+
export const helperUnitPath = (home) => join(home, '.config/systemd/user', HELPER_UNIT);
|
|
10
|
+
const quote = (s) => '"' + s.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%').replaceAll('\n', '\\n').replaceAll('\r', '\\r') + '"';
|
|
11
|
+
/** No credentials in the unit: the helper reads the same private store as shard. */
|
|
12
|
+
export function helperUnit(ctx) {
|
|
13
|
+
if (!ctx.runtime)
|
|
14
|
+
throw new Error('An installed shard is required.');
|
|
15
|
+
let script = ctx.runtime.script;
|
|
16
|
+
try {
|
|
17
|
+
script = realpathSync(script);
|
|
18
|
+
}
|
|
19
|
+
catch { /* preserve the supplied installed path */ }
|
|
20
|
+
const env = { HOME: ctx.io.env.HOME, PATH: ctx.io.env.PATH ?? '/usr/local/bin:/usr/bin:/bin', SHARDFLUX_CONFIG_DIR: ctx.store.dir,
|
|
21
|
+
SHARDFLUX_API_URL: ctx.apiUrl, SHARDFLUX_NO_UPDATE_CHECK: '1',
|
|
22
|
+
...Object.fromEntries(['DISPLAY', 'WAYLAND_DISPLAY', 'DBUS_SESSION_BUS_ADDRESS', 'XDG_RUNTIME_DIR', 'CODEX_HOME', 'CLAUDE_CONFIG_DIR'].flatMap(k => ctx.io.env[k] ? [[k, ctx.io.env[k]]] : [])) };
|
|
23
|
+
return `# Shardflux CLI ${CLI_VERSION}\n[Unit]\nDescription=Shardflux background helper\nAfter=network-online.target\n\n[Service]\nType=simple\nExecStart=${[ctx.runtime.node, script, 'device-helper', '--service'].map(s => quote(s.replaceAll('$', '$$'))).join(' ')}\n${Object.entries(env).map(([k, v]) => `Environment=${quote(`${k}=${v}`)}`).join('\n')}\nRestart=on-failure\nRestartSec=5\nTimeoutStopSec=10\nUMask=0077\n\n[Install]\nWantedBy=default.target\n`;
|
|
24
|
+
}
|
|
25
|
+
export function installLinuxHelper(ctx, deps = {}) {
|
|
26
|
+
if ((deps.platform ?? process.platform) !== 'linux' || !ctx.runtime || !ctx.io.env.HOME)
|
|
27
|
+
return false;
|
|
28
|
+
const run = deps.run ?? (args => spawnSync('systemctl', ['--user', ...args], { env: ctx.io.env, stdio: 'ignore', timeout: 5000 }).status === 0);
|
|
29
|
+
// Probe before writing: containers without a user manager use the command-lifetime helper.
|
|
30
|
+
if (!run(['show-environment']))
|
|
31
|
+
return false;
|
|
32
|
+
const file = helperUnitPath(ctx.io.env.HOME), text = helperUnit(ctx);
|
|
33
|
+
const receipt = join(ctx.store.dir, 'linux-helper-started'), digest = createHash('sha256').update(text).digest('hex');
|
|
34
|
+
let before = '', started = '';
|
|
35
|
+
try {
|
|
36
|
+
before = readFileSync(file, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch { /* first sign-in */ }
|
|
39
|
+
try {
|
|
40
|
+
started = readFileSync(receipt, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
catch { /* no successful start yet */ }
|
|
43
|
+
if (before !== text) {
|
|
44
|
+
writePrivateFile(dirname(file), file, text);
|
|
45
|
+
if (!run(['daemon-reload']) || !run(['enable', '--now', HELPER_UNIT]))
|
|
46
|
+
return false;
|
|
47
|
+
if (before && !run(['restart', HELPER_UNIT]))
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
else if (!run(['is-active', '--quiet', HELPER_UNIT])) {
|
|
51
|
+
if (!run(['enable', '--now', HELPER_UNIT]))
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
else if (started !== digest && !run(['restart', HELPER_UNIT]))
|
|
55
|
+
return false;
|
|
56
|
+
// A failed restart must be retried by the next command even though the new unit was already written.
|
|
57
|
+
if (started !== digest)
|
|
58
|
+
writePrivateFile(ctx.store.dir, receipt, digest);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
/** The caller owns fallback lifetime; never leave an unmanaged background process behind. */
|
|
62
|
+
export function startLinuxHelper(ctx) {
|
|
63
|
+
const noop = () => Promise.resolve();
|
|
64
|
+
if (process.platform !== 'linux' || !ctx.runtime)
|
|
65
|
+
return noop;
|
|
66
|
+
try {
|
|
67
|
+
ctx.apiKey();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return noop;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
if (installLinuxHelper(ctx))
|
|
74
|
+
return noop;
|
|
75
|
+
}
|
|
76
|
+
catch { /* use the command-lifetime helper */ }
|
|
77
|
+
const notice = join(ctx.store.dir, 'linux-helper-notice');
|
|
78
|
+
try {
|
|
79
|
+
readFileSync(notice);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
ctx.io.stderr.write('Shardflux helper runs while shard commands are open on this computer.\n');
|
|
83
|
+
writePrivateFile(ctx.store.dir, notice, '1\n');
|
|
84
|
+
}
|
|
85
|
+
const abort = new AbortController();
|
|
86
|
+
const work = runDeviceHelper({ ...ctx, signal: AbortSignal.any([ctx.signal, abort.signal]) }, { service: true }).catch(() => undefined);
|
|
87
|
+
return async () => { abort.abort(); await work; };
|
|
88
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Ctx } from './commands.js';
|
|
2
|
+
import type { Values } from './args.js';
|
|
3
|
+
/** Linux terminals use the existing native Codex harness; explicit app routes keep Cloud. */
|
|
4
|
+
export declare function linuxTerminalSession(ctx: Pick<Ctx, 'io' | 'json'>, values: Values, platform?: NodeJS.Platform): boolean;
|
|
5
|
+
export declare const shellWord: (s: string) => string;
|
|
6
|
+
export declare function linuxTerminalCommand(argv: string[], env: Record<string, string | undefined>): {
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
} | null;
|
|
10
|
+
/** Invoked before cloud work: the new terminal owns the attach, credentials and any prompts. */
|
|
11
|
+
export declare function openLinuxTerminal(ctx: Ctx, ref: string, app?: string): Promise<number>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { accessSync, constants } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
/** Linux terminals use the existing native Codex harness; explicit app routes keep Cloud. */
|
|
5
|
+
export function linuxTerminalSession(ctx, values, platform = process.platform) {
|
|
6
|
+
return platform === 'linux' && (values.in === 'terminal' || (values.in === undefined && !!ctx.io.stdin.isTTY && !ctx.json));
|
|
7
|
+
}
|
|
8
|
+
export const shellWord = (s) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
9
|
+
export function linuxTerminalCommand(argv, env) {
|
|
10
|
+
const executable = (name) => name.includes('/') ? [name] : (env.PATH ?? '').split(':').filter(Boolean).map(p => join(p, name));
|
|
11
|
+
const exists = (name) => executable(name).some(p => { try {
|
|
12
|
+
accessSync(p, constants.X_OK);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
} });
|
|
18
|
+
// The shell keeps the window open on an attach error so the user can read and retry it.
|
|
19
|
+
const attach = ['sh', '-c', `${argv.map(shellWord).join(' ')}; status=$?; if [ "$status" -ne 0 ]; then printf '\nPress Enter to close.'; read answer; fi; exit "$status"`];
|
|
20
|
+
for (const command of [...new Set([env.TERMINAL?.trim(), 'x-terminal-emulator', 'gnome-terminal'].filter((s) => !!s))]) {
|
|
21
|
+
if (exists(command))
|
|
22
|
+
return { command, args: [command === 'gnome-terminal' ? '--' : '-e', ...attach] };
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/** Invoked before cloud work: the new terminal owns the attach, credentials and any prompts. */
|
|
27
|
+
export async function openLinuxTerminal(ctx, ref, app) {
|
|
28
|
+
const words = ['agents', 'open', ref, '--in', 'terminal', ...(app ? ['--app', app] : [])];
|
|
29
|
+
const argv = ctx.runtime ? [ctx.runtime.node, ctx.runtime.script, '--api-url', ctx.apiUrl, ...words] : ['shard', ...words];
|
|
30
|
+
const choice = linuxTerminalCommand(argv, ctx.io.env);
|
|
31
|
+
if (choice) {
|
|
32
|
+
const child = spawn(choice.command, choice.args, { env: ctx.io.env, cwd: process.cwd(), detached: true, stdio: 'ignore' });
|
|
33
|
+
try {
|
|
34
|
+
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
|
|
35
|
+
child.unref();
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
catch { /* print a runnable command if the terminal could not start */ }
|
|
39
|
+
}
|
|
40
|
+
ctx.io.stdout.write(`${['shard', '--api-url', ctx.apiUrl, ...words].map(shellWord).join(' ')}\n`);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -13,12 +13,15 @@
|
|
|
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, PROJECT_COMMAND, commandHelp, parseCommandLine, resolveApiUrl, resolveWake, resolveWakeTimeout } from "./args.js";
|
|
16
|
+
import { CLI_VERSION, CENTER_COMMAND, ONBOARDING_COMMAND, PROJECT_COMMAND, commandHelp, parseCommandLine, resolveApiUrl, resolveWake, resolveWakeTimeout } from "./args.js";
|
|
17
17
|
import { commandCenter } from "./command-center.js";
|
|
18
|
-
import {
|
|
18
|
+
import { launchProjectCommand } from "./conversation-terminal.js";
|
|
19
19
|
import { runAcp } from "./acp.js";
|
|
20
20
|
import { registerInstalledT3 } from "./t3-provider.js";
|
|
21
|
+
import { refreshForwarderAgent } from "./cli-proxy.js";
|
|
21
22
|
import { runAcpLogin } from "./acp-login.js";
|
|
23
|
+
import { terminalOnboarding } from "./terminal-onboarding.js";
|
|
24
|
+
import { startLinuxHelper } from "./linux-helper.js";
|
|
22
25
|
import { runDeviceHelper } from "./device-helper.js";
|
|
23
26
|
import { housekeeping, startupHousekeeping } from "./agent-git-commands.js";
|
|
24
27
|
import { CredentialStore, configDir } from "./credentials.js";
|
|
@@ -660,7 +663,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
660
663
|
refreshMenubarInBackground({ dir: store.dir, home: io.env.HOME || homedir(), runtime: opts.runtime, env: { ...process.env, ...io.env } });
|
|
661
664
|
}
|
|
662
665
|
// The update check runs next to the command (not for `version`, which reports it itself).
|
|
663
|
-
if (!updateCheckDisabled(io.env) && !['version', 'update', 'ssh-proxy', 'acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh') {
|
|
666
|
+
if (!updateCheckDisabled(io.env) && !['version', 'update', 'ssh-proxy', 'acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh' && command !== 'cli-proxy forward') {
|
|
664
667
|
try {
|
|
665
668
|
updates = new UpdateCheck({ dir: store.dir, apiUrl, distribution, fetch: base, userAgent, registry: npmRegistry(io.env) });
|
|
666
669
|
updates.start();
|
|
@@ -673,11 +676,28 @@ export async function run(argv, io, opts = {}) {
|
|
|
673
676
|
catch (err) {
|
|
674
677
|
return report(err);
|
|
675
678
|
}
|
|
676
|
-
|
|
679
|
+
let stopHelper;
|
|
680
|
+
const startHelper = () => {
|
|
681
|
+
if (stopHelper)
|
|
682
|
+
return;
|
|
683
|
+
try {
|
|
684
|
+
ctx.apiKey();
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
stopHelper = startLinuxHelper(ctx);
|
|
690
|
+
};
|
|
691
|
+
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 ? (ctx, values) => terminalOnboarding(ctx, values, { startHelper }) : parsed.command === CENTER_COMMAND ? commandCenter : HANDLERS[parsed.command.path.join(' ')];
|
|
677
692
|
if (!handler)
|
|
678
693
|
return report(new UsageError(`unknown command "${parsed.command.path.join(' ')}"`));
|
|
679
|
-
|
|
694
|
+
// Long-running helpers other programs start (an editor, launchd, the menu bar) skip the per-command housekeeping.
|
|
695
|
+
if (!['acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh' && command !== 'cli-proxy forward') {
|
|
696
|
+
// An updated shard restarts this computer's CLIProxyAPI forwarder on itself (a no-op unless the version changed).
|
|
697
|
+
await refreshForwarderAgent(io.env, ctx.store.dir).catch(() => false);
|
|
680
698
|
await housekeeping(ctx);
|
|
699
|
+
startHelper();
|
|
700
|
+
}
|
|
681
701
|
const running = handler(ctx, parsed.values, parsed.positionals).then((code) => ({ code }), (err) => ({ err }));
|
|
682
702
|
// On Ctrl-C give the command a moment to clean up (exec sends a cancel), then stop regardless.
|
|
683
703
|
const interrupted = new Promise((resolve) => {
|
|
@@ -712,6 +732,9 @@ export async function run(argv, io, opts = {}) {
|
|
|
712
732
|
if (notice)
|
|
713
733
|
io.stderr.write(notice);
|
|
714
734
|
}
|
|
735
|
+
if (!['acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh' && command !== 'cli-proxy forward')
|
|
736
|
+
startHelper();
|
|
737
|
+
await stopHelper?.();
|
|
715
738
|
return code;
|
|
716
739
|
}
|
|
717
740
|
/**
|
|
@@ -738,7 +761,7 @@ export async function main(opts = {}) {
|
|
|
738
761
|
const runtime = { node: process.execPath, script: process.argv[1] ?? '' };
|
|
739
762
|
// The menu bar's update button runs this shard; it finds it here (macOS only, written when it changes).
|
|
740
763
|
if (process.platform === 'darwin' && runtime.script)
|
|
741
|
-
recordInstall(configDir(process.env), runtime, opts.distribution ?? CLI_DISTRIBUTION);
|
|
764
|
+
recordInstall(configDir(process.env), runtime, opts.distribution ?? CLI_DISTRIBUTION, process.env);
|
|
742
765
|
const code = await run(process.argv.slice(2), { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin, env: process.env }, { signal: controller.signal, runtime, ...(opts.distribution ? { distribution: opts.distribution } : {}) });
|
|
743
766
|
await Promise.all([flush(process.stdout), flush(process.stderr)]);
|
|
744
767
|
process.exit(code);
|
package/dist/onboarding.d.ts
CHANGED
|
@@ -26,10 +26,12 @@ type CredentialRow = {
|
|
|
26
26
|
checked_at: string;
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
|
+
/** `url`: a page the person opens (the T3 Connect sign-in), instead of a command. */
|
|
29
30
|
type Action = {
|
|
30
31
|
title: string;
|
|
31
32
|
command?: string[];
|
|
32
33
|
retry?: boolean;
|
|
34
|
+
url?: string;
|
|
33
35
|
};
|
|
34
36
|
export interface Connection {
|
|
35
37
|
id: 'account' | AgentApp | 't3';
|
package/dist/onboarding.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { delimiter, join, resolve, basename } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
4
5
|
import { resolveWorkspace } from "./commands.js";
|
|
6
|
+
import { AgentStateStore } from "./agent-state.js";
|
|
7
|
+
import { describeError } from "./errors.js";
|
|
8
|
+
import { t3PhoneLink } from "./t3-phone.js";
|
|
5
9
|
import { readAgentCredentials, syncAgentCredentials } from "./agent-credentials.js";
|
|
6
10
|
import { getConversation } from "./conversations.js";
|
|
7
11
|
import { codexHealth, readCodexConnector, restartCodexConnector } from "./codex-cloud.js";
|
|
@@ -79,15 +83,59 @@ export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.
|
|
|
79
83
|
return { ...base, mode: 'provider', state: 'action', fact: error instanceof Error ? error.message : 'T3 Code’s settings could not be updated.', action: { title: 'Check again' } };
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
|
-
// Stable T3: an SSH environment per project; the newest one this API set up is the one to add.
|
|
86
|
+
// Stable T3: an SSH environment per project; the newest one this API set up is the one to add. They are recorded in
|
|
87
|
+
// agents.json (targets.json keeps T3 out of its manifest); a manifest entry from an older CLI still counts.
|
|
88
|
+
const recorded = new AgentStateStore(ctx.store.dir).t3Environments(ctx.apiUrl).at(-1);
|
|
83
89
|
const manifest = await jsonFile(join(ctx.store.dir, 'targets.json'));
|
|
84
90
|
const entries = Array.isArray(manifest?.entries) ? manifest.entries : [];
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
const legacy = entries.filter(e => e.api_url === ctx.apiUrl && Array.isArray(e.targets) && e.targets.includes('t3') && typeof e.host === 'string').at(-1);
|
|
92
|
+
const host = recorded?.host ?? legacy?.host;
|
|
93
|
+
const workspaceKey = recorded?.workspace ?? (typeof legacy?.workspace === 'string' ? legacy.workspace : undefined);
|
|
94
|
+
if (host) {
|
|
95
|
+
const desktop = `In T3 Code: Add environment → SSH → ${host}`;
|
|
96
|
+
const phone = await t3Phone(ctx, workspaceKey);
|
|
97
|
+
if (!phone)
|
|
98
|
+
return { ...base, mode: 'ssh', state: 'verified', fact: desktop };
|
|
99
|
+
return { ...base, mode: 'ssh', ...phone, fact: phone.state === 'verified' ? `${desktop}. On your phone: ${phone.fact}` : phone.fact };
|
|
100
|
+
}
|
|
88
101
|
// An SSH environment is per project (shardflux-<project>-main): it is set up once a project is picked.
|
|
89
102
|
return { ...base, mode: 'ssh', state: 'info', fact: 'Available after you pick a project.', next: shard(['agents', 'new', '--in', 't3', '--path', '<project>']) };
|
|
90
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* The T3 app on the phone (T3 Connect, 0.17.0+): the workspace is linked with the person's one T3 Connect sign-in. The
|
|
106
|
+
* sign-in is a page to open once; afterwards every project appears in the T3 app by itself. Undefined when the API has
|
|
107
|
+
* no T3 Connect: the desktop route is the whole card then.
|
|
108
|
+
*/
|
|
109
|
+
async function t3Phone(ctx, key) {
|
|
110
|
+
if (!key)
|
|
111
|
+
return undefined;
|
|
112
|
+
try {
|
|
113
|
+
const ws = await resolveWorkspace(ctx, key);
|
|
114
|
+
const cloud = ctx.cloud();
|
|
115
|
+
const known = (await cloud.t3.status()).links.find(l => l.workspace_id === ws.id);
|
|
116
|
+
// A workspace connected to T3 before T3 Connect, or one whose link failed, is linked now: it is the person's choice.
|
|
117
|
+
const { link, signIn, needsLogin } = !known || known.status === 'error' || known.status === 'needs_sign_in'
|
|
118
|
+
? await t3PhoneLink(ctx, ws.id) : { link: known, signIn: undefined, needsLogin: false };
|
|
119
|
+
if (link.status === 'linked')
|
|
120
|
+
return { state: 'verified', fact: `${link.label} in the T3 app.` };
|
|
121
|
+
if (signIn) {
|
|
122
|
+
return { state: 'action', fact: `To use it from the T3 app on your phone, confirm ${signIn.user_code} once.`,
|
|
123
|
+
action: { title: 'Connect phone', url: signIn.verification_uri } };
|
|
124
|
+
}
|
|
125
|
+
if (needsLogin)
|
|
126
|
+
return { state: 'info', fact: 'Sign in to Shardflux on this Mac to use it from the T3 app.' };
|
|
127
|
+
return { state: 'pending', fact: `Adding ${link.label} to the T3 app on your phone…` };
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// No T3 Connect on this API, or nothing to ask it: the desktop route stands on its own.
|
|
131
|
+
if (!(error instanceof ShardfluxApiError) || describeError(error).code === 'not_found')
|
|
132
|
+
return undefined;
|
|
133
|
+
// Another member's T3 Connect sign-in links this project: it is in their T3 app, not something to fix here.
|
|
134
|
+
if (error.details?.reason === 't3_linked_by_other_member')
|
|
135
|
+
return { state: 'info', fact: 'In the T3 app of the member who linked it.' };
|
|
136
|
+
return { state: 'unknown', fact: 'The T3 app link could not be checked just now.', action: { title: 'Retry', retry: true } };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
91
139
|
const shard = (words) => ['shard', ...words];
|
|
92
140
|
/** Claude: the account login is ready and Claude itself accepts it right now. */
|
|
93
141
|
export function claudeConnection(row) {
|
|
@@ -105,7 +153,7 @@ export function codexConnection(local, synced, connector, health) {
|
|
|
105
153
|
const enable = { title: 'Connect Codex', command: shard(['agents', 'codex', 'enable']) };
|
|
106
154
|
if (!synced) {
|
|
107
155
|
return local ? { state: 'action', fact: 'Available on web and phone once connected.', action: enable }
|
|
108
|
-
: { state: 'action', fact: 'Sign in to Codex on this
|
|
156
|
+
: { state: 'action', fact: 'Sign in to Codex on this computer first.', action: { title: 'Sign in to Codex', command: ['codex', 'login'] } };
|
|
109
157
|
}
|
|
110
158
|
const retry = { title: 'Retry', retry: true, command: shard(['setup', '--inspect', '--apps', 'codex', '--connect', '--retry-codex']) };
|
|
111
159
|
switch (health) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI side of T3 Code mode (0.17.0+): `shard agents new --in t3 [--prompt…] [--on <ref>]`, `shard ./` and
|
|
3
|
+
* `--in auto` when T3 is where new conversations open, and `shard agents open t3 <conversation|workspace>`.
|
|
4
|
+
* --json results (contract shared with the menu bar):
|
|
5
|
+
* new: { target: "t3", mode, project, t3_thread_id, conversation_id, workspace: { workspace_key, id } | null,
|
|
6
|
+
* opened: true, focused: false, model, effort, ... }
|
|
7
|
+
* open: { opened: true, t3_thread_id, focused: false, ... }
|
|
8
|
+
* `focused` is false: T3 has no way to show a given thread from outside, so T3 is brought to the front instead.
|
|
9
|
+
*/
|
|
10
|
+
import type { Workspace } from '@shardflux/sdk';
|
|
11
|
+
import type { Ctx } from './commands.js';
|
|
12
|
+
import type { Values } from './args.js';
|
|
13
|
+
import type { AgentApp, T3Install, T3ThreadRequest, T3ThreadResult } from './t3-mode.js';
|
|
14
|
+
/**
|
|
15
|
+
* A `t3_*` refusal raised once the workspace, conversation or thread exists names them (details.workspace_key,
|
|
16
|
+
* conversation_id, t3_thread_id), so a client opens what exists instead of starting it again.
|
|
17
|
+
*/
|
|
18
|
+
export declare function withT3Context(error: unknown, ref: {
|
|
19
|
+
workspace_key?: string | null;
|
|
20
|
+
conversation_id?: string | null;
|
|
21
|
+
t3_thread_id?: string | null;
|
|
22
|
+
}): unknown;
|
|
23
|
+
/** The thread to start, when the command carries a first message (`--prompt`, `--prompt-file`, `--image`, `--model`, `--effort`). */
|
|
24
|
+
export declare function t3ThreadRequest(ctx: Ctx, values: Values, app: AgentApp): Promise<T3ThreadRequest | undefined>;
|
|
25
|
+
/**
|
|
26
|
+
* `agents new --in t3`: with a first message in ACP mode, the thread starts on T3's Shardflux provider (its own VM);
|
|
27
|
+
* otherwise the project VM is opened as T3's SSH environment (with its phone link), and a first message becomes a
|
|
28
|
+
* thread on the T3 server in that VM. `--on <ref>` uses that existing workspace instead of the project VM.
|
|
29
|
+
*/
|
|
30
|
+
export declare function launchT3(ctx: Ctx, values: Values, path: string, explicitApp: string | undefined): Promise<number>;
|
|
31
|
+
/** The T3 fields of an SSH-mode result (the thread, when one was started in the VM). */
|
|
32
|
+
export declare function t3Fields(ws: Workspace, project: string, install: T3Install | undefined, started: T3ThreadResult | undefined, conversation?: string | null): Record<string, unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* `agents open t3 <conversation>` for a conversation that runs on T3's Shardflux provider (ACP mode): T3 is brought to
|
|
35
|
+
* the front with that thread's id. A conversation of another route opens its VM as T3's SSH environment.
|
|
36
|
+
*/
|
|
37
|
+
export declare function openT3Conversation(ctx: Ctx, conversationId: string, values?: Values): Promise<number>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { out, resolveWorkspace, str } from "./commands.js";
|
|
2
|
+
import { CodedError, NotFoundError, UsageError } from "./errors.js";
|
|
3
|
+
import { projectIdentity } from "./project-identity.js";
|
|
4
|
+
import { AgentStateStore } from "./agent-state.js";
|
|
5
|
+
import { readInitialImages, readInitialPrompt } from "./initial-prompt.js";
|
|
6
|
+
import { findAcpThread, openT3, startAcpThread, t3Install, t3NotInstalled, t3OpenFailed } from "./t3-mode.js";
|
|
7
|
+
import { agentReachability } from "./agent-reachability.js";
|
|
8
|
+
import { conversationWorkspace, getConversation } from "./conversations.js";
|
|
9
|
+
import { resolveOn } from "./thread-on-computer.js";
|
|
10
|
+
import { ensureCloudClaude } from "./claude-cloud-login.js";
|
|
11
|
+
import { modelChoice } from "./agent-models.js";
|
|
12
|
+
/**
|
|
13
|
+
* A `t3_*` refusal raised once the workspace, conversation or thread exists names them (details.workspace_key,
|
|
14
|
+
* conversation_id, t3_thread_id), so a client opens what exists instead of starting it again.
|
|
15
|
+
*/
|
|
16
|
+
export function withT3Context(error, ref) {
|
|
17
|
+
if (!(error instanceof CodedError) || !error.code.startsWith('t3_'))
|
|
18
|
+
return error;
|
|
19
|
+
const details = { ...error.details, conversation_id: ref.conversation_id ?? error.details?.conversation_id ?? null, workspace_key: ref.workspace_key ?? error.details?.workspace_key ?? null,
|
|
20
|
+
t3_thread_id: ref.t3_thread_id ?? error.details?.t3_thread_id ?? null };
|
|
21
|
+
const next = new CodedError(error.code, error.message, error.hint, details);
|
|
22
|
+
next.stack = error.stack;
|
|
23
|
+
return next;
|
|
24
|
+
}
|
|
25
|
+
/** The thread to start, when the command carries a first message (`--prompt`, `--prompt-file`, `--image`, `--model`, `--effort`). */
|
|
26
|
+
export async function t3ThreadRequest(ctx, values, app) {
|
|
27
|
+
const prompt = await readInitialPrompt(ctx, values);
|
|
28
|
+
const images = await readInitialImages(ctx, values, prompt);
|
|
29
|
+
if (prompt === undefined) {
|
|
30
|
+
if (values.model !== undefined || values.effort !== undefined)
|
|
31
|
+
throw new UsageError('--model and --effort choose how the first message runs: add --prompt or --prompt-file.');
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
// The same choices as every other new thread (agent-models.ts); "default" leaves the agent's own.
|
|
35
|
+
const choice = modelChoice(values, app);
|
|
36
|
+
return { app, prompt, ...(choice.model ? { model: choice.model } : {}), ...(choice.effort ? { effort: choice.effort } : {}), ...(images.length ? { images } : {}) };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `agents new --in t3`: with a first message in ACP mode, the thread starts on T3's Shardflux provider (its own VM);
|
|
40
|
+
* otherwise the project VM is opened as T3's SSH environment (with its phone link), and a first message becomes a
|
|
41
|
+
* thread on the T3 server in that VM. `--on <ref>` uses that existing workspace instead of the project VM.
|
|
42
|
+
*/
|
|
43
|
+
export async function launchT3(ctx, values, path, explicitApp) {
|
|
44
|
+
const install = t3Install(ctx.io.env);
|
|
45
|
+
if (!install)
|
|
46
|
+
throw t3NotInstalled();
|
|
47
|
+
if (explicitApp !== undefined && explicitApp !== 'claude' && explicitApp !== 'codex')
|
|
48
|
+
throw new UsageError('agent app must be claude or codex');
|
|
49
|
+
const project = projectIdentity(path);
|
|
50
|
+
const app = explicitApp ?? new AgentStateStore(ctx.store.dir).app(project.remote ?? project.folder);
|
|
51
|
+
const request = await t3ThreadRequest(ctx, values, app);
|
|
52
|
+
const on = str(values.on);
|
|
53
|
+
if (values.review === true)
|
|
54
|
+
throw new UsageError('--review starts a Claude reviewer in its terminal; leave out --in t3.');
|
|
55
|
+
if (on !== undefined) {
|
|
56
|
+
if (request && install.mode === 'acp') {
|
|
57
|
+
throw new CodedError('t3_on_not_supported', 'With the Shardflux provider in T3 Code, every thread runs in its own new VM, so it cannot start on an existing workspace.', 'Leave out --on to start the thread in its own VM, or open that workspace in T3 Code with shard agents open t3 <workspace>.', { workspace_key: on, conversation_id: null });
|
|
58
|
+
}
|
|
59
|
+
const { ws, conversation } = await onComputer(ctx, on, project.folder);
|
|
60
|
+
// The thread's agent runs in that VM with the account's logins, as `agents open t3` prepares them.
|
|
61
|
+
await ensureCloudClaude(ctx, ws);
|
|
62
|
+
const { openDesktopWorkspace } = await import("./agent-target-commands.js");
|
|
63
|
+
return openDesktopWorkspace(ctx, ws, 't3', install.version ?? undefined, false, request, install, conversation);
|
|
64
|
+
}
|
|
65
|
+
if (request && install.mode === 'acp')
|
|
66
|
+
return acpThreadOutput(ctx, install, project, request);
|
|
67
|
+
const { launchProjectWith } = await import("./agent-commands.js");
|
|
68
|
+
return launchProjectWith(ctx, { ...values, in: 't3' }, [path, app], request, install);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The computer `--on` names, as for any new thread (thread-on-computer.ts): a workspace key or id, else a conversation
|
|
72
|
+
* id, key or name (its VM). The T3 server in that VM runs the thread.
|
|
73
|
+
*/
|
|
74
|
+
async function onComputer(ctx, ref, folder) {
|
|
75
|
+
if (!/^conv_[a-z0-9]{26}$/.test(ref)) {
|
|
76
|
+
try {
|
|
77
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
78
|
+
return { ws, conversation: ws.labels.shard_conversation || null };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!(error instanceof NotFoundError))
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const target = await resolveOn(ctx, ref, folder);
|
|
86
|
+
return { ws: await conversationWorkspace(ctx, target), conversation: target.id };
|
|
87
|
+
}
|
|
88
|
+
async function acpThreadOutput(ctx, install, project, request) {
|
|
89
|
+
const started = await startAcpThread(ctx, install, project.folder, request);
|
|
90
|
+
new AgentStateStore(ctx.store.dir).saveTarget(project.remote ?? project.folder, 't3');
|
|
91
|
+
out(ctx, `Started in T3 Code: ${request.prompt.split('\n')[0].slice(0, 80)}${started.conversation_id ? ` (${started.conversation_id})` : ''}\n`, { target: 't3', mode: started.mode, project: project.slug, t3_thread_id: started.t3_thread_id, conversation_id: started.conversation_id, workspace: started.workspace,
|
|
92
|
+
opened: true, focused: false, model: started.model, effort: started.effort, ...(started.effort_reason ? { effort_reason: started.effort_reason } : {}), app_bundle_id: 'com.t3tools.t3code' });
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
/** The T3 fields of an SSH-mode result (the thread, when one was started in the VM). */
|
|
96
|
+
export function t3Fields(ws, project, install, started, conversation) {
|
|
97
|
+
return { mode: install?.mode ?? 'ssh', project, t3_thread_id: started?.t3_thread_id ?? null, conversation_id: started?.conversation_id ?? conversation ?? (ws.labels.shard_conversation || null),
|
|
98
|
+
opened: true, focused: false, model: started?.model ?? null, effort: started?.effort ?? null, ...(started?.effort_reason ? { effort_reason: started.effort_reason } : {}) };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* `agents open t3 <conversation>` for a conversation that runs on T3's Shardflux provider (ACP mode): T3 is brought to
|
|
102
|
+
* the front with that thread's id. A conversation of another route opens its VM as T3's SSH environment.
|
|
103
|
+
*/
|
|
104
|
+
export async function openT3Conversation(ctx, conversationId, values = {}) {
|
|
105
|
+
const install = t3Install(ctx.io.env);
|
|
106
|
+
if (!install)
|
|
107
|
+
throw t3NotInstalled();
|
|
108
|
+
const conversation = await getConversation(ctx, conversationId);
|
|
109
|
+
if (install.mode === 'acp' && conversation.route === 'acp') {
|
|
110
|
+
let threadId;
|
|
111
|
+
try {
|
|
112
|
+
threadId = await findAcpThread(ctx, install, conversationId);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw withT3Context(error, { conversation_id: conversationId });
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
await openT3(ctx.io.env, install.path);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw threadId ? t3OpenFailed(error, { t3_thread_id: threadId, conversation_id: conversationId }) : error;
|
|
122
|
+
}
|
|
123
|
+
out(ctx, threadId ? `Opened T3 Code (thread ${threadId}).\n` : 'Opened T3 Code.\n', { target: 't3', mode: 'acp', conversation_id: conversationId, t3_thread_id: threadId, opened: true, focused: false, app_bundle_id: 'com.t3tools.t3code' });
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
// Its VM as T3's SSH environment, with the checks every `agents open t3` makes: --reachable and the cloud Claude login.
|
|
127
|
+
let ws = await conversationWorkspace(ctx, conversation);
|
|
128
|
+
if (values.reachable !== undefined)
|
|
129
|
+
ws = await ctx.cloud().workspaces.open({ key: ws.key, ...agentReachability(values), wait: false });
|
|
130
|
+
await ensureCloudClaude(ctx, ws);
|
|
131
|
+
const { openDesktopWorkspace } = await import("./agent-target-commands.js");
|
|
132
|
+
return openDesktopWorkspace(ctx, ws, 't3', install.version ?? undefined, false, undefined, install, conversationId);
|
|
133
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Workspace } from '@shardflux/sdk';
|
|
2
|
+
import type { Ctx } from './commands.js';
|
|
3
|
+
import { CodedError } from './errors.js';
|
|
4
|
+
import type { InitialImage } from './initial-prompt.js';
|
|
5
|
+
export declare const T3_BUNDLE_ID = "com.t3tools.t3code";
|
|
6
|
+
export type T3Mode = 'acp' | 'ssh';
|
|
7
|
+
export type AgentApp = 'claude' | 'codex';
|
|
8
|
+
export type OpenIn = 'terminal' | 'claude-desktop' | 'web' | 'codex-app' | 't3';
|
|
9
|
+
/** Where each app's new conversations can open: Claude in a terminal, Claude Desktop, claude.ai or T3; Codex in the Codex app or T3. */
|
|
10
|
+
export declare const OPEN_IN_TARGETS: Record<AgentApp, readonly OpenIn[]>;
|
|
11
|
+
export interface T3Install {
|
|
12
|
+
path: string;
|
|
13
|
+
version: string | null;
|
|
14
|
+
mode: T3Mode;
|
|
15
|
+
}
|
|
16
|
+
export interface T3Status {
|
|
17
|
+
installed: boolean;
|
|
18
|
+
mode: T3Mode | null;
|
|
19
|
+
connected: boolean;
|
|
20
|
+
version: string | null;
|
|
21
|
+
last_used_at: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface T3ThreadRequest {
|
|
24
|
+
app: AgentApp;
|
|
25
|
+
prompt: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
effort?: string;
|
|
28
|
+
images?: readonly InitialImage[];
|
|
29
|
+
}
|
|
30
|
+
/** Why a requested `--effort` reached T3 as none: the model offers no such level, or its levels are not known yet. */
|
|
31
|
+
export type EffortReason = 'not_offered' | 'models_not_known';
|
|
32
|
+
export interface T3ThreadResult {
|
|
33
|
+
mode: T3Mode;
|
|
34
|
+
t3_thread_id: string;
|
|
35
|
+
conversation_id: string | null;
|
|
36
|
+
workspace: {
|
|
37
|
+
workspace_key: string;
|
|
38
|
+
id: string;
|
|
39
|
+
} | null;
|
|
40
|
+
model: string | null;
|
|
41
|
+
effort: string | null;
|
|
42
|
+
effort_reason?: EffortReason;
|
|
43
|
+
}
|
|
44
|
+
/** Native lookups that tests replace: Spotlight's last-used dates of app bundles, one `mdls` for all of them. */
|
|
45
|
+
export declare const t3Native: {
|
|
46
|
+
lastUsedMany(paths: readonly string[], env: Record<string, string | undefined>): Record<string, string | null>;
|
|
47
|
+
};
|
|
48
|
+
/** Spotlight takes about 40 ms per call, so the dates are kept for a few minutes (app-usage.json in the config dir). */
|
|
49
|
+
export declare const LAST_USED_TTL_MS: number;
|
|
50
|
+
export declare function lastUsedDates(ctx: Ctx, paths: readonly string[], now?: number): Record<string, string | null>;
|
|
51
|
+
/** The installed T3 Code, preferring a build with ACP providers when several channels are installed. */
|
|
52
|
+
export declare function t3Install(env: Record<string, string | undefined>, platform?: NodeJS.Platform): T3Install | undefined;
|
|
53
|
+
export declare function t3HomeDir(env: Record<string, string | undefined>): string;
|
|
54
|
+
/** The Shardflux provider instance in T3's server settings (read only; every `shard` run writes it, see t3-provider.ts). */
|
|
55
|
+
export declare function t3ProviderRegistered(env: Record<string, string | undefined>): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* T3 is connected when a new thread can start in it now: in ACP mode the Shardflux provider is in T3's settings and
|
|
58
|
+
* this computer is signed in; in SSH mode this project has a T3 SSH environment set up from this computer (signed in).
|
|
59
|
+
* `last_used_at` is looked up only when asked for (`agents targets`).
|
|
60
|
+
*/
|
|
61
|
+
export declare function t3Status(ctx: Ctx, opts?: {
|
|
62
|
+
project?: string | null;
|
|
63
|
+
install?: T3Install | null;
|
|
64
|
+
lastUsed?: boolean;
|
|
65
|
+
}): T3Status;
|
|
66
|
+
export interface OpenInChoice {
|
|
67
|
+
target: OpenIn;
|
|
68
|
+
saved: boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Where each app's new conversations open for `project`: the choice saved with `agents targets --open-in` while it is
|
|
72
|
+
* available, else T3 when it is connected and the most recently used of the installed desktop apps, else a terminal
|
|
73
|
+
* (Claude) or the Codex app. Lookups run only when an answer needs them: none for a saved terminal, web or Codex app
|
|
74
|
+
* choice, and Spotlight only when T3 is installed and connected (cached, see lastUsedDates).
|
|
75
|
+
*/
|
|
76
|
+
export declare function openInFor(ctx: Ctx, project: string | null, known?: {
|
|
77
|
+
clients?: readonly string[];
|
|
78
|
+
}): Record<AgentApp, OpenInChoice>;
|
|
79
|
+
export declare function openInChoices(ctx: Ctx, project: string | null, known?: {
|
|
80
|
+
clients?: readonly string[];
|
|
81
|
+
}): Record<AgentApp, OpenIn>;
|
|
82
|
+
/** `--open-in claude=t3,codex=t3`: validated against what is installed, then saved in agents.json. */
|
|
83
|
+
export declare function saveOpenIn(ctx: Ctx, spec: string, status: T3Status, clients: readonly string[]): void;
|
|
84
|
+
/** A T3 thread was started, then a later step failed: the error names the thread so a client opens it, never starts another. */
|
|
85
|
+
export declare function t3OpenFailed(error: unknown, ref: {
|
|
86
|
+
t3_thread_id: string;
|
|
87
|
+
conversation_id?: string | null;
|
|
88
|
+
workspace_key?: string | null;
|
|
89
|
+
}): CodedError;
|
|
90
|
+
export declare const t3NotInstalled: () => CodedError;
|
|
91
|
+
/** The first line of the prompt, as T3 titles a thread before it names it. */
|
|
92
|
+
export declare function t3ThreadTitle(prompt: string): string;
|
|
93
|
+
/**
|
|
94
|
+
* Opens T3 Code (or brings it to the front): the install `shard` chose (`open -a <path>`). Every T3 channel shares one
|
|
95
|
+
* bundle id, so `open -b` could start another installed channel.
|
|
96
|
+
*/
|
|
97
|
+
export declare function openT3(env: Record<string, string | undefined>, appPath: string): Promise<void>;
|
|
98
|
+
/** Starts the thread in the VM's T3 server (T3_THREAD_SCRIPT); the prompt travels on stdin, never in argv. */
|
|
99
|
+
export declare function startSshThread(ctx: Ctx, ws: Workspace, version: string | undefined, projectPath: string, request: T3ThreadRequest): Promise<T3ThreadResult>;
|
|
100
|
+
interface Descriptor {
|
|
101
|
+
serverVersion?: string;
|
|
102
|
+
orchestrationProtocolVersion?: number;
|
|
103
|
+
}
|
|
104
|
+
export interface LocalT3Server {
|
|
105
|
+
origin: string;
|
|
106
|
+
descriptor: Descriptor;
|
|
107
|
+
base: string;
|
|
108
|
+
}
|
|
109
|
+
interface T3Session {
|
|
110
|
+
token: string;
|
|
111
|
+
sessionId: string;
|
|
112
|
+
}
|
|
113
|
+
/** The running desktop server; T3 is opened (which starts it) when none answers. */
|
|
114
|
+
export declare function localT3Server(ctx: Ctx, install: T3Install, waitMs?: number): Promise<LocalT3Server>;
|
|
115
|
+
/** One Effect RPC request over T3's WebSocket (JSON frames: Request → Exit), authorized by a ticket from the session. */
|
|
116
|
+
export declare function t3Rpc(server: LocalT3Server, session: T3Session, tag: string, payload: unknown, signal: AbortSignal, timeoutMs?: number): Promise<unknown>;
|
|
117
|
+
/** Starts a T3 thread on the Shardflux provider (ACP mode) and brings T3 to the front. */
|
|
118
|
+
export declare function startAcpThread(ctx: Ctx, install: T3Install, folder: string, request: T3ThreadRequest, waitMs?: number): Promise<T3ThreadResult>;
|
|
119
|
+
/** The T3 thread of a Shardflux conversation started in T3 (ACP mode): the one remembered, else a search of recent Shardflux threads. */
|
|
120
|
+
export declare function findAcpThread(ctx: Ctx, install: T3Install, conversationId: string): Promise<string | null>;
|
|
121
|
+
export {};
|