@hanamorilabs/tab 0.1.5 → 0.1.7
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 +28 -1
- package/dist/cli.js +33 -3
- package/dist/clients.js +25 -1
- package/dist/codex-home.js +57 -4
- package/dist/manage.js +60 -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,29 @@ 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 subscribe claude --plan "Claude Max 5x" --price 100 # Claude Code on your Max plan
|
|
45
|
+
tab subscribe codex # Codex on your ChatGPT plan
|
|
46
|
+
tab claude # your own Claude login through the tab
|
|
47
|
+
tab codex # your own ChatGPT login (codex login once, inside tab codex)
|
|
48
|
+
tab grok # still metered: the flock's xAI key, charged to the cap
|
|
49
|
+
tab quota # 5h / 7d windows the vendors reported on the last call
|
|
50
|
+
tab unsubscribe codex # Codex back on the meter
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A subscription belongs to the harness, not the Agent: Claude Code can be on
|
|
54
|
+
Max while Codex and Grok on the same Agent stay on the meter. The plan pays
|
|
55
|
+
for the tokens; the tab still gates every call (kill switch, velocity, model
|
|
56
|
+
and tool policy) and records it at list price as a `SUBSCRIPTION` row that
|
|
57
|
+
never touches the cap. Claude Code gets
|
|
58
|
+
`ANTHROPIC_BASE_URL=<proxy>/t/<key>/anthropic` and no API key; Codex gets a
|
|
59
|
+
home whose `chatgpt_base_url` is `<proxy>/t/<key>/openai/backend-api/`, with
|
|
60
|
+
your existing `~/.codex/auth.json` copied in once (never written back). The
|
|
61
|
+
rows are read on every launch, so the console's **Subscriptions** panel and
|
|
62
|
+
`tab subscribe` agree. `docs/subscriptions.md` has the wire.
|
|
63
|
+
|
|
41
64
|
## Self-hosted
|
|
42
65
|
|
|
43
66
|
The proxy runs on your box and holds the provider keys; the ledger, tabs and
|
|
@@ -59,6 +82,9 @@ Codex shows `FlockTab - Self-hosted` as its provider.
|
|
|
59
82
|
tab login approve this machine in the console, once
|
|
60
83
|
tab <agent> [args] claude, codex, grok, kimi, gemini, or any command, on the tab
|
|
61
84
|
tab use pick or change the Agent this folder runs as (.flocktab)
|
|
85
|
+
tab subscribe claude|codex [agent] that harness on your own plan (--plan, --price)
|
|
86
|
+
tab unsubscribe claude|codex [agent] back on the metered tab for that harness
|
|
87
|
+
tab quota [agent] what the plans have left: 5h and 7d windows
|
|
62
88
|
tab alias setup <name>... make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
|
|
63
89
|
tab alias remove <name>... undo that; `tab alias list` shows them
|
|
64
90
|
tab status flock, folder Agent, proxy health, provider key state
|
|
@@ -76,7 +102,7 @@ session from `tab login`. An Agent is named by slug or name; left out, it is
|
|
|
76
102
|
this folder's Agent. Every read takes `--json`.
|
|
77
103
|
|
|
78
104
|
```
|
|
79
|
-
tab list every Agent: state, spent of cap, window, project
|
|
105
|
+
tab list every Agent: state, spent of cap, window, plans (claude, codex), project
|
|
80
106
|
tab close my-project tab open my-project the kill switch (402 tab_closed on the next call)
|
|
81
107
|
tab cap my-project 25 --window week cap in dollars, and the window
|
|
82
108
|
tab rename my-project "My project (nightly)" the slug stays
|
|
@@ -121,5 +147,6 @@ proxy http://127.0.0.1:8787 (self-hosted) up
|
|
|
121
147
|
flock Hanamori Labs
|
|
122
148
|
folder flocktab-codex
|
|
123
149
|
plan team
|
|
150
|
+
plans none; every harness on the metered tab
|
|
124
151
|
byok provider keys on this box
|
|
125
152
|
```
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
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 subscribe claude this folder's Agent runs Claude Code on your Claude Max plan
|
|
11
|
+
* tab subscribe codex ...and Codex on your ChatGPT plan; tab unsubscribe undoes it
|
|
12
|
+
* tab quota what the plans have left (5h / 7d windows) for this Agent
|
|
10
13
|
* tab alias setup codex make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
|
|
11
14
|
* tab alias remove codex undo that; `tab alias list` shows them
|
|
12
15
|
* tab status which flock, which Agent here, is the proxy up
|
|
@@ -25,7 +28,7 @@ import { hostname } from "node:os";
|
|
|
25
28
|
import { createInterface } from "node:readline/promises";
|
|
26
29
|
import { stdin, stdout } from "node:process";
|
|
27
30
|
import { bold, box, cyan, dim, green, heading, line, rows, underline, yellow } from "./ui.js";
|
|
28
|
-
import { envFor, knownClients } from "./clients.js";
|
|
31
|
+
import { clientFor, envFor, knownClients } from "./clients.js";
|
|
29
32
|
import { prepareCodexHome } from "./codex-home.js";
|
|
30
33
|
import { ConsoleApiError, createAgent, issueAgentKey, listAgents } from "./console-api.js";
|
|
31
34
|
import { consoleUrlFor, DeviceLoginError, startDeviceLogin, waitForApproval } from "./device-login.js";
|
|
@@ -53,6 +56,9 @@ function usage() {
|
|
|
53
56
|
cmd(others.join(" | tab "), "the same, for those agents"),
|
|
54
57
|
cmd("<command> [args]", "any other agent, both APIs pointed at the tab"),
|
|
55
58
|
cmd("use", "pick or change the Agent this folder runs as (.flocktab)"),
|
|
59
|
+
cmd("subscribe claude|codex", "run that harness on your own plan (--plan \"Max 5x\" --price 100)"),
|
|
60
|
+
cmd("unsubscribe claude|codex", "back on the metered tab for that harness"),
|
|
61
|
+
cmd("quota [agent]", "what the plans have left: 5h and 7d windows"),
|
|
56
62
|
cmd("alias setup <name>...", "make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)"),
|
|
57
63
|
cmd("alias remove <name>...", "undo that; `tab alias list` shows them"),
|
|
58
64
|
cmd("status", "which flock, which Agent here, is the proxy up"),
|
|
@@ -480,6 +486,8 @@ async function status() {
|
|
|
480
486
|
}
|
|
481
487
|
else {
|
|
482
488
|
pairs.push(["plan", me.flock.plan]);
|
|
489
|
+
const plans = (me.subscriptions ?? []).map((p) => (p === "anthropic" ? "claude" : p === "openai" ? "codex" : p));
|
|
490
|
+
pairs.push(["plans", plans.length > 0 ? `${bold(plans.join(", "))} ${dim("on your own login; everything else metered")}` : dim("none; every harness on the metered tab")]);
|
|
483
491
|
const byok = !me.byok
|
|
484
492
|
? dim("no provider key on this flock")
|
|
485
493
|
: me.unlock === "none"
|
|
@@ -506,9 +514,21 @@ async function runAgent(name, args) {
|
|
|
506
514
|
const agent = await resolveAgent(config);
|
|
507
515
|
if (!agent)
|
|
508
516
|
return 1;
|
|
509
|
-
|
|
517
|
+
// Which harness is this, and is it on the person's own plan? The
|
|
518
|
+
// subscription belongs to the harness (Claude Code: anthropic, Codex:
|
|
519
|
+
// openai), read fresh on every launch so `tab subscribe` or the console
|
|
520
|
+
// applies to the next run. Any other harness is metered.
|
|
521
|
+
const base = clientFor(name);
|
|
522
|
+
const vendor = base.shape === "anthropic" ? "anthropic" : base.isolatedHome === "codex" ? "openai" : undefined;
|
|
523
|
+
const me = await whoami(config.proxyUrl, { key: agent.key, unlock: config.unlock });
|
|
524
|
+
const auth = vendor && !("error" in me) && (me.subscriptions ?? []).includes(vendor) ? "subscription" : "key";
|
|
525
|
+
// On a subscription there is no provider key to unlock; the bare key rides in the URL.
|
|
526
|
+
const key = auth === "subscription" ? agent.key : presentedKey({ key: agent.key, unlock: config.unlock });
|
|
510
527
|
reportFolder(config.proxyUrl, key);
|
|
511
|
-
const { spec, env } = envFor({ name, proxyUrl: config.proxyUrl, presentedKey: key });
|
|
528
|
+
const { spec, env } = envFor({ name, proxyUrl: config.proxyUrl, presentedKey: key, auth });
|
|
529
|
+
if (auth === "subscription") {
|
|
530
|
+
say(dim(`${spec.label} runs on your own ${vendor === "openai" ? "ChatGPT" : "Claude"} login; FlockTab keeps the record and the kill switch.`));
|
|
531
|
+
}
|
|
512
532
|
// An alias shim named like the agent must not be what we spawn.
|
|
513
533
|
env.PATH = pathWithoutShims();
|
|
514
534
|
if (spec.isolatedHome === "codex") {
|
|
@@ -517,6 +537,7 @@ async function runAgent(name, args) {
|
|
|
517
537
|
proxyUrl: config.proxyUrl,
|
|
518
538
|
presentedKey: key,
|
|
519
539
|
mode: config.mode,
|
|
540
|
+
auth,
|
|
520
541
|
});
|
|
521
542
|
}
|
|
522
543
|
const child = spawn(spec.command, [...(spec.args ?? []), ...args], { stdio: "inherit", env });
|
|
@@ -720,6 +741,12 @@ async function managed(command, rest) {
|
|
|
720
741
|
return await manage.outside(config, flags);
|
|
721
742
|
case "live":
|
|
722
743
|
return await manage.live(config, flags);
|
|
744
|
+
case "quota":
|
|
745
|
+
return await manage.quota(config, flags);
|
|
746
|
+
case "subscribe":
|
|
747
|
+
return await manage.subscribe(config, flags, true);
|
|
748
|
+
case "unsubscribe":
|
|
749
|
+
return await manage.subscribe(config, flags, false);
|
|
723
750
|
default:
|
|
724
751
|
return await manage.web(config, flags);
|
|
725
752
|
}
|
|
@@ -769,6 +796,9 @@ async function main(argv) {
|
|
|
769
796
|
case "spend":
|
|
770
797
|
case "outside":
|
|
771
798
|
case "live":
|
|
799
|
+
case "quota":
|
|
800
|
+
case "subscribe":
|
|
801
|
+
case "unsubscribe":
|
|
772
802
|
case "web":
|
|
773
803
|
return managed(command, rest);
|
|
774
804
|
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,12 @@ export function parseFlags(argv) {
|
|
|
59
59
|
}
|
|
60
60
|
return flags;
|
|
61
61
|
}
|
|
62
|
+
/** `anthropic` is Claude Code, `openai` is Codex: say the harness, not the vendor. */
|
|
63
|
+
const HARNESS = { anthropic: "claude", openai: "codex" };
|
|
64
|
+
const plans = (t) => {
|
|
65
|
+
const subs = t.subscriptions ?? [];
|
|
66
|
+
return subs.length === 0 ? dim("-") : subs.map((s) => `${yellow(HARNESS[s.provider] ?? s.provider)}${s.plan ? dim(` ${s.plan}`) : ""}`).join(", ");
|
|
67
|
+
};
|
|
62
68
|
async function tabsOf(call) {
|
|
63
69
|
return (await call("GET", "/api/cli/tabs")).tabs;
|
|
64
70
|
}
|
|
@@ -92,7 +98,7 @@ export async function list(config, flags) {
|
|
|
92
98
|
const tabs = await tabsOf(api(config));
|
|
93
99
|
emit(flags.json, { tabs }, () => tabs.length === 0
|
|
94
100
|
? 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: [2, 3, 4] }));
|
|
101
|
+
: table(["agent", "state", "spent", "cap", "used", "window", "plans", "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, plans(t), t.projectName ?? dim("-")]), { right: [2, 3, 4] }));
|
|
96
102
|
return 0;
|
|
97
103
|
}
|
|
98
104
|
export async function setState(config, flags, next) {
|
|
@@ -139,6 +145,59 @@ export async function archive(config, flags, ask) {
|
|
|
139
145
|
emit(flags.json, { tab, archived: true }, () => `${bold(tab.name)} archived.`);
|
|
140
146
|
return 0;
|
|
141
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* `tab subscribe claude [--plan "Claude Max 5x"] [--price 100]` and
|
|
150
|
+
* `tab unsubscribe codex`: which harness of an Agent runs on the person's
|
|
151
|
+
* own plan. The subscription belongs to the harness, so Claude Code can be
|
|
152
|
+
* on Max while Codex and Grok on the same Agent stay on the meter. On a
|
|
153
|
+
* plan the vendor pays; the tab still gates every call and records it at
|
|
154
|
+
* list price as a shadow row, and the seat's monthly price sits beside the
|
|
155
|
+
* meter in the project rollup.
|
|
156
|
+
*/
|
|
157
|
+
export async function subscribe(config, flags, on) {
|
|
158
|
+
const call = api(config);
|
|
159
|
+
const verb = on ? "subscribe" : "unsubscribe";
|
|
160
|
+
const [harness, given] = flags.args;
|
|
161
|
+
if (!harness)
|
|
162
|
+
throw new ManageError(`tab ${verb} claude|codex [agent]${on ? ' [--plan "Claude Max 5x"] [--price 100]' : ""}`);
|
|
163
|
+
const slug = await resolveSlug(call, given);
|
|
164
|
+
const path = `/api/cli/tabs/${encodeURIComponent(slug)}/subscriptions/${encodeURIComponent(harness.toLowerCase())}`;
|
|
165
|
+
const body = {};
|
|
166
|
+
if (flags.opts.plan !== undefined)
|
|
167
|
+
body.plan = flags.opts.plan;
|
|
168
|
+
if (flags.opts.price !== undefined)
|
|
169
|
+
body.monthlyDollars = flags.opts.price.replace(/^\$/, "");
|
|
170
|
+
const { tab } = await call(on ? "PUT" : "DELETE", path, on ? body : undefined);
|
|
171
|
+
const provider = harness.toLowerCase() === "codex" || harness.toLowerCase() === "chatgpt" || harness.toLowerCase() === "openai" ? "openai" : "anthropic";
|
|
172
|
+
const seat = (tab.subscriptions ?? []).find((s) => s.provider === provider);
|
|
173
|
+
const label = HARNESS[provider] ?? provider;
|
|
174
|
+
emit(flags.json, { tab }, () => on
|
|
175
|
+
? `${bold(tab.name)}: ${bold(label)} runs on your own ${provider === "openai" ? "ChatGPT" : "Claude"} login${seat?.plan ? ` (${seat.plan}${seat.monthlyCents && seat.monthlyCents !== "0" ? `, ${money(seat.monthlyCents)}/mo` : ""})` : ""}.\n` +
|
|
176
|
+
`${dim(`tab ${label} uses that login from the next run. The kill switch and policies still apply; calls land in the ledger at list price, never on the tab's spend. Other harnesses stay metered.`)}`
|
|
177
|
+
: `${bold(tab.name)}: ${bold(label)} is back on the metered tab: the flock's provider key, charged to the cap.`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
/** `tab quota [agent]`: what the vendor said the plan has left, on the last call. */
|
|
181
|
+
export async function quota(config, flags) {
|
|
182
|
+
const call = api(config);
|
|
183
|
+
const slug = await resolveSlug(call, flags.args[0]);
|
|
184
|
+
const { tab, quota: windows } = await call("GET", `/api/cli/tabs/${encodeURIComponent(slug)}/quota`);
|
|
185
|
+
emit(flags.json, { tab, quota: windows }, () => {
|
|
186
|
+
if ((tab.subscriptions ?? []).length === 0)
|
|
187
|
+
return `${bold(tab.name)} has no harness on a plan; ${dim("tab subscribe claude")} or ${dim("tab subscribe codex")} puts one there.`;
|
|
188
|
+
if (windows.length === 0)
|
|
189
|
+
return `${bold(tab.name)}: no quota reported yet. It shows after the first call through tab claude or tab codex.`;
|
|
190
|
+
const left = (w) => {
|
|
191
|
+
const pctLeft = Math.max(0, 100 - w.usedPct);
|
|
192
|
+
const paint = pctLeft <= 10 ? red : pctLeft <= 30 ? yellow : green;
|
|
193
|
+
return paint(`${pctLeft}% left`);
|
|
194
|
+
};
|
|
195
|
+
const when = (iso) => (iso ? new Date(iso).toLocaleString() : dim("-"));
|
|
196
|
+
return (`${bold(tab.name)} ${plans(tab)}\n` +
|
|
197
|
+
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] }));
|
|
198
|
+
});
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
142
201
|
function listOpt(raw) {
|
|
143
202
|
if (raw === undefined)
|
|
144
203
|
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.4";
|
|
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.7";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanamorilabs/tab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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.4",
|
|
26
|
+
"@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.4",
|
|
27
|
+
"@hanamorilabs/flocktab-proxy-linux-x64": "0.1.4",
|
|
28
|
+
"@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.4",
|
|
29
|
+
"@hanamorilabs/flocktab-proxy-win-x64": "0.1.4"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.18.6",
|