@shardflux/cli 0.20.0 → 0.23.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 +312 -1
- package/README.md +242 -75
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +19 -15
- package/dist/account-pool.js +47 -96
- package/dist/account-scope.d.ts +65 -0
- package/dist/account-scope.js +246 -0
- package/dist/acp-journal.js +58 -6
- package/dist/acp-login.d.ts +13 -5
- package/dist/acp-login.js +37 -8
- package/dist/acp-models.d.ts +6 -1
- package/dist/acp-models.js +29 -19
- package/dist/acp.js +120 -14
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +110 -42
- package/dist/agent-config-return.d.ts +46 -2
- package/dist/agent-config-return.js +110 -4
- package/dist/agent-credentials.js +7 -3
- package/dist/agent-repository.d.ts +16 -3
- package/dist/agent-repository.js +149 -74
- package/dist/agent-setup.d.ts +14 -3
- package/dist/agent-setup.js +78 -36
- package/dist/agent-target-commands.d.ts +20 -1
- package/dist/agent-target-commands.js +73 -31
- package/dist/agent-targets.js +6 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +95 -54
- package/dist/bin.js +5 -1
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -14
- package/dist/claude-cloud-login.d.ts +24 -2
- package/dist/claude-cloud-login.js +151 -11
- package/dist/cli-proxy-hub.d.ts +61 -25
- package/dist/cli-proxy-hub.js +202 -54
- package/dist/cli-proxy.d.ts +44 -1
- package/dist/cli-proxy.js +177 -28
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +14 -4
- package/dist/codex-cloud.js +135 -31
- package/dist/codex-linux.d.ts +14 -0
- package/dist/codex-linux.js +69 -0
- package/dist/command-center-actions.js +18 -5
- package/dist/command-center-model.d.ts +2 -0
- package/dist/command-center-model.js +6 -1
- package/dist/command-center.js +3 -3
- package/dist/commands.js +173 -24
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +8 -5
- package/dist/conversation-terminal.d.ts +10 -2
- package/dist/conversation-terminal.js +78 -13
- package/dist/conversations.d.ts +86 -1
- package/dist/conversations.js +306 -23
- package/dist/desktop-linux.js +16 -3
- package/dist/device-bridge.d.ts +1 -1
- package/dist/device-bridge.js +6 -4
- package/dist/device-helper.js +43 -22
- package/dist/errors.d.ts +29 -2
- package/dist/errors.js +50 -7
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +133 -57
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +3 -1
- package/dist/initial-prompt.d.ts +8 -0
- package/dist/initial-prompt.js +43 -1
- package/dist/large-media.d.ts +14 -0
- package/dist/large-media.js +100 -0
- package/dist/linux-terminal.d.ts +5 -3
- package/dist/linux-terminal.js +6 -20
- package/dist/local-sessions.d.ts +26 -2
- package/dist/local-sessions.js +113 -24
- package/dist/machine-setup.d.ts +11 -1
- package/dist/machine-setup.js +69 -22
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +11 -13
- package/dist/menubar.d.ts +6 -5
- package/dist/menubar.js +38 -28
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +69 -0
- package/dist/move-worktrees.js +252 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +111 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +63 -29
- package/dist/prepared-repository.d.ts +204 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1307 -0
- package/dist/project-preparation.d.ts +62 -0
- package/dist/project-preparation.js +316 -0
- package/dist/project-sync-manifest.d.ts +4 -0
- package/dist/project-sync-manifest.js +5 -1
- package/dist/project-sync-transfer.js +1 -1
- package/dist/project-sync.d.ts +22 -4
- package/dist/project-sync.js +34 -8
- package/dist/project-template-setup.d.ts +33 -1
- package/dist/project-template-setup.js +59 -2
- package/dist/secret-ask.d.ts +72 -0
- package/dist/secret-ask.js +351 -0
- package/dist/seed-phases.d.ts +8 -1
- package/dist/seed-phases.js +17 -2
- package/dist/setup-cache.d.ts +7 -0
- package/dist/setup-cache.js +32 -0
- package/dist/t3-launch.d.ts +2 -1
- package/dist/t3-launch.js +12 -6
- package/dist/t3-mode.d.ts +8 -2
- package/dist/t3-mode.js +19 -4
- package/dist/t3-phone.d.ts +4 -2
- package/dist/t3-phone.js +12 -4
- package/dist/t3-proxy.d.ts +81 -0
- package/dist/t3-proxy.js +325 -0
- package/dist/terminal-apps.d.ts +168 -0
- package/dist/terminal-apps.js +574 -0
- package/dist/terminal-onboarding.js +6 -1
- package/dist/this-computer.d.ts +11 -0
- package/dist/this-computer.js +36 -0
- package/dist/thread-on-computer.js +4 -1
- package/dist/update.d.ts +2 -0
- package/dist/update.js +43 -9
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
package/dist/cli-proxy.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `shard cli-proxy` (status), `shard cli-proxy import` and the hidden `shard cli-proxy forward`: one hosted
|
|
2
|
+
* `shard cli-proxy` (status), `shard cli-proxy import`, `shard cli-proxy use` and the hidden `shard cli-proxy forward`: one hosted
|
|
3
3
|
* CLIProxyAPI per account (cli-proxy-hub.ts), reached at http://127.0.0.1:8317 on this computer (a launchd agent runs
|
|
4
4
|
* the forwarder, cli-proxy-forward.ts) and in every VM (the hub info in the machine template; sf-agent forwards there).
|
|
5
5
|
*
|
|
6
6
|
* Import moves the user's CLIProxyAPI from this computer: the hub is made from their config, the local copy stops
|
|
7
7
|
* (only one copy may refresh a login), their auth files go to the hub (never printed), the forwarder takes 127.0.0.1:8317
|
|
8
8
|
* and the machine template carries the hub info. Onboarding calls `detectLocalCliProxy()` and `importCliProxy(ctx)`.
|
|
9
|
+
*
|
|
10
|
+
* The hub is the user's own CLIProxyAPI: it routes requests across their logins as their config says, and each login
|
|
11
|
+
* stays enabled or disabled as they left it. `shard cli-proxy use` is their switch: one login for a provider, or all.
|
|
9
12
|
*/
|
|
10
13
|
import { execFile } from 'node:child_process';
|
|
11
14
|
import { createHash } from 'node:crypto';
|
|
@@ -18,7 +21,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
18
21
|
import { AgentStateStore } from "./agent-state.js";
|
|
19
22
|
import { syncAgentConfig } from "./agent-config-sync.js";
|
|
20
23
|
import { CliProxyForwarder, IDENTITY_PATH, hubInfoSource } from "./cli-proxy-forward.js";
|
|
21
|
-
import { ADD_AUTH_SCRIPT, CLI_PROXY_PORT, CLI_PROXY_VM_PORT, HUB_AUTH_DIR, HUB_KEY, HubError, SET_ASIDE_SCRIPT, STOP_SCRIPT, ensureCliProxyHub, hubCallback, readHubInfo, sdkHubCloud, vmHubInfoText, writeHubInfo } from "./cli-proxy-hub.js";
|
|
24
|
+
import { ADD_AUTH_SCRIPT, CLI_PROXY_PORT, CLI_PROXY_VM_PORT, HUB_AUTH_DIR, HUB_KEY, HubError, SET_ASIDE_SCRIPT, STOP_SCRIPT, ensureCliProxyHub, hubCallback, listHubLogins, readHubInfo, sdkHubCloud, useHubLogins, vmHubInfoText, writeHubInfo } from "./cli-proxy-hub.js";
|
|
22
25
|
import { CLI_VERSION } from "./args.js";
|
|
23
26
|
import { UsageError } from "./errors.js";
|
|
24
27
|
import { json } from "./format.js";
|
|
@@ -625,6 +628,12 @@ export function sdkMachine(ctx) {
|
|
|
625
628
|
},
|
|
626
629
|
};
|
|
627
630
|
}
|
|
631
|
+
/** The auth file a management API entry names, relative to the hub's auth directory. */
|
|
632
|
+
function entryFile(f) {
|
|
633
|
+
if (typeof f.path === 'string' && f.path.startsWith(`${HUB_AUTH_DIR}/`))
|
|
634
|
+
return f.path.slice(HUB_AUTH_DIR.length + 1);
|
|
635
|
+
return [f.id, f.name].find((v) => typeof v === 'string' && v !== '') ?? null;
|
|
636
|
+
}
|
|
628
637
|
/** The hub's logins from its management API (wakes the hub). */
|
|
629
638
|
export async function hubAccounts(info, timeoutMs = 45_000) {
|
|
630
639
|
const res = await fetch(new URL('v0/management/auth-files', info.callback_url), {
|
|
@@ -640,6 +649,8 @@ export async function hubAccounts(info, timeoutMs = 45_000) {
|
|
|
640
649
|
email: typeof f.email === 'string' && f.email ? f.email : null,
|
|
641
650
|
status: typeof f.status === 'string' && f.status ? f.status : null,
|
|
642
651
|
disabled: f.disabled === true,
|
|
652
|
+
file: entryFile(f),
|
|
653
|
+
active: f.disabled !== true,
|
|
643
654
|
}));
|
|
644
655
|
}
|
|
645
656
|
/** Whether the hub answers /healthz through its callback URL, and the edge's reason when it does not. */
|
|
@@ -654,11 +665,61 @@ export async function hubHealth(info, timeoutMs = 45_000) {
|
|
|
654
665
|
}
|
|
655
666
|
}
|
|
656
667
|
const accountLine = (a) => ` ${a.provider.padEnd(8)} ${a.email ?? '(no email)'}${a.disabled ? ' disabled' : a.status && a.status !== 'active' ? ` ${a.status}` : ''}`;
|
|
668
|
+
/** The logins a `--use` or `shard cli-proxy use` value names: by email (any case), path in the auth folder, or file name. */
|
|
669
|
+
export function matchLogins(spec, logins) {
|
|
670
|
+
const want = spec.trim().toLowerCase();
|
|
671
|
+
return logins.filter((l) => l.provider !== null && want !== '' && (l.email?.toLowerCase() === want || l.file.toLowerCase() === want || basename(l.file).toLowerCase() === want));
|
|
672
|
+
}
|
|
673
|
+
const loginName = (l) => l.email ?? l.file;
|
|
674
|
+
const refs = (logins, active) => logins.filter((l) => l.provider !== null && l.active === active).map((l) => ({ provider: l.provider, email: l.email, file: l.file }));
|
|
675
|
+
/**
|
|
676
|
+
* Resolves `--use` (repeatable): each value names the one login that serves its provider, on this computer or already
|
|
677
|
+
* in the hub.
|
|
678
|
+
* Checked before anything changes, so a typo stops the import with the logins to pick from.
|
|
679
|
+
*/
|
|
680
|
+
function resolveUse(specs, local, hub) {
|
|
681
|
+
const out = new Map();
|
|
682
|
+
const localLogins = (local?.logins ?? []).map((l) => {
|
|
683
|
+
let text = '';
|
|
684
|
+
try {
|
|
685
|
+
text = readFileSync(join(local.authDir, l.file), 'utf8');
|
|
686
|
+
}
|
|
687
|
+
catch { /* unreadable: matched by name only */ }
|
|
688
|
+
return { ...l, identity: loginIdentity(text, l.file) };
|
|
689
|
+
});
|
|
690
|
+
const by = new Map();
|
|
691
|
+
for (const spec of specs) {
|
|
692
|
+
const mineHere = matchLogins(spec, localLogins), inHub = matchLogins(spec, hub);
|
|
693
|
+
if (!mineHere.length && !inHub.length) {
|
|
694
|
+
const all = [...localLogins.filter((l) => l.provider), ...hub.filter((l) => l.provider)].map((l) => `${l.provider} ${loginName(l)}`);
|
|
695
|
+
throw new UsageError(`No login matches --use ${spec}.`, all.length ? `pick one of: ${[...new Set(all)].join(', ')}` : 'name a login by its email');
|
|
696
|
+
}
|
|
697
|
+
for (const provider of new Set([...mineHere, ...inHub].map((l) => l.provider))) {
|
|
698
|
+
const here = mineHere.filter((l) => l.provider === provider), there = inHub.filter((l) => l.provider === provider);
|
|
699
|
+
const ids = new Set(here.map((l) => l.identity));
|
|
700
|
+
if (ids.size > 1 || (!here.length && there.length > 1)) {
|
|
701
|
+
throw new UsageError(`--use ${spec} matches more than one ${provider} login (${[...here, ...there].map((l) => l.file).join(', ')}).`, 'name one by its file name: --use <file>');
|
|
702
|
+
}
|
|
703
|
+
const choice = here[0] ? { local: here[0].file, identity: here[0].identity } : { hub: there[0].file };
|
|
704
|
+
const earlier = by.get(provider);
|
|
705
|
+
if (earlier !== undefined && earlier !== spec)
|
|
706
|
+
throw new UsageError(`--use ${earlier} and --use ${spec} both name a ${provider} login; --use names one login per provider.`, 'keep one --use per provider');
|
|
707
|
+
by.set(provider, spec);
|
|
708
|
+
out.set(provider, choice);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return out;
|
|
712
|
+
}
|
|
713
|
+
const useSpecs = (v) => (Array.isArray(v) ? v : typeof v === 'string' ? [v] : []).filter((x) => x.trim() !== '');
|
|
657
714
|
/**
|
|
658
715
|
* Moves CLIProxyAPI from this computer into the account (or connects this computer to the account's hub): ensure the
|
|
659
716
|
* hub from the user's config, stop the local copy, upload the auth files the hub does not have yet (never overwriting
|
|
660
717
|
* one: the hub's copy may hold a newer refresh token), start the forwarder on 127.0.0.1:8317 and put the hub info into
|
|
661
718
|
* the machine template.
|
|
719
|
+
*
|
|
720
|
+
* Each login arrives as it is on this computer (enabled or disabled), and the hub routes across the enabled ones as
|
|
721
|
+
* the user's config says. `--use <email|file>` (repeatable, one per provider) makes that login the only one serving
|
|
722
|
+
* its provider (the provider's others are kept in the hub, disabled), as `shard cli-proxy use` does.
|
|
662
723
|
*/
|
|
663
724
|
export async function importCliProxy(ctx, opts = {}) {
|
|
664
725
|
if (cliProxyTools.inVm()) {
|
|
@@ -676,6 +737,8 @@ export async function importCliProxy(ctx, opts = {}) {
|
|
|
676
737
|
const ensured = await ensureCliProxyHub(hub, { dir: ctx.store.dir, apiUrl: ctx.apiUrl, ...(userConfig !== undefined ? { userConfig } : {}), progress: (m) => progress(ctx, m) });
|
|
677
738
|
for (const value of [ensured.info.callback_url, ensured.info.management_key])
|
|
678
739
|
ctx.addSecret(value);
|
|
740
|
+
// Which login of each provider is active: checked against this computer's and the hub's before anything is stopped.
|
|
741
|
+
const use = resolveUse(useSpecs(values.use), local, ensured.logins);
|
|
679
742
|
// The forwarder takes 127.0.0.1:8317: refuse before stopping anything when another program holds it.
|
|
680
743
|
if (local && local.port !== CLI_PROXY_PORT) {
|
|
681
744
|
const answers = opts.stop?.answers ?? cliProxyTools.answers;
|
|
@@ -689,7 +752,8 @@ export async function importCliProxy(ctx, opts = {}) {
|
|
|
689
752
|
const uploaded = [], kept = [];
|
|
690
753
|
const added = [], already = [];
|
|
691
754
|
let movedAside = [];
|
|
692
|
-
|
|
755
|
+
let hubLogins = ensured.logins;
|
|
756
|
+
if (local?.logins.length || use.size) {
|
|
693
757
|
const opened = await hub.open(false);
|
|
694
758
|
if (!opened)
|
|
695
759
|
throw new HubError('The hub was deleted during the import.', `run: ${AGAIN}`);
|
|
@@ -697,34 +761,13 @@ export async function importCliProxy(ctx, opts = {}) {
|
|
|
697
761
|
// auth file's JSON, the hub's and this computer's), under the hub's logins lock, so two computers importing at
|
|
698
762
|
// once never add one account twice and an account the hub holds keeps its newer tokens, whatever the file names.
|
|
699
763
|
await opened.ws.withLock('logins', async () => {
|
|
700
|
-
|
|
701
|
-
const present = await opened.ws.authFiles();
|
|
702
|
-
for (const rel of present) {
|
|
703
|
-
const text = await opened.ws.read(`${HUB_AUTH_DIR}/${rel}`);
|
|
704
|
-
inHub.add(text === null ? `file:${rel}` : loginIdentity(text, rel));
|
|
705
|
-
}
|
|
706
|
-
const names = new Set(present);
|
|
707
|
-
for (const login of local.logins) {
|
|
708
|
-
const text = readFileSync(join(local.authDir, login.file), 'utf8');
|
|
709
|
-
const account = loginIdentity(text, login.file);
|
|
710
|
-
const r = inHub.has(account) || names.has(login.file) ? null : await opened.ws.sh(ADD_AUTH_SCRIPT, [login.file], { stdin: text });
|
|
711
|
-
if (r && r.code !== 0)
|
|
712
|
-
throw new HubError(`Could not add ${login.file} to the hub: ${r.stderr.trim().split('\n').at(-1) || `exit ${r.code}`}`, `run: ${AGAIN}`);
|
|
713
|
-
if (!r || r.stdout.trim() === 'exists') {
|
|
714
|
-
kept.push(login.file);
|
|
715
|
-
already.push({ provider: login.provider, email: login.email });
|
|
716
|
-
continue;
|
|
717
|
-
}
|
|
718
|
-
uploaded.push(login.file);
|
|
719
|
-
added.push({ provider: login.provider, email: login.email });
|
|
720
|
-
inHub.add(account);
|
|
721
|
-
names.add(login.file);
|
|
722
|
-
}
|
|
764
|
+
hubLogins = await uploadLogins(opened.ws, local, use, { uploaded, kept, added, already });
|
|
723
765
|
});
|
|
724
766
|
if (uploaded.length)
|
|
725
767
|
progress(ctx, `Moved ${uploaded.length} login${uploaded.length === 1 ? '' : 's'} to the hub`);
|
|
726
768
|
// The hub holds every login now: no local copy keeps tokens it could refresh.
|
|
727
|
-
|
|
769
|
+
if (local?.logins.length)
|
|
770
|
+
movedAside = moveAuthFilesAside(local.authDirs);
|
|
728
771
|
}
|
|
729
772
|
if (local) {
|
|
730
773
|
new AgentStateStore(ctx.store.dir).update((doc) => {
|
|
@@ -760,9 +803,50 @@ export async function importCliProxy(ctx, opts = {}) {
|
|
|
760
803
|
const accounts = await hubAccounts(ensured.info).catch(() => null);
|
|
761
804
|
return {
|
|
762
805
|
hub: { workspace_id: ensured.info.workspace_id, created: ensured.created, process: ensured.process },
|
|
763
|
-
logins: { uploaded, kept, added, already, moved_aside: movedAside.map((m) => m.to)
|
|
806
|
+
logins: { uploaded, kept, added, already, moved_aside: movedAside.map((m) => m.to), active: refs(hubLogins, true), disabled: refs(hubLogins, false) },
|
|
807
|
+
config: ensured.config ? 'from_this_computer' : 'kept', local: stop.result, port: local?.port ?? CLI_PROXY_PORT, forwarder: { launchd }, machine, dropped: ensured.config?.dropped ?? [], accounts,
|
|
764
808
|
};
|
|
765
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Under the hub's logins lock: uploads this computer's logins the hub lacks, each as it is (enabled or disabled), then
|
|
812
|
+
* applies --use. Returns the hub's logins after.
|
|
813
|
+
*/
|
|
814
|
+
async function uploadLogins(ws, local, use, out) {
|
|
815
|
+
const inHub = new Map();
|
|
816
|
+
const present = await ws.authFiles();
|
|
817
|
+
for (const rel of present) {
|
|
818
|
+
const text = await ws.read(`${HUB_AUTH_DIR}/${rel}`);
|
|
819
|
+
const id = text === null ? `file:${rel}` : loginIdentity(text, rel);
|
|
820
|
+
if (!inHub.has(id))
|
|
821
|
+
inHub.set(id, rel);
|
|
822
|
+
}
|
|
823
|
+
const names = new Set(present);
|
|
824
|
+
for (const login of local?.logins ?? []) {
|
|
825
|
+
const text = readFileSync(join(local.authDir, login.file), 'utf8');
|
|
826
|
+
const identity = loginIdentity(text, login.file);
|
|
827
|
+
const r = inHub.has(identity) || names.has(login.file) ? null : await ws.sh(ADD_AUTH_SCRIPT, [login.file], { stdin: text });
|
|
828
|
+
if (r && r.code !== 0)
|
|
829
|
+
throw new HubError(`Could not add ${login.file} to the hub: ${r.stderr.trim().split('\n').at(-1) || `exit ${r.code}`}`, `run: ${AGAIN}`);
|
|
830
|
+
if (!r || r.stdout.trim() === 'exists') {
|
|
831
|
+
out.kept.push(login.file);
|
|
832
|
+
out.already.push({ provider: login.provider, email: login.email });
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
out.uploaded.push(login.file);
|
|
836
|
+
out.added.push({ provider: login.provider, email: login.email });
|
|
837
|
+
inHub.set(identity, login.file);
|
|
838
|
+
names.add(login.file);
|
|
839
|
+
}
|
|
840
|
+
if (!use.size)
|
|
841
|
+
return listHubLogins(ws);
|
|
842
|
+
const only = {};
|
|
843
|
+
for (const [provider, choice] of use) {
|
|
844
|
+
const file = 'hub' in choice ? choice.hub : inHub.get(choice.identity);
|
|
845
|
+
if (file !== undefined)
|
|
846
|
+
only[provider] = file;
|
|
847
|
+
}
|
|
848
|
+
return useHubLogins(ws, { only });
|
|
849
|
+
}
|
|
766
850
|
/**
|
|
767
851
|
* Brings the hub's logins back to this computer: the hub stops first (from then on only this computer refreshes
|
|
768
852
|
* them), its auth files as last refreshed are written to the local auth directory (0600), the hub sets its copies
|
|
@@ -840,6 +924,69 @@ async function exportHandler(ctx, values) {
|
|
|
840
924
|
].join('\n') + '\n');
|
|
841
925
|
return 0;
|
|
842
926
|
}
|
|
927
|
+
/**
|
|
928
|
+
* `shard cli-proxy use <email|file>`: that login alone serves its provider (an email with a Claude and a Codex login
|
|
929
|
+
* sets both); the provider's other logins stay in the hub, disabled. `--all`: every login serves again, and the hub
|
|
930
|
+
* routes across them as its config says. Under the hub's logins lock.
|
|
931
|
+
*/
|
|
932
|
+
export async function useCliProxyLogin(ctx, spec, opts = {}) {
|
|
933
|
+
if (cliProxyTools.inVm())
|
|
934
|
+
throw new UsageError('shard cli-proxy use runs on a computer connected to your hosted CLIProxyAPI; this is a Shardflux VM.', 'run it on that computer: shard cli-proxy use <email>');
|
|
935
|
+
if (!opts.all && !spec.trim())
|
|
936
|
+
throw new UsageError('Name the login to use, or --all for every login.', 'run: shard cli-proxy use <email>');
|
|
937
|
+
if (opts.all && spec.trim())
|
|
938
|
+
throw new UsageError('Name a login or use --all, not both.', 'run: shard cli-proxy use --all');
|
|
939
|
+
const hub = opts.hub ?? sdkHubCloud(ctx.cloud(), { signal: ctx.signal, wakeTimeoutMs: ctx.wakeTimeoutMs, agentLabel: ctx.agentLabel });
|
|
940
|
+
const opened = await hub.open(false);
|
|
941
|
+
if (!opened)
|
|
942
|
+
throw new UsageError('This account has no hosted CLIProxyAPI yet.', `move yours: ${AGAIN}`);
|
|
943
|
+
return opened.ws.withLock('logins', async () => {
|
|
944
|
+
const before = await listHubLogins(opened.ws);
|
|
945
|
+
if (opts.all) {
|
|
946
|
+
const after = await useHubLogins(opened.ws, { all: true });
|
|
947
|
+
return { switched: [], active: refs(after, true), disabled: refs(after, false) };
|
|
948
|
+
}
|
|
949
|
+
const matches = matchLogins(spec, before);
|
|
950
|
+
if (!matches.length) {
|
|
951
|
+
const all = before.filter((l) => l.provider).map((l) => `${l.provider} ${loginName(l)}`);
|
|
952
|
+
throw new UsageError(`No login in your hosted CLIProxyAPI matches ${spec}.`, all.length ? `pick one of: ${all.join(', ')}` : `add yours: ${AGAIN}`);
|
|
953
|
+
}
|
|
954
|
+
const only = {};
|
|
955
|
+
for (const provider of new Set(matches.map((l) => l.provider))) {
|
|
956
|
+
const mine = matches.filter((l) => l.provider === provider);
|
|
957
|
+
if (mine.length > 1)
|
|
958
|
+
throw new UsageError(`${spec} matches more than one ${provider} login (${mine.map((l) => l.file).join(', ')}).`, 'name one by its file name: shard cli-proxy use <file>');
|
|
959
|
+
only[provider] = mine[0].file;
|
|
960
|
+
}
|
|
961
|
+
const after = await useHubLogins(opened.ws, { only });
|
|
962
|
+
const ref = (l) => ({ provider: l.provider, email: l.email, file: l.file });
|
|
963
|
+
const switched = Object.entries(only).map(([provider, file]) => {
|
|
964
|
+
const was = before.filter((l) => l.provider === provider && l.active);
|
|
965
|
+
return { provider, from: was.length === 1 ? ref(was[0]) : null, to: ref(after.find((l) => l.file === file)) };
|
|
966
|
+
});
|
|
967
|
+
return { switched, active: refs(after, true), disabled: refs(after, false) };
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
async function useHandler(ctx, values, positionals) {
|
|
971
|
+
const spec = positionals[0] ?? (typeof values.login === 'string' ? values.login : '');
|
|
972
|
+
const r = await useCliProxyLogin(ctx, spec, { all: values.all === true });
|
|
973
|
+
if (ctx.json) {
|
|
974
|
+
ctx.io.stdout.write(json(r));
|
|
975
|
+
return 0;
|
|
976
|
+
}
|
|
977
|
+
const lines = values.all === true
|
|
978
|
+
? [`Every login serves requests, routed as your CLIProxyAPI config says: ${r.active.map((l) => `${l.provider} ${loginName(l)}`).join(', ') || 'none yet'}.`]
|
|
979
|
+
: r.switched.map((s) => (s.from?.file === s.to.file
|
|
980
|
+
? `${loginName(s.to)} already serves your ${s.provider} requests.`
|
|
981
|
+
: `${loginName(s.to)} serves your ${s.provider} requests now. Claude Code, Codex and T3 Code use it from their next request.`));
|
|
982
|
+
for (const provider of new Set(r.switched.map((s) => s.provider))) {
|
|
983
|
+
const others = r.disabled.filter((l) => l.provider === provider);
|
|
984
|
+
if (others.length)
|
|
985
|
+
lines.push(`Kept in the hub, disabled: ${others.map(loginName).join(', ')}. Use them all again: shard cli-proxy use --all`);
|
|
986
|
+
}
|
|
987
|
+
ctx.io.stdout.write(`${lines.join('\n')}\n`);
|
|
988
|
+
return 0;
|
|
989
|
+
}
|
|
843
990
|
// ---- handlers ------------------------------------------------------------------------------------------------------
|
|
844
991
|
function statusLines(state, accounts, forwarding, launchd, machineVersion) {
|
|
845
992
|
const hub = state === null ? 'missing (run shard cli-proxy import)' : state === 'running' ? 'running' : ['suspended', 'parked', 'stopped'].includes(state) ? 'sleeping (wakes on the next request)' : state;
|
|
@@ -948,6 +1095,7 @@ async function importHandler(ctx, values) {
|
|
|
948
1095
|
...(r.config === 'kept' ? ['The hub keeps its settings (client keys, routing, upstream keys).'] : []),
|
|
949
1096
|
...r.logins.moved_aside.map((dir) => `This computer’s auth files are in ${dir} now, where no CLIProxyAPI reads them.`),
|
|
950
1097
|
...(r.port !== CLI_PROXY_PORT ? [`Point tools that used http://127.0.0.1:${r.port} at http://127.0.0.1:${CLI_PROXY_PORT}.`] : []),
|
|
1098
|
+
...(r.logins.disabled.length ? [`Kept in the hub, disabled: ${r.logins.disabled.map((l) => `${l.provider} ${loginName(l)}`).join(', ')}. Use them too: shard cli-proxy use --all`] : []),
|
|
951
1099
|
...(r.accounts ? [`Accounts: ${r.accounts.length}`, ...r.accounts.map(accountLine)] : []),
|
|
952
1100
|
r.forwarder.launchd ? `This computer: http://127.0.0.1:${CLI_PROXY_PORT} forwards to it (starts at login).` : `This computer: run shard cli-proxy forward to reach it at http://127.0.0.1:${CLI_PROXY_PORT}.`,
|
|
953
1101
|
r.machine && 'version' in r.machine ? `VMs: every new VM reaches it at http://127.0.0.1:${CLI_PROXY_PORT} (machine version ${r.machine.version}).` : '',
|
|
@@ -1012,5 +1160,6 @@ export const CLI_PROXY_HANDLERS = {
|
|
|
1012
1160
|
'cli-proxy': status,
|
|
1013
1161
|
'cli-proxy import': importHandler,
|
|
1014
1162
|
'cli-proxy export': exportHandler,
|
|
1163
|
+
'cli-proxy use': useHandler,
|
|
1015
1164
|
'cli-proxy forward': forwardHandler,
|
|
1016
1165
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shard settings` (0.20.1+): this computer's settings for its cloud conversations, kept in agents.json next to
|
|
3
|
+
* open_in and terminal_app. It sets the title marker (title_prefix), and shows where conversations open and the
|
|
4
|
+
* terminal app (set with `shard agents targets --open-in/--terminal`). The menu bar and the Linux app read and write
|
|
5
|
+
* the marker through it.
|
|
6
|
+
*
|
|
7
|
+
* The marker is what a conversation's Claude session title starts with in claude.ai and the Claude app
|
|
8
|
+
* ("☁ Fix login bug · todo-app"). Stored: absent for the platform's default, "" for none, else the text; the guest
|
|
9
|
+
* adds the space after it. New conversations send it as `title_prefix`, legacy agent launches as SF_TITLE_PREFIX.
|
|
10
|
+
*/
|
|
11
|
+
import type { Handler } from './commands.js';
|
|
12
|
+
import type { OpenIn } from './t3-mode.js';
|
|
13
|
+
/** The platform's marker (the guest's "☁ " without its space). */
|
|
14
|
+
export declare const DEFAULT_TITLE_PREFIX = "\u2601";
|
|
15
|
+
/** At most this many characters on one line, as the API takes it (POST /v1/conversations title_prefix). */
|
|
16
|
+
export declare const TITLE_PREFIX_MAX = 16;
|
|
17
|
+
/** `--title-prefix <text|none|default>`: null for the default (the stored choice is removed), "" for none. */
|
|
18
|
+
export declare function parseTitlePrefix(spec: string): string | null;
|
|
19
|
+
/** The marker this computer chose: undefined for the default (nothing is sent), "" for none. An unreadable or invalid
|
|
20
|
+
* stored value counts as the default, so it never blocks a start. */
|
|
21
|
+
export declare function storedTitlePrefix(dir: string): string | undefined;
|
|
22
|
+
export declare function saveTitlePrefix(dir: string, choice: string | null): void;
|
|
23
|
+
/** `shard settings --json`. title_prefix: null is the default, "" none. open_in and terminal_app are the saved
|
|
24
|
+
* choices (null: automatic). */
|
|
25
|
+
export declare function clientSettings(dir: string): {
|
|
26
|
+
title_prefix: string | null;
|
|
27
|
+
title_prefix_default: string;
|
|
28
|
+
open_in: {
|
|
29
|
+
claude: OpenIn | null;
|
|
30
|
+
codex: OpenIn | null;
|
|
31
|
+
};
|
|
32
|
+
terminal_app: {
|
|
33
|
+
name: string;
|
|
34
|
+
id: import("./terminal-apps.js").TerminalId;
|
|
35
|
+
mode: import("./terminal-apps.js").TerminalMode;
|
|
36
|
+
} | null;
|
|
37
|
+
};
|
|
38
|
+
export declare function settingsText(s: ReturnType<typeof clientSettings>): string;
|
|
39
|
+
export declare const SETTINGS_HANDLERS: Record<string, Handler>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { out } from "./commands.js";
|
|
2
|
+
import { AgentStateStore } from "./agent-state.js";
|
|
3
|
+
import { UsageError } from "./errors.js";
|
|
4
|
+
import { objectValue } from "./acp-rpc.js";
|
|
5
|
+
import { openInTargets } from "./t3-mode.js";
|
|
6
|
+
import { storedTerminal, terminalName } from "./terminal-apps.js";
|
|
7
|
+
/** The platform's marker (the guest's "☁ " without its space). */
|
|
8
|
+
export const DEFAULT_TITLE_PREFIX = '☁';
|
|
9
|
+
/** At most this many characters on one line, as the API takes it (POST /v1/conversations title_prefix). */
|
|
10
|
+
export const TITLE_PREFIX_MAX = 16;
|
|
11
|
+
const SAMPLE = 'Fix login bug · todo-app';
|
|
12
|
+
// The API's pattern: no C0 control characters and no DEL.
|
|
13
|
+
const control = (value) => [...value].some(c => { const n = c.codePointAt(0); return n < 0x20 || n === 0x7f; });
|
|
14
|
+
const validPrefix = (value) => !control(value) && [...value].length <= TITLE_PREFIX_MAX;
|
|
15
|
+
/** `--title-prefix <text|none|default>`: null for the default (the stored choice is removed), "" for none. */
|
|
16
|
+
export function parseTitlePrefix(spec) {
|
|
17
|
+
const text = spec.trim();
|
|
18
|
+
if (text === 'default')
|
|
19
|
+
return null;
|
|
20
|
+
if (text === 'none' || text === '')
|
|
21
|
+
return '';
|
|
22
|
+
if (control(text))
|
|
23
|
+
throw new UsageError('--title-prefix is one line of text: no tabs, line breaks or control characters.');
|
|
24
|
+
if ([...text].length > TITLE_PREFIX_MAX)
|
|
25
|
+
throw new UsageError(`--title-prefix is at most ${TITLE_PREFIX_MAX} characters (none for no marker, default for ${DEFAULT_TITLE_PREFIX}).`);
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
/** The marker this computer chose: undefined for the default (nothing is sent), "" for none. An unreadable or invalid
|
|
29
|
+
* stored value counts as the default, so it never blocks a start. */
|
|
30
|
+
export function storedTitlePrefix(dir) {
|
|
31
|
+
try {
|
|
32
|
+
const value = new AgentStateStore(dir).read().title_prefix;
|
|
33
|
+
return typeof value === 'string' && validPrefix(value) ? value : undefined;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function saveTitlePrefix(dir, choice) {
|
|
40
|
+
new AgentStateStore(dir).update(doc => { if (choice === null)
|
|
41
|
+
delete doc.title_prefix;
|
|
42
|
+
else
|
|
43
|
+
doc.title_prefix = choice; });
|
|
44
|
+
}
|
|
45
|
+
/** `shard settings --json`. title_prefix: null is the default, "" none. open_in and terminal_app are the saved
|
|
46
|
+
* choices (null: automatic). */
|
|
47
|
+
export function clientSettings(dir) {
|
|
48
|
+
const doc = new AgentStateStore(dir).read();
|
|
49
|
+
const saved = objectValue(doc.open_in);
|
|
50
|
+
const openIn = (app) => {
|
|
51
|
+
const value = saved[app];
|
|
52
|
+
return typeof value === 'string' && openInTargets(app).includes(value) ? value : null;
|
|
53
|
+
};
|
|
54
|
+
const terminal = storedTerminal(dir);
|
|
55
|
+
const prefix = typeof doc.title_prefix === 'string' && validPrefix(doc.title_prefix) ? doc.title_prefix : null;
|
|
56
|
+
return {
|
|
57
|
+
title_prefix: prefix,
|
|
58
|
+
title_prefix_default: DEFAULT_TITLE_PREFIX,
|
|
59
|
+
open_in: { claude: openIn('claude'), codex: openIn('codex') },
|
|
60
|
+
terminal_app: terminal ? { ...terminal, name: terminalName(terminal.id) } : null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const sampleTitle = (prefix) => `${prefix === null ? DEFAULT_TITLE_PREFIX + ' ' : prefix ? prefix + ' ' : ''}${SAMPLE}`;
|
|
64
|
+
const markerLine = (prefix) => prefix === null ? `${DEFAULT_TITLE_PREFIX} (default)` : prefix === '' ? 'none' : prefix;
|
|
65
|
+
export function settingsText(s) {
|
|
66
|
+
const target = (t) => t ?? 'automatic';
|
|
67
|
+
return [
|
|
68
|
+
`Title marker ${markerLine(s.title_prefix)} "${sampleTitle(s.title_prefix)}"`,
|
|
69
|
+
`Open in Claude: ${target(s.open_in.claude)} · Codex: ${target(s.open_in.codex)}`,
|
|
70
|
+
`Terminal app ${s.terminal_app ? `${s.terminal_app.name}, ${s.terminal_app.mode === 'tab' ? 'new tab' : 'new window'}` : 'automatic'}`,
|
|
71
|
+
'',
|
|
72
|
+
'Change the marker with shard settings --title-prefix <text|none|default>; where conversations open with shard agents targets --open-in and --terminal.',
|
|
73
|
+
].join('\n') + '\n';
|
|
74
|
+
}
|
|
75
|
+
function settings(ctx, values) {
|
|
76
|
+
const spec = values['title-prefix'];
|
|
77
|
+
if (spec !== undefined && typeof spec !== 'string')
|
|
78
|
+
throw new UsageError('--title-prefix takes text, none or default.');
|
|
79
|
+
if (typeof spec === 'string') {
|
|
80
|
+
const choice = parseTitlePrefix(spec);
|
|
81
|
+
saveTitlePrefix(ctx.store.dir, choice);
|
|
82
|
+
const s = clientSettings(ctx.store.dir);
|
|
83
|
+
out(ctx, `${s.title_prefix === '' ? 'New conversations have no title marker' : `New conversations’ titles start with ${markerLine(s.title_prefix)}`}: "${sampleTitle(s.title_prefix)}".\n`, s);
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
const s = clientSettings(ctx.store.dir);
|
|
87
|
+
out(ctx, settingsText(s), s);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
// A refused --title-prefix rejects the command's promise (a usage error, exit 2) rather than throwing.
|
|
91
|
+
export const SETTINGS_HANDLERS = { settings: (ctx, values) => Promise.resolve().then(() => settings(ctx, values)) };
|
package/dist/codex-cloud.d.ts
CHANGED
|
@@ -3,12 +3,18 @@ import type { Ctx, Handler } from './commands.js';
|
|
|
3
3
|
import type { Values } from './args.js';
|
|
4
4
|
import type { ModelChoice } from './agent-models.js';
|
|
5
5
|
import type { Conversation } from './conversations.js';
|
|
6
|
+
import type { LocalConversation } from './conversations.js';
|
|
6
7
|
import type { InitialImage } from './initial-prompt.js';
|
|
7
|
-
/**
|
|
8
|
-
export declare function openCodexThread(task: CodexCloudTask, env: Ctx['io']['env']): Promise<void>;
|
|
8
|
+
/** Cloud threads open in the browser, preserving the durable provider destination on every OS. */
|
|
9
|
+
export declare function openCodexThread(task: CodexCloudTask, env: Ctx['io']['env'], platform?: NodeJS.Platform): Promise<void>;
|
|
9
10
|
export declare function submitCloudInitialPrompt(ctx: Ctx, c: Conversation, prompt: string, workspaceKey: string): Promise<void>;
|
|
10
|
-
/** Prints the Cloud thread (and its link) and opens it in
|
|
11
|
+
/** Prints the Cloud thread (and its link) and opens it in your browser; --json prints it instead. */
|
|
11
12
|
export declare function printCloudThread(ctx: Ctx, c: Conversation, task: CodexCloudTask, submitted?: boolean, choice?: ModelChoice): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* A Cloud conversation where the Codex app cannot show it (0.23.0+): where its chat lives, whether it waits for an
|
|
15
|
+
* answer, and how to answer and follow it from here. Never a new start: the conversation exists.
|
|
16
|
+
*/
|
|
17
|
+
export declare function cloudChatElsewhere(c: Pick<Conversation, 'id' | 'key' | 'state'>, url: string): string;
|
|
12
18
|
/** How long the account's Codex remote device may take to come up before it counts as stuck. */
|
|
13
19
|
export declare const CODEX_STARTING_LIMIT_MS: number;
|
|
14
20
|
export type CodexConnectorView = {
|
|
@@ -48,5 +54,9 @@ export declare function ensureCodexCloud(ctx: Ctx): Promise<{
|
|
|
48
54
|
last_seen_at: string | null;
|
|
49
55
|
}>;
|
|
50
56
|
export declare function launchCodexCloud(ctx: Ctx, values: Values, path: string, images?: readonly InitialImage[]): Promise<number>;
|
|
51
|
-
|
|
57
|
+
/** A start whose carrying process said so in the last 45 s. */
|
|
58
|
+
export declare const startCarried: (start: NonNullable<LocalConversation["cloud_start"]>) => boolean;
|
|
59
|
+
export declare function openCodexConversation(ctx: Ctx, c: Conversation, history?: true | string): Promise<number>;
|
|
60
|
+
/** Continues the same accepted identity, never creates a replacement VM/chat. */
|
|
61
|
+
export declare function resumeCloudStartup(ctx: Ctx, c: Conversation): Promise<CodexCloudTask>;
|
|
52
62
|
export declare const CODEX_CLOUD_HANDLERS: Record<string, Handler>;
|