@hanamorilabs/tab 0.1.8 → 0.1.10
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/README.md +7 -2
- package/dist/cli.js +266 -18
- package/dist/clients.js +8 -4
- package/dist/logins.js +3 -1
- package/dist/manage.js +56 -5
- package/dist/pool-run.js +42 -0
- package/dist/pool.js +142 -0
- package/dist/proxy-bin.js +12 -2
- package/dist/version.js +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -51,7 +51,9 @@ tab claude # your own Claude login through the tab
|
|
|
51
51
|
tab codex # your own ChatGPT login (codex login once, inside tab codex)
|
|
52
52
|
tab grok # your SuperGrok login
|
|
53
53
|
tab kimi # your Kimi Code login
|
|
54
|
-
tab accounts # accounts seen: plan, seat price, 30 days at list,
|
|
54
|
+
tab accounts # accounts seen: plan, seat price, 30 days at list, how much is used
|
|
55
|
+
tab pool add claude work # another Claude login in its own folder; tab claude then runs as the one with most room
|
|
56
|
+
tab pool # the logins, and how used each is; tab pool at 80 sets when to move
|
|
55
57
|
tab agent kind me@joseairosa.com api # back to the meter
|
|
56
58
|
```
|
|
57
59
|
|
|
@@ -90,10 +92,13 @@ tab <agent> [args] claude, codex, grok, kimi, gemini, or any command, on the t
|
|
|
90
92
|
tab use pick or change the Agent this folder runs as (.flocktab)
|
|
91
93
|
tab agent create <name> --subscription|--api a new Agent of that kind (--cap 50)
|
|
92
94
|
tab agent kind <agent> api|subscription change it
|
|
93
|
-
tab accounts accounts your subscription Agents were seen on: plan, price, quota
|
|
95
|
+
tab accounts accounts your subscription Agents were seen on: plan, price, quota used
|
|
96
|
+
tab pool several logins of one vendor: add claude|codex <name>, at <percent>, swap auto|launch
|
|
94
97
|
tab alias setup <name>... make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
|
|
95
98
|
tab alias remove <name>... undo that; `tab alias list` shows them
|
|
96
99
|
tab status flock, folder Agent, proxy health, provider key state
|
|
100
|
+
tab log [-f] [--all] everything through the tab for this machine's Agents, one line per call
|
|
101
|
+
tab log --proxy [-f] self-hosted: the local proxy's own log (cached tokens, milliseconds)
|
|
97
102
|
tab version tab and proxy versions
|
|
98
103
|
tab up | down self-hosted: start or stop the local proxy
|
|
99
104
|
tab update the newest tab and proxy from npm; restarts a running local proxy
|
package/dist/cli.js
CHANGED
|
@@ -23,13 +23,17 @@
|
|
|
23
23
|
* terminal, so an interactive agent behaves exactly as it does without `tab`.
|
|
24
24
|
*/
|
|
25
25
|
import { spawn } from "node:child_process";
|
|
26
|
+
import { mkdir } from "node:fs/promises";
|
|
26
27
|
import { hostname } from "node:os";
|
|
28
|
+
import path from "node:path";
|
|
27
29
|
import { createInterface } from "node:readline/promises";
|
|
28
30
|
import { stdin, stdout } from "node:process";
|
|
29
31
|
import { bold, box, cyan, dim, green, heading, line, rows, underline, yellow } from "./ui.js";
|
|
30
32
|
import { envFor, knownClients, subscriptionVendorFor } from "./clients.js";
|
|
31
33
|
import { describeLogin, localLogin } from "./logins.js";
|
|
32
34
|
import { prepareCodexHome } from "./codex-home.js";
|
|
35
|
+
import { addMember, loadPool, memberEnv, poolVendorFor, prepareClaudeMember, removeMember, savePool, usedPct } from "./pool.js";
|
|
36
|
+
import { runPooled } from "./pool-run.js";
|
|
33
37
|
import { ConsoleApiError, createAgent, issueAgentKey, listAgents } from "./console-api.js";
|
|
34
38
|
import { consoleUrlFor, DeviceLoginError, startDeviceLogin, waitForApproval } from "./device-login.js";
|
|
35
39
|
import { clearConfig, configDir, configPath, HOSTED_PROXY, isLocalProxy, loadConfig, LOCAL_PROXY, normalizeProxyUrl, presentedKey, saveConfig, } from "./config.js";
|
|
@@ -37,7 +41,7 @@ import { reportFolder } from "./folder.js";
|
|
|
37
41
|
import { listAliases, pathLine, pathWithoutShims, removeAlias, shimDir, shimDirOnPath, validAliasName, writeAlias } from "./alias.js";
|
|
38
42
|
import * as manage from "./manage.js";
|
|
39
43
|
import { agentNameFor, projectRoot, readProject, writeProject } from "./project.js";
|
|
40
|
-
import { downloadProxy, hasProxyEnv, packagedBinPath, proxyBinPath, proxyEnvPath, proxyInstalled, proxyLogPath, PROXY_VERSION, startProxy, stopProxy, tailProxyLog, writeProxyEnv, } from "./proxy-bin.js";
|
|
44
|
+
import { downloadProxy, hasProxyEnv, packagedBinPath, proxyBinPath, proxyEnvPath, proxyInstalled, proxyLogPath, hasProxyLog, PROXY_VERSION, startProxy, stopProxy, tailProxyLog, writeProxyEnv, } from "./proxy-bin.js";
|
|
41
45
|
import { proxyHealth, waitHealthy } from "./selfhost.js";
|
|
42
46
|
import { TAB_VERSION } from "./version.js";
|
|
43
47
|
const say = (text) => console.error(text);
|
|
@@ -58,10 +62,15 @@ function usage() {
|
|
|
58
62
|
cmd("use", "pick or change the Agent this folder runs as (.flocktab)"),
|
|
59
63
|
cmd("agent create <name>", "a new Agent: --subscription (your own logins) or --api (the flock's key, metered)"),
|
|
60
64
|
cmd("agent kind <agent> api|subscription", "change it"),
|
|
61
|
-
cmd("accounts", "accounts your subscription Agents were seen on: plan, price, quota
|
|
65
|
+
cmd("accounts", "accounts your subscription Agents were seen on: plan, price, quota used"),
|
|
66
|
+
cmd("pool", "several logins of one vendor; tab claude runs as the one with most room"),
|
|
67
|
+
cmd("pool add claude|codex <name>", "sign another login in (its own folder; --dir <path> to use one you have)"),
|
|
68
|
+
cmd("pool at <percent>", "move to another login once this much of a window is used (default 80)"),
|
|
62
69
|
cmd("alias setup <name>...", "make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)"),
|
|
63
70
|
cmd("alias remove <name>...", "undo that; `tab alias list` shows them"),
|
|
64
71
|
cmd("status", "which flock, which Agent here, is the proxy up"),
|
|
72
|
+
cmd("log [-f] [--all]", "everything through the tab on this machine's Agents, one line per call; -f follows"),
|
|
73
|
+
cmd("log --proxy [-f]", "self-hosted: the local proxy's own log (cached tokens, milliseconds; no keys, logins or prompts)"),
|
|
65
74
|
cmd("version", "tab and proxy versions"),
|
|
66
75
|
cmd("list", "every Agent: state, spent of cap, window, project"),
|
|
67
76
|
cmd("close | open [agent]", "the kill switch; omit the Agent for this folder's"),
|
|
@@ -509,7 +518,51 @@ async function status() {
|
|
|
509
518
|
say(box(rows(pairs, 0).split("\n"), { title: "FlockTab", tone }));
|
|
510
519
|
return tone === "ok" ? 0 : 1;
|
|
511
520
|
}
|
|
512
|
-
|
|
521
|
+
/** `--pool`, `--no-pool` and `--as <member>` are tab's; everything else is the harness's. */
|
|
522
|
+
function takePoolFlags(argv) {
|
|
523
|
+
const args = [];
|
|
524
|
+
let noPool = false;
|
|
525
|
+
let asked = false;
|
|
526
|
+
let pinned;
|
|
527
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
528
|
+
const a = argv[i];
|
|
529
|
+
if (a === "--no-pool")
|
|
530
|
+
noPool = true;
|
|
531
|
+
else if (a === "--pool")
|
|
532
|
+
asked = true;
|
|
533
|
+
else if (a === "--as" && argv[i + 1])
|
|
534
|
+
pinned = argv[(i += 1)];
|
|
535
|
+
else if (a.startsWith("--as="))
|
|
536
|
+
pinned = a.slice(5);
|
|
537
|
+
else
|
|
538
|
+
args.push(a);
|
|
539
|
+
}
|
|
540
|
+
return { args, noPool, asked: asked || pinned !== undefined, ...(pinned ? { pinned } : {}) };
|
|
541
|
+
}
|
|
542
|
+
async function flockAccounts(config) {
|
|
543
|
+
return (await manage.api(config)("GET", "/api/cli/accounts")).accounts;
|
|
544
|
+
}
|
|
545
|
+
/** Each member with the login its folder holds and how used the vendor last said that account is. */
|
|
546
|
+
async function poolStandings(config, vendor, members) {
|
|
547
|
+
const accounts = await flockAccounts(config).catch(() => []);
|
|
548
|
+
return Promise.all(members.map(async (member) => {
|
|
549
|
+
const login = await localLogin(vendor, vendor === "anthropic" ? { claudeConfigDir: member.dir } : { codexHome: path.join(member.dir, "codex") });
|
|
550
|
+
const email = login?.email;
|
|
551
|
+
const account = email ? accounts.find((a) => a.provider === vendor && a.email?.toLowerCase() === email.toLowerCase()) : undefined;
|
|
552
|
+
return { member, used: account ? usedPct(account.quota) : undefined, ...(email ? { email } : {}) };
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
/** Quiet: no call of this Agent in flight, and no reply from the vendor in the last 20 seconds. */
|
|
556
|
+
async function poolIdle(config, vendor, agentName) {
|
|
557
|
+
const call = manage.api(config);
|
|
558
|
+
const { rows } = await call("GET", "/api/cli/ledger?limit=20");
|
|
559
|
+
if (rows.some((r) => r.agent === agentName && r.status === "pending"))
|
|
560
|
+
return false;
|
|
561
|
+
const recent = Date.now() - 20_000;
|
|
562
|
+
return !(await flockAccounts(config)).some((a) => a.provider === vendor && new Date(a.lastSeenAt).getTime() > recent);
|
|
563
|
+
}
|
|
564
|
+
async function runAgent(name, argv) {
|
|
565
|
+
const { args, noPool, asked, pinned } = takePoolFlags(argv);
|
|
513
566
|
const config = await ensureLogin();
|
|
514
567
|
if (!config)
|
|
515
568
|
return 2;
|
|
@@ -525,13 +578,43 @@ async function runAgent(name, args) {
|
|
|
525
578
|
// consumer plan, is metered with the flock's key.
|
|
526
579
|
const vendor = subscriptionVendorFor(name);
|
|
527
580
|
const me = await whoami(config.proxyUrl, { key: agent.key, unlock: config.unlock });
|
|
528
|
-
|
|
581
|
+
let kind = !("error" in me) && me.kind === "subscription" ? "subscription" : "api";
|
|
582
|
+
// A proxy older than 0.1.7 answers whoami without the kind. Guessing api
|
|
583
|
+
// would hand a subscription Agent's harness the tab key, so ask the console.
|
|
584
|
+
const { consoleUrl, token } = config;
|
|
585
|
+
if (!("error" in me) && me.kind === undefined && config.mode === "self-hosted") {
|
|
586
|
+
// Not restarted here: other harnesses may be streaming through it.
|
|
587
|
+
warn(`The local proxy is older than this tab. When nothing is running through it: ${bold("tab down && tab up")}`);
|
|
588
|
+
}
|
|
589
|
+
if (consoleUrl && token && ("error" in me || me.kind === undefined)) {
|
|
590
|
+
try {
|
|
591
|
+
const listed = await listAgents(consoleUrl, token);
|
|
592
|
+
if (listed.agents.find((a) => a.id === agent.agentId)?.kind === "subscription")
|
|
593
|
+
kind = "subscription";
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// Console unreachable: stay on what the proxy said.
|
|
597
|
+
}
|
|
598
|
+
}
|
|
529
599
|
const auth = kind === "subscription" && vendor ? "subscription" : "key";
|
|
530
600
|
// On a subscription there is no provider key to unlock; the bare key rides in the URL.
|
|
531
601
|
const key = auth === "subscription" ? agent.key : presentedKey({ key: agent.key, unlock: config.unlock });
|
|
532
602
|
reportFolder(config.proxyUrl, key);
|
|
533
603
|
const { spec, env } = envFor({ name, proxyUrl: config.proxyUrl, presentedKey: key, auth });
|
|
534
|
-
|
|
604
|
+
const poolVendor = auth === "subscription" ? poolVendorFor(name) : undefined;
|
|
605
|
+
const pool = poolVendor ? await loadPool() : undefined;
|
|
606
|
+
const members = (poolVendor && pool?.members[poolVendor]) || [];
|
|
607
|
+
const pooled = members.length > 0 && !noPool;
|
|
608
|
+
if (asked && !pooled) {
|
|
609
|
+
fail(auth !== "subscription"
|
|
610
|
+
? `A pool is for subscription Agents; this folder runs as an API-key Agent. ${dim("tab agent kind <agent> subscription")}`
|
|
611
|
+
: `No ${name} logins in the pool yet. ${dim(`tab pool add ${name} <name>`)}`);
|
|
612
|
+
return 2;
|
|
613
|
+
}
|
|
614
|
+
if (pooled) {
|
|
615
|
+
// The member's folder is where the harness keeps that login; tab sets the folder, never the login.
|
|
616
|
+
}
|
|
617
|
+
else if (auth === "subscription") {
|
|
535
618
|
const login = describeLogin(await localLogin(vendor));
|
|
536
619
|
const vendorName = vendor === "openai" ? "ChatGPT" : vendor === "xai" ? "SuperGrok" : vendor === "kimi" ? "Kimi" : "Claude";
|
|
537
620
|
say(dim(`${spec.label} on your own ${vendorName} login${login ? `: ${login}` : ""}. FlockTab keeps the record and the kill switch; the account is read off each reply.`));
|
|
@@ -550,22 +633,154 @@ async function runAgent(name, args) {
|
|
|
550
633
|
auth,
|
|
551
634
|
});
|
|
552
635
|
}
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
636
|
+
const launch = (launchArgs, launchEnv) => {
|
|
637
|
+
const child = spawn(spec.command, [...(spec.args ?? []), ...launchArgs], { stdio: "inherit", env: launchEnv });
|
|
638
|
+
const done = new Promise((resolve) => {
|
|
639
|
+
child.on("error", (err) => {
|
|
640
|
+
if (err.code === "ENOENT") {
|
|
641
|
+
fail(`${spec.label} is not installed (${spec.command} not on PATH). ${dim(spec.install)}`);
|
|
642
|
+
if (!knownClients().includes(name))
|
|
643
|
+
say(dim(`Not a tab command either; see ${bold("tab help")}.`));
|
|
644
|
+
resolve(127);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
fail(err.message);
|
|
648
|
+
resolve(1);
|
|
649
|
+
});
|
|
650
|
+
child.on("close", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
|
|
565
651
|
});
|
|
566
|
-
|
|
652
|
+
return { done, stop: () => void child.kill("SIGTERM") };
|
|
653
|
+
};
|
|
654
|
+
if (!pooled || !poolVendor || !pool)
|
|
655
|
+
return launch(args, env).done;
|
|
656
|
+
// Codex members each need their own home written before the first launch.
|
|
657
|
+
if (poolVendor === "openai") {
|
|
658
|
+
for (const member of members) {
|
|
659
|
+
await prepareCodexHome({ baseDir: member.dir, proxyUrl: config.proxyUrl, presentedKey: key, mode: config.mode, auth, userCodexHome: path.join(member.dir, "no-personal-login") });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return runPooled(args, {
|
|
663
|
+
harness: name,
|
|
664
|
+
at: pool.at,
|
|
665
|
+
swap: pool.swap,
|
|
666
|
+
...(pinned ? { pinned } : {}),
|
|
667
|
+
standings: () => poolStandings(config, poolVendor, members),
|
|
668
|
+
idle: () => poolIdle(config, poolVendor, agent.agentName),
|
|
669
|
+
start: (standing, launchArgs) => launch(launchArgs, { ...env, ...memberEnv(poolVendor, standing.member) }),
|
|
670
|
+
wait: (ms, until) => new Promise((resolve) => {
|
|
671
|
+
const timer = setTimeout(() => resolve(true), ms);
|
|
672
|
+
void until.then(() => {
|
|
673
|
+
clearTimeout(timer);
|
|
674
|
+
resolve(false);
|
|
675
|
+
});
|
|
676
|
+
}),
|
|
677
|
+
say: (text) => say(dim(text)),
|
|
567
678
|
});
|
|
568
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* `tab pool`: several logins of one vendor on this machine. Each member is
|
|
682
|
+
* a folder the harness signs in to itself; tab only chooses which folder a
|
|
683
|
+
* launch uses, from the usage the vendors report.
|
|
684
|
+
*/
|
|
685
|
+
async function poolCommand(argv) {
|
|
686
|
+
const [sub, ...rest] = argv;
|
|
687
|
+
const pool = await loadPool();
|
|
688
|
+
const vendorOf = (word) => poolVendorFor(word === "chatgpt" ? "codex" : (word ?? ""));
|
|
689
|
+
if (sub === "at") {
|
|
690
|
+
const pct = Number(rest[0]);
|
|
691
|
+
if (!Number.isFinite(pct) || pct < 1 || pct > 100) {
|
|
692
|
+
fail("tab pool at <percent>, from 1 to 100.");
|
|
693
|
+
return 2;
|
|
694
|
+
}
|
|
695
|
+
await savePool({ ...pool, at: Math.round(pct) });
|
|
696
|
+
ok(`The pool moves to another login at ${Math.round(pct)}% used.`);
|
|
697
|
+
return 0;
|
|
698
|
+
}
|
|
699
|
+
if (sub === "swap") {
|
|
700
|
+
if (rest[0] !== "auto" && rest[0] !== "launch") {
|
|
701
|
+
fail(`tab pool swap auto|launch. ${dim("auto: relaunch into the same conversation when idle. launch: only choose when a harness starts.")}`);
|
|
702
|
+
return 2;
|
|
703
|
+
}
|
|
704
|
+
await savePool({ ...pool, swap: rest[0] });
|
|
705
|
+
ok(rest[0] === "auto" ? "The pool moves a running harness when it is idle." : "The pool only chooses at launch.");
|
|
706
|
+
return 0;
|
|
707
|
+
}
|
|
708
|
+
if (sub === "add" || sub === "remove" || sub === "rm") {
|
|
709
|
+
const vendor = vendorOf(rest[0]);
|
|
710
|
+
const flags = manage.parseFlags(rest.slice(1));
|
|
711
|
+
const memberName = flags.args[0];
|
|
712
|
+
if (!vendor || !memberName) {
|
|
713
|
+
fail(`tab pool ${sub} claude|codex <name>${sub === "add" ? " [--dir <folder>]" : ""}`);
|
|
714
|
+
return 2;
|
|
715
|
+
}
|
|
716
|
+
if (sub !== "add") {
|
|
717
|
+
await savePool(removeMember(pool, vendor, memberName));
|
|
718
|
+
ok(`Removed ${bold(memberName)} from the pool. Its folder and login are left where they are.`);
|
|
719
|
+
return 0;
|
|
720
|
+
}
|
|
721
|
+
let next;
|
|
722
|
+
try {
|
|
723
|
+
next = addMember(pool, vendor, memberName, flags.opts.dir);
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
fail(err.message);
|
|
727
|
+
return 2;
|
|
728
|
+
}
|
|
729
|
+
const member = next.members[vendor].at(-1);
|
|
730
|
+
const loginOf = () => localLogin(vendor, vendor === "anthropic" ? { claudeConfigDir: member.dir } : { codexHome: path.join(member.dir, "codex") });
|
|
731
|
+
if (!flags.opts.dir && vendor === "anthropic")
|
|
732
|
+
await prepareClaudeMember(member.dir);
|
|
733
|
+
if (vendor === "openai")
|
|
734
|
+
await mkdir(path.join(member.dir, "codex"), { recursive: true, mode: 0o700 });
|
|
735
|
+
if (!(await loginOf())?.email) {
|
|
736
|
+
const harness = vendor === "anthropic" ? "claude" : "codex";
|
|
737
|
+
say(dim(vendor === "anthropic" ? `Opening Claude Code in ${member.dir}. Sign in as the account for ${memberName} (/login), then /exit.` : `Signing Codex in for ${memberName}.`));
|
|
738
|
+
// Plain launch: no proxy, no tab key. This only creates the login, in the harness's own store.
|
|
739
|
+
const loginEnv = { ...process.env, ...memberEnv(vendor, member), PATH: pathWithoutShims() };
|
|
740
|
+
for (const k of ["ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "OPENAI_BASE_URL", "OPENAI_API_KEY"])
|
|
741
|
+
delete loginEnv[k];
|
|
742
|
+
const code = await new Promise((resolve) => {
|
|
743
|
+
const child = spawn(harness, vendor === "anthropic" ? [] : ["login"], { stdio: "inherit", env: loginEnv });
|
|
744
|
+
child.on("error", () => resolve(127));
|
|
745
|
+
child.on("close", (c) => resolve(c ?? 1));
|
|
746
|
+
});
|
|
747
|
+
if (code === 127) {
|
|
748
|
+
fail(`${harness} is not installed.`);
|
|
749
|
+
return 127;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const login = await loginOf();
|
|
753
|
+
if (!login?.email) {
|
|
754
|
+
fail(`No login found in ${member.dir}. Nothing was added; run the same command again to retry.`);
|
|
755
|
+
return 1;
|
|
756
|
+
}
|
|
757
|
+
await savePool(next);
|
|
758
|
+
ok(`${bold(memberName)} is in the pool as ${describeLogin(login)}.`);
|
|
759
|
+
return 0;
|
|
760
|
+
}
|
|
761
|
+
if (sub && sub !== "list" && sub !== "ls") {
|
|
762
|
+
fail(`Unknown: tab pool ${sub}. ${dim("tab pool | add | remove | at | swap")}`);
|
|
763
|
+
return 2;
|
|
764
|
+
}
|
|
765
|
+
const config = await ensureLogin();
|
|
766
|
+
if (!config)
|
|
767
|
+
return 2;
|
|
768
|
+
const vendors = ["anthropic", "openai"].filter((v) => (pool.members[v] ?? []).length > 0);
|
|
769
|
+
if (vendors.length === 0) {
|
|
770
|
+
say(dim(`No logins in the pool. ${bold("tab pool add claude work")} signs one in; then ${bold("tab claude")} runs as whichever has most room.`));
|
|
771
|
+
return 0;
|
|
772
|
+
}
|
|
773
|
+
for (const vendor of vendors) {
|
|
774
|
+
const standings = await poolStandings(config, vendor, pool.members[vendor]);
|
|
775
|
+
say(heading(vendor === "anthropic" ? "Claude" : "ChatGPT (Codex)"));
|
|
776
|
+
say(rows(standings.map((s) => [
|
|
777
|
+
s.member.name,
|
|
778
|
+
`${s.email ?? yellow("not signed in")} ${s.used === undefined ? dim("not seen yet") : s.used >= pool.at ? yellow(`${Math.round(s.used)}% used`) : green(`${Math.round(s.used)}% used`)} ${dim(s.member.dir)}`,
|
|
779
|
+
]), 0));
|
|
780
|
+
}
|
|
781
|
+
say(dim(`Moves at ${pool.at}% used (${pool.swap === "auto" ? "also while running, when idle" : "at launch only"}). tab claude --as <name> pins one; --no-pool uses your usual login.`));
|
|
782
|
+
return 0;
|
|
783
|
+
}
|
|
569
784
|
/**
|
|
570
785
|
* `tab alias codex claude`: shims so the plain commands run on the tab.
|
|
571
786
|
* `tab alias` alone lists them; `tab unalias codex` removes one.
|
|
@@ -695,6 +910,29 @@ ${installed}` : "Updated.");
|
|
|
695
910
|
await stopProxy();
|
|
696
911
|
return (await bringUp(config.proxyUrl)) ? 0 : 1;
|
|
697
912
|
}
|
|
913
|
+
/**
|
|
914
|
+
* `tab logs`: the local proxy's log. One line per call: kind, vendor, Agent,
|
|
915
|
+
* model, outcome, status, tokens (cached apart) and duration. The proxy never
|
|
916
|
+
* writes a key, a login, a prompt or an email there. `-f` follows it.
|
|
917
|
+
*/
|
|
918
|
+
async function logs(args) {
|
|
919
|
+
const flags = manage.parseFlags(args);
|
|
920
|
+
const follow = args.includes("-f") || flags.opts.follow !== undefined;
|
|
921
|
+
const lines = Math.min(5000, Math.max(1, Number(flags.opts.lines ?? 50) || 50));
|
|
922
|
+
const file = proxyLogPath();
|
|
923
|
+
if (!(await hasProxyLog())) {
|
|
924
|
+
warn(`No local proxy log yet (${file}). It is written by the self-hosted proxy: ${bold("tab up")}.`);
|
|
925
|
+
return 1;
|
|
926
|
+
}
|
|
927
|
+
const [cmd, cmdArgs] = process.platform === "win32"
|
|
928
|
+
? ["powershell", ["-NoProfile", "-Command", `Get-Content -Path '${file}' -Tail ${lines}${follow ? " -Wait" : ""}`]]
|
|
929
|
+
: ["tail", [`-n`, String(lines), ...(follow ? ["-f"] : []), file]];
|
|
930
|
+
const child = spawn(cmd, cmdArgs, { stdio: "inherit" });
|
|
931
|
+
return new Promise((resolve) => {
|
|
932
|
+
child.on("error", () => resolve(1));
|
|
933
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
934
|
+
});
|
|
935
|
+
}
|
|
698
936
|
async function selfHosted(action) {
|
|
699
937
|
const config = await loadConfig();
|
|
700
938
|
const proxyUrl = config?.mode === "self-hosted" ? config.proxyUrl : LOCAL_PROXY;
|
|
@@ -751,6 +989,8 @@ async function managed(command, rest) {
|
|
|
751
989
|
return await manage.outside(config, flags);
|
|
752
990
|
case "live":
|
|
753
991
|
return await manage.live(config, flags);
|
|
992
|
+
case "log":
|
|
993
|
+
return await manage.log(config, manage.parseFlags(rest.filter((a) => a !== "-f")), rest.includes("-f") || flags.opts.follow !== undefined);
|
|
754
994
|
case "accounts":
|
|
755
995
|
case "quota":
|
|
756
996
|
return await manage.accounts(config, flags);
|
|
@@ -810,8 +1050,16 @@ async function main(argv) {
|
|
|
810
1050
|
case "agent":
|
|
811
1051
|
case "web":
|
|
812
1052
|
return managed(command, rest);
|
|
1053
|
+
case "pool":
|
|
1054
|
+
return poolCommand(rest);
|
|
813
1055
|
case "status":
|
|
814
1056
|
return status();
|
|
1057
|
+
case "log":
|
|
1058
|
+
if (rest.includes("--proxy"))
|
|
1059
|
+
return logs(rest.filter((a) => a !== "--proxy"));
|
|
1060
|
+
return managed("log", rest);
|
|
1061
|
+
case "logs":
|
|
1062
|
+
return logs(rest);
|
|
815
1063
|
case "up":
|
|
816
1064
|
return selfHosted("up");
|
|
817
1065
|
case "down":
|
package/dist/clients.js
CHANGED
|
@@ -107,10 +107,14 @@ export function envFor(input) {
|
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
109
|
env.ANTHROPIC_BASE_URL = input.proxyUrl;
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
110
|
+
// The tab key goes in as the auth token, not the API key. Claude Code
|
|
111
|
+
// asks a person to approve an `ANTHROPIC_API_KEY` once and remembers a
|
|
112
|
+
// No, after which it silently uses its own Claude login and the call
|
|
113
|
+
// arrives without the tab key. `ANTHROPIC_AUTH_TOKEN` is checked first,
|
|
114
|
+
// never asks, and switches the Claude login off for the session. It is
|
|
115
|
+
// sent as `Authorization: Bearer`, which the proxy reads like `x-api-key`.
|
|
116
|
+
env.ANTHROPIC_AUTH_TOKEN = input.presentedKey;
|
|
117
|
+
delete env.ANTHROPIC_API_KEY;
|
|
114
118
|
};
|
|
115
119
|
const setOpenAI = () => {
|
|
116
120
|
if (auth === "subscription") {
|
package/dist/logins.js
CHANGED
|
@@ -44,7 +44,9 @@ function jwtClaims(token) {
|
|
|
44
44
|
export async function localLogin(vendor, opts = {}) {
|
|
45
45
|
const home = opts.home ?? homedir();
|
|
46
46
|
if (vendor === "anthropic") {
|
|
47
|
-
|
|
47
|
+
// With CLAUDE_CONFIG_DIR set (a pool member), Claude Code keeps the file inside that folder.
|
|
48
|
+
const file = opts.claudeConfigDir ? path.join(opts.claudeConfigDir, ".claude.json") : path.join(home, ".claude.json");
|
|
49
|
+
const account = (await jsonFile(file))?.oauthAccount;
|
|
48
50
|
if (!account)
|
|
49
51
|
return undefined;
|
|
50
52
|
const email = str(account.emailAddress);
|
package/dist/manage.js
CHANGED
|
@@ -182,7 +182,7 @@ export async function agent(config, flags) {
|
|
|
182
182
|
/**
|
|
183
183
|
* `tab accounts` (also `tab quota`): every account the flock's subscription
|
|
184
184
|
* Agents were seen on, with the plan the vendor reported, its seat price,
|
|
185
|
-
* what the last 30 days would have cost at list, and
|
|
185
|
+
* what the last 30 days would have cost at list, and how much of the plan is used.
|
|
186
186
|
*/
|
|
187
187
|
export async function accounts(config, flags) {
|
|
188
188
|
const call = api(config);
|
|
@@ -193,9 +193,9 @@ export async function accounts(config, flags) {
|
|
|
193
193
|
const left = (w) => {
|
|
194
194
|
const pctLeft = Math.max(0, 100 - w.usedPct);
|
|
195
195
|
const paint = pctLeft <= 10 ? red : pctLeft <= 30 ? yellow : green;
|
|
196
|
-
return paint(`${
|
|
196
|
+
return paint(`${Math.min(100, Math.max(0, w.usedPct))}%`);
|
|
197
197
|
};
|
|
198
|
-
return table(["vendor", "account", "plan", "/month", "30d at list", "calls", "
|
|
198
|
+
return table(["vendor", "account", "plan", "/month", "30d at list", "calls", "used", "agents"], list.map((a) => [
|
|
199
199
|
a.vendor,
|
|
200
200
|
a.label ?? a.email ?? dim(a.id),
|
|
201
201
|
a.planLabel ?? dim("unknown"),
|
|
@@ -298,6 +298,55 @@ export async function key(config, flags) {
|
|
|
298
298
|
(flags.opts.show !== undefined ? `\n${issued.key}` : `\n${dim("Add --show to print it once.")}`));
|
|
299
299
|
return 0;
|
|
300
300
|
}
|
|
301
|
+
/** One call, one condensed line: when, which Agent, how it went, what it cost or was worth, what it used. */
|
|
302
|
+
export function logLine(r) {
|
|
303
|
+
const shadow = r.status === "shadow";
|
|
304
|
+
const status = shadow ? "SUBSCRIPTION" : (r.status ?? r.decision).toUpperCase();
|
|
305
|
+
const paint = status === "BLOCKED" || status === "CLOSED" ? red : status === "PENDING" ? yellow : green;
|
|
306
|
+
return `${dim(r.at)} ${bold(r.agent)} ${paint(status)} ${money(r.cents)}${shadow ? dim(" at list") : ""} ${dim(r.reason)}`;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* `tab log`: everything that went through the tab for the Agents on this
|
|
310
|
+
* machine, one condensed line per call, hosted or self-hosted alike because
|
|
311
|
+
* it is the ledger's own record. `-f` follows it; `--all` is the whole flock.
|
|
312
|
+
*/
|
|
313
|
+
export async function log(config, flags, follow) {
|
|
314
|
+
const call = api(config);
|
|
315
|
+
const limit = Math.min(500, Math.max(1, Number(flags.opts.lines ?? 30) || 30));
|
|
316
|
+
const mine = new Set(Object.values(config.agents ?? {}).map((a) => a.agentName.toLowerCase()));
|
|
317
|
+
const wanted = (r) => flags.opts.all !== undefined || mine.size === 0 || mine.has(r.agent.toLowerCase());
|
|
318
|
+
const fetchRows = async () => (await call("GET", `/api/cli/ledger?limit=${flags.opts.all !== undefined ? limit : limit * 4}`)).rows.filter(wanted).slice(0, limit);
|
|
319
|
+
const first = await fetchRows();
|
|
320
|
+
if (flags.json && !follow) {
|
|
321
|
+
out(JSON.stringify({ rows: first }, null, 2));
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
if (first.length === 0 && !follow)
|
|
325
|
+
out(dim("Nothing yet. Run tab claude or tab codex in a project folder."));
|
|
326
|
+
const seen = new Set();
|
|
327
|
+
for (const r of [...first].reverse()) {
|
|
328
|
+
seen.add(r.id);
|
|
329
|
+
out(flags.json ? JSON.stringify(r) : logLine(r));
|
|
330
|
+
}
|
|
331
|
+
if (!follow)
|
|
332
|
+
return 0;
|
|
333
|
+
for (;;) {
|
|
334
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
335
|
+
let rows;
|
|
336
|
+
try {
|
|
337
|
+
rows = await fetchRows();
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
for (const r of [...rows].reverse()) {
|
|
343
|
+
if (seen.has(r.id))
|
|
344
|
+
continue;
|
|
345
|
+
seen.add(r.id);
|
|
346
|
+
out(flags.json ? JSON.stringify(r) : logLine(r));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
301
350
|
export async function ledger(config, flags) {
|
|
302
351
|
const call = api(config);
|
|
303
352
|
const params = new URLSearchParams();
|
|
@@ -311,9 +360,11 @@ export async function ledger(config, flags) {
|
|
|
311
360
|
emit(flags.json, { timezone, rows: list }, () => list.length === 0
|
|
312
361
|
? dim("No decisions yet.")
|
|
313
362
|
: table(["time", "agent", "action", "amount", "status", "reason"], list.map((r) => {
|
|
314
|
-
|
|
363
|
+
// A subscription call is recorded at list price; nothing was held, so there is no "of".
|
|
364
|
+
const shadow = r.status === "shadow";
|
|
365
|
+
const status = shadow ? "SUBSCRIPTION" : (r.status ?? r.decision).toUpperCase();
|
|
315
366
|
const paint = status === "BLOCKED" || status === "CLOSED" ? red : status === "PENDING" ? yellow : green;
|
|
316
|
-
const held = r.reservedCents && r.reservedCents !== r.cents ? dim(` of ${money(r.reservedCents)}`) : "";
|
|
367
|
+
const held = !shadow && r.reservedCents && r.reservedCents !== r.cents ? dim(` of ${money(r.reservedCents)}`) : "";
|
|
317
368
|
return [r.at, r.agent, r.action.length > 28 ? `${r.action.slice(0, 27)}…` : r.action, `${money(r.cents)}${held}`, paint(status), r.reason];
|
|
318
369
|
}), { right: [3] }) + `\n${dim(`times in ${timezone}`)}`);
|
|
319
370
|
return 0;
|
package/dist/pool-run.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running a harness on the pool: choose the login at launch, watch what the
|
|
3
|
+
* vendors report, and when the login in use is over the threshold while
|
|
4
|
+
* another has room, wait for a quiet moment and relaunch into the same
|
|
5
|
+
* conversation as that other login.
|
|
6
|
+
*/
|
|
7
|
+
import { pickMember, relaunchArgs, shouldSwap } from "./pool.js";
|
|
8
|
+
const label = (s) => `${s.member.name}${s.email ? ` (${s.email})` : ""}${s.used === undefined ? "" : `, ${Math.round(s.used)}% used`}`;
|
|
9
|
+
export async function runPooled(args, deps) {
|
|
10
|
+
const all = await deps.standings();
|
|
11
|
+
let current = deps.pinned ? all.find((s) => s.member.name === deps.pinned) : pickMember(all, deps.at);
|
|
12
|
+
if (!current) {
|
|
13
|
+
deps.say(deps.pinned ? `No pool member named ${deps.pinned}. See tab pool.` : "The pool is empty. Add a login with tab pool add.");
|
|
14
|
+
return 2;
|
|
15
|
+
}
|
|
16
|
+
if (!deps.pinned && (current.used ?? 0) >= deps.at)
|
|
17
|
+
deps.say(`Every login in the pool is over ${deps.at}%. Running as the least used.`);
|
|
18
|
+
let launchArgs = args;
|
|
19
|
+
for (;;) {
|
|
20
|
+
deps.say(`Pool: running as ${label(current)}.`);
|
|
21
|
+
const child = deps.start(current, launchArgs);
|
|
22
|
+
let next;
|
|
23
|
+
if (!deps.pinned && deps.swap === "auto") {
|
|
24
|
+
while (await deps.wait(deps.intervalMs ?? 60_000, child.done)) {
|
|
25
|
+
// A failed look at the console is not a reason to touch a running harness.
|
|
26
|
+
const now = await deps.standings().catch(() => undefined);
|
|
27
|
+
const to = now ? shouldSwap(now, deps.at, current.member.name) : undefined;
|
|
28
|
+
if (to && (await deps.idle().catch(() => false))) {
|
|
29
|
+
next = to;
|
|
30
|
+
child.stop();
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const code = await child.done;
|
|
36
|
+
if (!next)
|
|
37
|
+
return code;
|
|
38
|
+
deps.say(`Pool: ${current.member.name} is over ${deps.at}%. Continuing as ${label(next)}; the conversation carries over, the vendor's prompt cache does not.`);
|
|
39
|
+
current = next;
|
|
40
|
+
launchArgs = relaunchArgs(deps.harness, args);
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/pool.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A subscription pool: several logins of one vendor on this machine, and the
|
|
3
|
+
* rule for which one a harness runs as. Each member is a folder the harness
|
|
4
|
+
* itself signs in to (`CLAUDE_CONFIG_DIR` for Claude Code, `CODEX_HOME` for
|
|
5
|
+
* Codex), so the login stays where the harness keeps it: `tab` never reads,
|
|
6
|
+
* copies or forwards a token. Usage comes from what the vendors report on
|
|
7
|
+
* every reply, already kept per account. When the login in use crosses the
|
|
8
|
+
* threshold and another has room, the harness is relaunched into the same
|
|
9
|
+
* conversation as that other login.
|
|
10
|
+
*/
|
|
11
|
+
import { chmod, mkdir, readdir, readFile, symlink, writeFile, lstat } from "node:fs/promises";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { configDir } from "./config.js";
|
|
15
|
+
export const DEFAULT_AT = 80;
|
|
16
|
+
export function poolPath(env = process.env) {
|
|
17
|
+
return path.join(configDir(env), "pool.json");
|
|
18
|
+
}
|
|
19
|
+
export function poolVendorFor(harness) {
|
|
20
|
+
if (harness === "claude")
|
|
21
|
+
return "anthropic";
|
|
22
|
+
if (harness === "codex")
|
|
23
|
+
return "openai";
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
export async function loadPool(env = process.env) {
|
|
27
|
+
const empty = { at: DEFAULT_AT, swap: "auto", members: {} };
|
|
28
|
+
try {
|
|
29
|
+
const raw = JSON.parse(await readFile(poolPath(env), "utf8"));
|
|
30
|
+
const at = typeof raw.at === "number" && raw.at >= 1 && raw.at <= 100 ? raw.at : DEFAULT_AT;
|
|
31
|
+
const members = {};
|
|
32
|
+
for (const vendor of ["anthropic", "openai"]) {
|
|
33
|
+
const list = raw.members?.[vendor];
|
|
34
|
+
if (Array.isArray(list))
|
|
35
|
+
members[vendor] = list.filter((m) => typeof m?.name === "string" && typeof m?.dir === "string").map((m) => ({ name: m.name, dir: m.dir }));
|
|
36
|
+
}
|
|
37
|
+
return { at, swap: raw.swap === "launch" ? "launch" : "auto", members };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return empty;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function savePool(pool, env = process.env) {
|
|
44
|
+
const file = poolPath(env);
|
|
45
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
46
|
+
await writeFile(file, `${JSON.stringify(pool, null, 2)}\n`, { mode: 0o600 });
|
|
47
|
+
await chmod(file, 0o600);
|
|
48
|
+
return file;
|
|
49
|
+
}
|
|
50
|
+
export function addMember(pool, vendor, name, dir, env = process.env) {
|
|
51
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._@-]{0,63}$/.test(name))
|
|
52
|
+
throw new Error("A member name is letters, digits, dot, dash, underscore or @, up to 64.");
|
|
53
|
+
const list = pool.members[vendor] ?? [];
|
|
54
|
+
if (list.some((m) => m.name === name))
|
|
55
|
+
throw new Error(`${name} is already in the pool.`);
|
|
56
|
+
const member = { name, dir: dir ? path.resolve(dir) : path.join(configDir(env), "pool", vendor, name) };
|
|
57
|
+
return { ...pool, members: { ...pool.members, [vendor]: [...list, member] } };
|
|
58
|
+
}
|
|
59
|
+
export function removeMember(pool, vendor, name) {
|
|
60
|
+
return { ...pool, members: { ...pool.members, [vendor]: (pool.members[vendor] ?? []).filter((m) => m.name !== name) } };
|
|
61
|
+
}
|
|
62
|
+
/** The env that makes a harness use this member's login folder. */
|
|
63
|
+
export function memberEnv(vendor, member) {
|
|
64
|
+
return vendor === "anthropic" ? { CLAUDE_CONFIG_DIR: member.dir } : { CODEX_HOME: path.join(member.dir, "codex") };
|
|
65
|
+
}
|
|
66
|
+
/** How used an account is: its fullest window that has not reset yet. */
|
|
67
|
+
export function usedPct(quota, now = new Date()) {
|
|
68
|
+
const live = quota.filter((w) => !w.resetsAt || new Date(w.resetsAt).getTime() > now.getTime());
|
|
69
|
+
if (quota.length === 0)
|
|
70
|
+
return undefined;
|
|
71
|
+
return live.reduce((max, w) => Math.max(max, w.usedPct), 0);
|
|
72
|
+
}
|
|
73
|
+
const room = (s) => s.used ?? 0;
|
|
74
|
+
/** Stay on the current login while it is under the threshold; else the one with most room. */
|
|
75
|
+
export function pickMember(all, at, current) {
|
|
76
|
+
if (all.length === 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
const now = all.find((s) => s.member.name === current);
|
|
79
|
+
if (now && room(now) < at)
|
|
80
|
+
return now;
|
|
81
|
+
return [...all].sort((a, b) => room(a) - room(b))[0];
|
|
82
|
+
}
|
|
83
|
+
/** The login to move to, when the current one is over and another has room. */
|
|
84
|
+
export function shouldSwap(all, at, current) {
|
|
85
|
+
const now = all.find((s) => s.member.name === current);
|
|
86
|
+
if (!now || room(now) < at)
|
|
87
|
+
return undefined;
|
|
88
|
+
const best = pickMember(all, at, current);
|
|
89
|
+
return best && best.member.name !== current && room(best) < at ? best : undefined;
|
|
90
|
+
}
|
|
91
|
+
/** Arguments that reopen the conversation the previous login was in. */
|
|
92
|
+
export function relaunchArgs(harness, args) {
|
|
93
|
+
if (harness === "codex")
|
|
94
|
+
return ["resume", "--last"];
|
|
95
|
+
const kept = [];
|
|
96
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
97
|
+
const a = args[i];
|
|
98
|
+
if (a === "--continue" || a === "-c")
|
|
99
|
+
continue;
|
|
100
|
+
// One-shot and resume arguments belong to the first launch only.
|
|
101
|
+
if (a === "--resume" || a === "-r" || a === "-p" || a === "--print") {
|
|
102
|
+
if (args[i + 1] && !args[i + 1].startsWith("-"))
|
|
103
|
+
i += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
kept.push(a);
|
|
107
|
+
}
|
|
108
|
+
return [...kept, "--continue"];
|
|
109
|
+
}
|
|
110
|
+
const NEVER_SHARED = new Set([".credentials.json", ".claude.json", ".claude.json.backup"]);
|
|
111
|
+
/**
|
|
112
|
+
* A Claude member folder: everything in `~/.claude` linked in (settings,
|
|
113
|
+
* skills, and `projects`, which is what lets `--continue` find the
|
|
114
|
+
* conversation under another login), except the login itself. The member's
|
|
115
|
+
* own `.claude.json` starts as a copy of the person's without the account.
|
|
116
|
+
*/
|
|
117
|
+
export async function prepareClaudeMember(dir, home = homedir()) {
|
|
118
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
119
|
+
const shared = path.join(home, ".claude");
|
|
120
|
+
for (const entry of await readdir(shared).catch(() => [])) {
|
|
121
|
+
if (NEVER_SHARED.has(entry))
|
|
122
|
+
continue;
|
|
123
|
+
const target = path.join(dir, entry);
|
|
124
|
+
if (await lstat(target).then(() => true, () => false))
|
|
125
|
+
continue;
|
|
126
|
+
await symlink(path.join(shared, entry), target);
|
|
127
|
+
}
|
|
128
|
+
const own = path.join(dir, ".claude.json");
|
|
129
|
+
if (await lstat(own).then(() => true, () => false))
|
|
130
|
+
return;
|
|
131
|
+
let seed = {};
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(await readFile(path.join(home, ".claude.json"), "utf8"));
|
|
134
|
+
if (parsed && typeof parsed === "object")
|
|
135
|
+
seed = { ...parsed };
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// No file to start from: Claude Code makes its own.
|
|
139
|
+
}
|
|
140
|
+
delete seed.oauthAccount;
|
|
141
|
+
await writeFile(own, JSON.stringify(seed), { mode: 0o600 });
|
|
142
|
+
}
|
package/dist/proxy-bin.js
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* runtime beyond the binary itself.
|
|
9
9
|
*/
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
|
-
import { chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
11
|
+
import { access, chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { configDir } from "./config.js";
|
|
15
15
|
/** The proxy release `tab up` fetches. Bump with each proxy tag. */
|
|
16
|
-
export const PROXY_VERSION = "0.1.
|
|
16
|
+
export const PROXY_VERSION = "0.1.7";
|
|
17
17
|
export const RELEASES = "https://github.com/joseairosa/flocktab/releases/download";
|
|
18
18
|
export function targetFor(platform = process.platform, arch = process.arch) {
|
|
19
19
|
if (platform === "darwin")
|
|
@@ -214,3 +214,13 @@ export async function tailProxyLog(lines = 8, env = process.env) {
|
|
|
214
214
|
return [];
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
+
/** Whether the local proxy has written a log yet. */
|
|
218
|
+
export async function hasProxyLog(env = process.env) {
|
|
219
|
+
try {
|
|
220
|
+
await access(proxyLogPath(env));
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Written by scripts/write-version.mjs from package.json at build; `tab version` prints it. */
|
|
2
|
-
export const TAB_VERSION = "0.1.
|
|
2
|
+
export const TAB_VERSION = "0.1.10";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanamorilabs/tab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Run any AI agent on a FlockTab tab: tab claude, tab codex, tab <command>.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"prepublishOnly": "pnpm build"
|
|
23
23
|
},
|
|
24
24
|
"optionalDependencies": {
|
|
25
|
-
"@hanamorilabs/flocktab-proxy-darwin-arm64": "0.1.
|
|
26
|
-
"@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.
|
|
27
|
-
"@hanamorilabs/flocktab-proxy-linux-x64": "0.1.
|
|
28
|
-
"@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.
|
|
29
|
-
"@hanamorilabs/flocktab-proxy-win-x64": "0.1.
|
|
25
|
+
"@hanamorilabs/flocktab-proxy-darwin-arm64": "0.1.7",
|
|
26
|
+
"@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.7",
|
|
27
|
+
"@hanamorilabs/flocktab-proxy-linux-x64": "0.1.7",
|
|
28
|
+
"@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.7",
|
|
29
|
+
"@hanamorilabs/flocktab-proxy-win-x64": "0.1.7"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.18.6",
|