@hanamorilabs/tab 0.1.5 → 0.1.6
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 +23 -1
- package/dist/cli.js +53 -7
- package/dist/clients.js +25 -1
- package/dist/codex-home.js +57 -4
- package/dist/manage.js +45 -1
- package/dist/proxy-bin.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -38,6 +38,25 @@ is keyed, and keeps it in the config file so `tab claude` is one command.
|
|
|
38
38
|
Set `FLOCKTAB_PROXY_URL`, `FLOCKTAB_KEY` and `FLOCKTAB_UNLOCK` instead when
|
|
39
39
|
you would rather not have a file or a folder, for example in CI.
|
|
40
40
|
|
|
41
|
+
## On a subscription (Claude Max, ChatGPT)
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
tab use --subscription --plan "Claude Max 5x" --price 100
|
|
45
|
+
tab claude # your own Claude login through the tab
|
|
46
|
+
tab codex # your own ChatGPT login (codex login once, inside tab codex)
|
|
47
|
+
tab quota # 5h / 7d windows the vendor reported on the last call
|
|
48
|
+
tab use --api-key # back to the metered tab
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The plan pays for the tokens; the tab still gates every call (kill switch,
|
|
52
|
+
velocity, model and tool policy) and records it at list price as a
|
|
53
|
+
`SUBSCRIPTION` row that never touches the cap. Claude Code gets
|
|
54
|
+
`ANTHROPIC_BASE_URL=<proxy>/t/<key>/anthropic` and no API key; Codex gets a
|
|
55
|
+
home whose `chatgpt_base_url` is `<proxy>/t/<key>/openai/backend-api/`, with
|
|
56
|
+
your existing `~/.codex/auth.json` copied in once (never written back). The
|
|
57
|
+
mode is the Agent's, read on every launch, so the console's **Runs on** panel
|
|
58
|
+
and `tab use` agree. `docs/subscriptions.md` has the wire.
|
|
59
|
+
|
|
41
60
|
## Self-hosted
|
|
42
61
|
|
|
43
62
|
The proxy runs on your box and holds the provider keys; the ledger, tabs and
|
|
@@ -59,6 +78,8 @@ Codex shows `FlockTab - Self-hosted` as its provider.
|
|
|
59
78
|
tab login approve this machine in the console, once
|
|
60
79
|
tab <agent> [args] claude, codex, grok, kimi, gemini, or any command, on the tab
|
|
61
80
|
tab use pick or change the Agent this folder runs as (.flocktab)
|
|
81
|
+
tab use --subscription | --api-key this Agent on your Claude Max / ChatGPT plan, or back on the meter
|
|
82
|
+
tab quota [agent] what the subscription has left: 5h and 7d windows
|
|
62
83
|
tab alias setup <name>... make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
|
|
63
84
|
tab alias remove <name>... undo that; `tab alias list` shows them
|
|
64
85
|
tab status flock, folder Agent, proxy health, provider key state
|
|
@@ -76,7 +97,7 @@ session from `tab login`. An Agent is named by slug or name; left out, it is
|
|
|
76
97
|
this folder's Agent. Every read takes `--json`.
|
|
77
98
|
|
|
78
99
|
```
|
|
79
|
-
tab list every Agent: state, spent of cap, window, project
|
|
100
|
+
tab list every Agent: state, mode (meter | plan), spent of cap, window, project
|
|
80
101
|
tab close my-project tab open my-project the kill switch (402 tab_closed on the next call)
|
|
81
102
|
tab cap my-project 25 --window week cap in dollars, and the window
|
|
82
103
|
tab rename my-project "My project (nightly)" the slug stays
|
|
@@ -121,5 +142,6 @@ proxy http://127.0.0.1:8787 (self-hosted) up
|
|
|
121
142
|
flock Hanamori Labs
|
|
122
143
|
folder flocktab-codex
|
|
123
144
|
plan team
|
|
145
|
+
mode metered (the flock's provider key, charged to the tab)
|
|
124
146
|
byok provider keys on this box
|
|
125
147
|
```
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* tab codex [...args] Codex on the tab
|
|
8
8
|
* tab <cmd> [...args] anything else, both APIs pointed at the tab
|
|
9
9
|
* tab use pick (or change) the Agent this folder runs as
|
|
10
|
+
* tab use --subscription this folder's Agent runs on your Claude Max / ChatGPT plan
|
|
11
|
+
* tab quota what the plan has left (5h / 7d windows) for this Agent
|
|
10
12
|
* tab alias setup codex make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
|
|
11
13
|
* tab alias remove codex undo that; `tab alias list` shows them
|
|
12
14
|
* tab status which flock, which Agent here, is the proxy up
|
|
@@ -53,6 +55,9 @@ function usage() {
|
|
|
53
55
|
cmd(others.join(" | tab "), "the same, for those agents"),
|
|
54
56
|
cmd("<command> [args]", "any other agent, both APIs pointed at the tab"),
|
|
55
57
|
cmd("use", "pick or change the Agent this folder runs as (.flocktab)"),
|
|
58
|
+
cmd("use --subscription", "this Agent runs on your Claude Max / ChatGPT plan (--plan \"Max 5x\" --price 100)"),
|
|
59
|
+
cmd("use --api-key", "back to the metered tab with the flock's provider key"),
|
|
60
|
+
cmd("quota [agent]", "what the subscription has left: 5h and 7d windows"),
|
|
56
61
|
cmd("alias setup <name>...", "make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)"),
|
|
57
62
|
cmd("alias remove <name>...", "undo that; `tab alias list` shows them"),
|
|
58
63
|
cmd("status", "which flock, which Agent here, is the proxy up"),
|
|
@@ -480,6 +485,7 @@ async function status() {
|
|
|
480
485
|
}
|
|
481
486
|
else {
|
|
482
487
|
pairs.push(["plan", me.flock.plan]);
|
|
488
|
+
pairs.push(["mode", me.authMode === "subscription" ? `${bold("subscription")} ${dim("(your Claude Max / ChatGPT login; FlockTab keeps the record)")}` : `metered ${dim("(the flock's provider key, charged to the tab)")}`]);
|
|
483
489
|
const byok = !me.byok
|
|
484
490
|
? dim("no provider key on this flock")
|
|
485
491
|
: me.unlock === "none"
|
|
@@ -506,9 +512,21 @@ async function runAgent(name, args) {
|
|
|
506
512
|
const agent = await resolveAgent(config);
|
|
507
513
|
if (!agent)
|
|
508
514
|
return 1;
|
|
509
|
-
|
|
515
|
+
// The Agent's mode is the console's call, read fresh on every launch so a
|
|
516
|
+
// switch there (or `tab use --subscription`) applies to the next run.
|
|
517
|
+
const me = await whoami(config.proxyUrl, { key: agent.key, unlock: config.unlock });
|
|
518
|
+
const auth = !("error" in me) && me.authMode === "subscription" ? "subscription" : "key";
|
|
519
|
+
// On a subscription there is no provider key to unlock; the bare key rides in the URL.
|
|
520
|
+
const key = auth === "subscription" ? agent.key : presentedKey({ key: agent.key, unlock: config.unlock });
|
|
510
521
|
reportFolder(config.proxyUrl, key);
|
|
511
|
-
const { spec, env } = envFor({ name, proxyUrl: config.proxyUrl, presentedKey: key });
|
|
522
|
+
const { spec, env } = envFor({ name, proxyUrl: config.proxyUrl, presentedKey: key, auth });
|
|
523
|
+
if (auth === "subscription") {
|
|
524
|
+
if (spec.shape === "openai" && spec.isolatedHome !== "codex") {
|
|
525
|
+
fail(`${spec.label} has no subscription route. Only Claude Code and Codex can run on a plan; switch this Agent back with ${bold("tab use --api-key")}.`);
|
|
526
|
+
return 2;
|
|
527
|
+
}
|
|
528
|
+
say(dim(`${agent.agentName} runs on your own ${spec.isolatedHome === "codex" ? "ChatGPT" : "Claude"} login; FlockTab keeps the record and the kill switch.`));
|
|
529
|
+
}
|
|
512
530
|
// An alias shim named like the agent must not be what we spawn.
|
|
513
531
|
env.PATH = pathWithoutShims();
|
|
514
532
|
if (spec.isolatedHome === "codex") {
|
|
@@ -517,6 +535,7 @@ async function runAgent(name, args) {
|
|
|
517
535
|
proxyUrl: config.proxyUrl,
|
|
518
536
|
presentedKey: key,
|
|
519
537
|
mode: config.mode,
|
|
538
|
+
auth,
|
|
520
539
|
});
|
|
521
540
|
}
|
|
522
541
|
const child = spawn(spec.command, [...(spec.args ?? []), ...args], { stdio: "inherit", env });
|
|
@@ -588,15 +607,39 @@ function pathHint() {
|
|
|
588
607
|
say(` ${bold(pathLine())}`);
|
|
589
608
|
return 0;
|
|
590
609
|
}
|
|
591
|
-
/**
|
|
592
|
-
|
|
610
|
+
/**
|
|
611
|
+
* `tab use`: pick or change the Agent for this folder. With `--subscription`
|
|
612
|
+
* or `--api-key` the folder's Agent (picked first if there is none) is
|
|
613
|
+
* switched in the console; `--plan` and `--price` label the seat.
|
|
614
|
+
*/
|
|
615
|
+
async function use(args) {
|
|
593
616
|
const config = await ensureLogin();
|
|
594
617
|
if (!config)
|
|
595
618
|
return 2;
|
|
619
|
+
const flags = manage.parseFlags(args);
|
|
620
|
+
const wantsSubscription = flags.opts.subscription !== undefined;
|
|
621
|
+
const wantsKey = flags.opts["api-key"] !== undefined || flags.opts.apikey !== undefined;
|
|
622
|
+
if (wantsSubscription && wantsKey) {
|
|
623
|
+
fail("Pick one: tab use --subscription or tab use --api-key.");
|
|
624
|
+
return 2;
|
|
625
|
+
}
|
|
596
626
|
if (!(await ensureProxy(config)))
|
|
597
627
|
return 1;
|
|
598
|
-
|
|
599
|
-
|
|
628
|
+
if (!wantsSubscription && !wantsKey) {
|
|
629
|
+
const agent = await resolveAgent(config, { pick: true });
|
|
630
|
+
return agent ? 0 : 1;
|
|
631
|
+
}
|
|
632
|
+
const agent = await resolveAgent(config);
|
|
633
|
+
if (!agent)
|
|
634
|
+
return 1;
|
|
635
|
+
try {
|
|
636
|
+
const project = await readProject(process.cwd());
|
|
637
|
+
return await manage.setAuthMode(config, flags, project?.agent ?? agent.agentName, wantsSubscription ? "subscription" : "api_key");
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
641
|
+
return 1;
|
|
642
|
+
}
|
|
600
643
|
}
|
|
601
644
|
/**
|
|
602
645
|
* First `tab up` on a box: the provider keys the proxy will spend, typed
|
|
@@ -720,6 +763,8 @@ async function managed(command, rest) {
|
|
|
720
763
|
return await manage.outside(config, flags);
|
|
721
764
|
case "live":
|
|
722
765
|
return await manage.live(config, flags);
|
|
766
|
+
case "quota":
|
|
767
|
+
return await manage.quota(config, flags);
|
|
723
768
|
default:
|
|
724
769
|
return await manage.web(config, flags);
|
|
725
770
|
}
|
|
@@ -745,7 +790,7 @@ async function main(argv) {
|
|
|
745
790
|
ok("Forgot the session and keys on this machine.");
|
|
746
791
|
return 0;
|
|
747
792
|
case "use":
|
|
748
|
-
return use();
|
|
793
|
+
return use(rest);
|
|
749
794
|
case "alias":
|
|
750
795
|
return alias(rest);
|
|
751
796
|
case "unalias":
|
|
@@ -769,6 +814,7 @@ async function main(argv) {
|
|
|
769
814
|
case "spend":
|
|
770
815
|
case "outside":
|
|
771
816
|
case "live":
|
|
817
|
+
case "quota":
|
|
772
818
|
case "web":
|
|
773
819
|
return managed(command, rest);
|
|
774
820
|
case "status":
|
package/dist/clients.js
CHANGED
|
@@ -72,11 +72,27 @@ export function openaiBase(proxyUrl, provider) {
|
|
|
72
72
|
* overridden on purpose: a stray `OPENAI_BASE_URL` from another tool would
|
|
73
73
|
* otherwise route around the tab, which is the one thing this must not do.
|
|
74
74
|
*/
|
|
75
|
+
/** `<proxy>/t/<key>/<provider>`: the subscription passthrough base for a vendor. */
|
|
76
|
+
export function passthroughBase(proxyUrl, key, provider) {
|
|
77
|
+
return `${proxyUrl}/t/${key}/${provider}`;
|
|
78
|
+
}
|
|
75
79
|
export function envFor(input) {
|
|
76
|
-
const
|
|
80
|
+
const base = clientFor(input.name);
|
|
77
81
|
const known = input.name in KNOWN;
|
|
82
|
+
const auth = input.auth ?? "key";
|
|
78
83
|
const env = { ...(input.base ?? process.env) };
|
|
84
|
+
// Codex: pinned to the API key on a metered tab, to the ChatGPT login on a subscription.
|
|
85
|
+
const spec = base.isolatedHome === "codex"
|
|
86
|
+
? { ...base, args: ["-c", auth === "subscription" ? 'preferred_auth_method="chatgpt"' : 'preferred_auth_method="apikey"'] }
|
|
87
|
+
: base;
|
|
79
88
|
const setAnthropic = () => {
|
|
89
|
+
if (auth === "subscription") {
|
|
90
|
+
// Claude Code signs in with its own Claude account; FlockTab only moves the base URL.
|
|
91
|
+
env.ANTHROPIC_BASE_URL = passthroughBase(input.proxyUrl, input.presentedKey, "anthropic");
|
|
92
|
+
delete env.ANTHROPIC_API_KEY;
|
|
93
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
80
96
|
env.ANTHROPIC_BASE_URL = input.proxyUrl;
|
|
81
97
|
env.ANTHROPIC_API_KEY = input.presentedKey;
|
|
82
98
|
// Claude Code prefers an auth token when one is set; make sure a stale
|
|
@@ -84,6 +100,13 @@ export function envFor(input) {
|
|
|
84
100
|
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
85
101
|
};
|
|
86
102
|
const setOpenAI = () => {
|
|
103
|
+
if (auth === "subscription") {
|
|
104
|
+
// Only Codex has a subscription route (its own home points at it); a
|
|
105
|
+
// plain OpenAI key would bypass the plan, so none is set.
|
|
106
|
+
delete env.OPENAI_BASE_URL;
|
|
107
|
+
delete env.OPENAI_API_KEY;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
87
110
|
env.OPENAI_BASE_URL = openaiBase(input.proxyUrl, spec.provider);
|
|
88
111
|
env.OPENAI_API_KEY = input.presentedKey;
|
|
89
112
|
};
|
|
@@ -99,5 +122,6 @@ export function envFor(input) {
|
|
|
99
122
|
}
|
|
100
123
|
// So the child, and anything it spawns, can tell it is on a tab.
|
|
101
124
|
env.FLOCKTAB_PROXY_URL = input.proxyUrl;
|
|
125
|
+
env.FLOCKTAB_AUTH_MODE = auth;
|
|
102
126
|
return { spec, env };
|
|
103
127
|
}
|
package/dist/codex-home.js
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The person's own `~/.codex` is never read or written.
|
|
12
12
|
*/
|
|
13
|
-
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
13
|
+
import { chmod, copyFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
14
|
+
import { homedir } from "node:os";
|
|
14
15
|
import path from "node:path";
|
|
16
|
+
import { passthroughBase } from "./clients.js";
|
|
15
17
|
export function codexAuthJson(presentedKey) {
|
|
16
18
|
return `${JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: presentedKey })}\n`;
|
|
17
19
|
}
|
|
@@ -19,6 +21,20 @@ export function codexAuthJson(presentedKey) {
|
|
|
19
21
|
export function codexProviderId(mode) {
|
|
20
22
|
return mode === "self-hosted" ? "FlockTab - Self-hosted" : "FlockTab - Hosted";
|
|
21
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* On a ChatGPT subscription Codex talks to `chatgpt_base_url` with its own
|
|
26
|
+
* login, so that is what moves: to the passthrough, where the tab keeps the
|
|
27
|
+
* record. No provider block; the default OpenAI provider is the ChatGPT one.
|
|
28
|
+
*/
|
|
29
|
+
export function codexSubscriptionToml(proxyUrl, presentedKey, model) {
|
|
30
|
+
const lines = [
|
|
31
|
+
`preferred_auth_method = "chatgpt"`,
|
|
32
|
+
`chatgpt_base_url = ${JSON.stringify(`${passthroughBase(proxyUrl, presentedKey, "openai")}/backend-api/`)}`,
|
|
33
|
+
...(model ? [`model = ${JSON.stringify(model)}`] : []),
|
|
34
|
+
``,
|
|
35
|
+
];
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
22
38
|
export function codexConfigToml(proxyUrl, model, mode = "hosted") {
|
|
23
39
|
const id = codexProviderId(mode);
|
|
24
40
|
const lines = [
|
|
@@ -41,9 +57,46 @@ export async function prepareCodexHome(input) {
|
|
|
41
57
|
await chmod(home, 0o700);
|
|
42
58
|
const auth = path.join(home, "auth.json");
|
|
43
59
|
const config = path.join(home, "config.toml");
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
if (input.auth === "subscription") {
|
|
61
|
+
await adoptChatGptLogin(auth, input.userCodexHome ?? path.join(homedir(), ".codex"));
|
|
62
|
+
await writeFile(config, codexSubscriptionToml(input.proxyUrl, input.presentedKey, input.model), { mode: 0o600 });
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await writeFile(auth, codexAuthJson(input.presentedKey), { mode: 0o600 });
|
|
66
|
+
await chmod(auth, 0o600);
|
|
67
|
+
await writeFile(config, codexConfigToml(input.proxyUrl, input.model, input.mode ?? "hosted"), { mode: 0o600 });
|
|
68
|
+
}
|
|
47
69
|
await chmod(config, 0o600);
|
|
48
70
|
return home;
|
|
49
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The ChatGPT login for the tab's Codex home. A login already there (from
|
|
74
|
+
* `codex login` run through `tab codex`) stays. Otherwise the person's own
|
|
75
|
+
* `~/.codex/auth.json` is copied, owner-only: same machine, same person,
|
|
76
|
+
* and `~/.codex` itself is still never written. A leftover API-key auth
|
|
77
|
+
* file from metered mode is replaced; with nothing to copy, Codex asks the
|
|
78
|
+
* person to sign in and keeps that login in the tab home.
|
|
79
|
+
*/
|
|
80
|
+
async function adoptChatGptLogin(auth, userCodexHome) {
|
|
81
|
+
const isApiKey = async (file) => {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
84
|
+
return parsed.auth_mode === "apikey" || (typeof parsed.OPENAI_API_KEY === "string" && parsed.OPENAI_API_KEY.startsWith("ft_"));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const existing = await isApiKey(auth);
|
|
91
|
+
if (existing === false)
|
|
92
|
+
return "kept";
|
|
93
|
+
if (existing === true)
|
|
94
|
+
await unlink(auth).catch(() => undefined);
|
|
95
|
+
const theirs = path.join(userCodexHome, "auth.json");
|
|
96
|
+
if ((await isApiKey(theirs)) === false) {
|
|
97
|
+
await copyFile(theirs, auth);
|
|
98
|
+
await chmod(auth, 0o600);
|
|
99
|
+
return "copied";
|
|
100
|
+
}
|
|
101
|
+
return "none";
|
|
102
|
+
}
|
package/dist/manage.js
CHANGED
|
@@ -59,6 +59,7 @@ export function parseFlags(argv) {
|
|
|
59
59
|
}
|
|
60
60
|
return flags;
|
|
61
61
|
}
|
|
62
|
+
const mode = (t) => (t.authMode === "subscription" ? `${yellow("plan")}${t.subscriptionPlan ? dim(` ${t.subscriptionPlan}`) : ""}` : "meter");
|
|
62
63
|
async function tabsOf(call) {
|
|
63
64
|
return (await call("GET", "/api/cli/tabs")).tabs;
|
|
64
65
|
}
|
|
@@ -92,7 +93,7 @@ export async function list(config, flags) {
|
|
|
92
93
|
const tabs = await tabsOf(api(config));
|
|
93
94
|
emit(flags.json, { tabs }, () => tabs.length === 0
|
|
94
95
|
? dim("No Agents yet. Run tab claude or tab codex in a project folder.")
|
|
95
|
-
: table(["agent", "state", "spent", "cap", "used", "window", "project"], tabs.map((t) => [t.name === t.slug ? t.slug : `${t.name} ${dim(t.slug)}`, state(t.state), money(t.spentCents), money(t.capCents), pct(t.spentCents, t.capCents), t.window, t.projectName ?? dim("-")]), { right: [
|
|
96
|
+
: table(["agent", "state", "mode", "spent", "cap", "used", "window", "project"], tabs.map((t) => [t.name === t.slug ? t.slug : `${t.name} ${dim(t.slug)}`, state(t.state), mode(t), money(t.spentCents), money(t.capCents), pct(t.spentCents, t.capCents), t.window, t.projectName ?? dim("-")]), { right: [3, 4, 5] }));
|
|
96
97
|
return 0;
|
|
97
98
|
}
|
|
98
99
|
export async function setState(config, flags, next) {
|
|
@@ -139,6 +140,49 @@ export async function archive(config, flags, ask) {
|
|
|
139
140
|
emit(flags.json, { tab, archived: true }, () => `${bold(tab.name)} archived.`);
|
|
140
141
|
return 0;
|
|
141
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* `tab use --subscription [--plan "Claude Max 5x"] [--price 100]` and
|
|
145
|
+
* `tab use --api-key`: which login the Agent runs on. On a subscription the
|
|
146
|
+
* vendor's plan pays; the tab still gates every call and records it at list
|
|
147
|
+
* price as a shadow row, and the seat's monthly price sits beside the meter
|
|
148
|
+
* in the project rollup.
|
|
149
|
+
*/
|
|
150
|
+
export async function setAuthMode(config, flags, given, authMode) {
|
|
151
|
+
const call = api(config);
|
|
152
|
+
const slug = await resolveSlug(call, given);
|
|
153
|
+
const body = { authMode };
|
|
154
|
+
if (flags.opts.plan !== undefined)
|
|
155
|
+
body.subscriptionPlan = flags.opts.plan;
|
|
156
|
+
if (flags.opts.price !== undefined)
|
|
157
|
+
body.subscriptionMonthlyDollars = flags.opts.price.replace(/^\$/, "");
|
|
158
|
+
const { tab } = await call("PATCH", `/api/cli/tabs/${encodeURIComponent(slug)}`, body);
|
|
159
|
+
emit(flags.json, { tab }, () => authMode === "subscription"
|
|
160
|
+
? `${bold(tab.name)} runs on a subscription${tab.subscriptionPlan ? ` (${tab.subscriptionPlan}${tab.subscriptionMonthlyCents ? `, ${money(tab.subscriptionMonthlyCents)}/mo` : ""})` : ""}.\n` +
|
|
161
|
+
`${dim("tab claude uses your own Claude login, tab codex your ChatGPT login. The kill switch and policies still apply; calls land in the ledger at list price, never on the tab's spend.")}`
|
|
162
|
+
: `${bold(tab.name)} is back on the metered tab: the flock's provider key, charged to the cap.`);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
/** `tab quota [agent]`: what the vendor said the plan has left, on the last call. */
|
|
166
|
+
export async function quota(config, flags) {
|
|
167
|
+
const call = api(config);
|
|
168
|
+
const slug = await resolveSlug(call, flags.args[0]);
|
|
169
|
+
const { tab, quota: windows } = await call("GET", `/api/cli/tabs/${encodeURIComponent(slug)}/quota`);
|
|
170
|
+
emit(flags.json, { tab, quota: windows }, () => {
|
|
171
|
+
if (tab.authMode !== "subscription")
|
|
172
|
+
return `${bold(tab.name)} is on the metered tab; ${dim("tab use --subscription")} puts it on a plan.`;
|
|
173
|
+
if (windows.length === 0)
|
|
174
|
+
return `${bold(tab.name)}: no quota reported yet. It shows after the first call through tab claude or tab codex.`;
|
|
175
|
+
const left = (w) => {
|
|
176
|
+
const pctLeft = Math.max(0, 100 - w.usedPct);
|
|
177
|
+
const paint = pctLeft <= 10 ? red : pctLeft <= 30 ? yellow : green;
|
|
178
|
+
return paint(`${pctLeft}% left`);
|
|
179
|
+
};
|
|
180
|
+
const when = (iso) => (iso ? new Date(iso).toLocaleString() : dim("-"));
|
|
181
|
+
return (`${bold(tab.name)}${tab.subscriptionPlan ? dim(` ${tab.subscriptionPlan}`) : ""}\n` +
|
|
182
|
+
table(["provider", "window", "used", "left", "resets", "seen"], windows.map((w) => [w.provider, w.window, `${w.usedPct}%`, left(w), when(w.resetsAt), when(w.observedAt)]), { right: [2] }));
|
|
183
|
+
});
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
142
186
|
function listOpt(raw) {
|
|
143
187
|
if (raw === undefined)
|
|
144
188
|
return undefined;
|
package/dist/proxy-bin.js
CHANGED
|
@@ -13,7 +13,7 @@ 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.3";
|
|
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")
|
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.6";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanamorilabs/tab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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.3",
|
|
26
|
+
"@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.3",
|
|
27
|
+
"@hanamorilabs/flocktab-proxy-linux-x64": "0.1.3",
|
|
28
|
+
"@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.3",
|
|
29
|
+
"@hanamorilabs/flocktab-proxy-win-x64": "0.1.3"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.18.6",
|