@hanamorilabs/tab 0.1.6 → 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 +17 -12
- package/dist/cli.js +28 -44
- package/dist/manage.js +34 -19
- package/dist/proxy-bin.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -41,21 +41,25 @@ you would rather not have a file or a folder, for example in CI.
|
|
|
41
41
|
## On a subscription (Claude Max, ChatGPT)
|
|
42
42
|
|
|
43
43
|
```
|
|
44
|
-
tab
|
|
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
|
|
45
46
|
tab claude # your own Claude login through the tab
|
|
46
47
|
tab codex # your own ChatGPT login (codex login once, inside tab codex)
|
|
47
|
-
tab
|
|
48
|
-
tab
|
|
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
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
54
58
|
`ANTHROPIC_BASE_URL=<proxy>/t/<key>/anthropic` and no API key; Codex gets a
|
|
55
59
|
home whose `chatgpt_base_url` is `<proxy>/t/<key>/openai/backend-api/`, with
|
|
56
60
|
your existing `~/.codex/auth.json` copied in once (never written back). The
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
rows are read on every launch, so the console's **Subscriptions** panel and
|
|
62
|
+
`tab subscribe` agree. `docs/subscriptions.md` has the wire.
|
|
59
63
|
|
|
60
64
|
## Self-hosted
|
|
61
65
|
|
|
@@ -78,8 +82,9 @@ Codex shows `FlockTab - Self-hosted` as its provider.
|
|
|
78
82
|
tab login approve this machine in the console, once
|
|
79
83
|
tab <agent> [args] claude, codex, grok, kimi, gemini, or any command, on the tab
|
|
80
84
|
tab use pick or change the Agent this folder runs as (.flocktab)
|
|
81
|
-
tab
|
|
82
|
-
tab
|
|
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
|
|
83
88
|
tab alias setup <name>... make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
|
|
84
89
|
tab alias remove <name>... undo that; `tab alias list` shows them
|
|
85
90
|
tab status flock, folder Agent, proxy health, provider key state
|
|
@@ -97,7 +102,7 @@ session from `tab login`. An Agent is named by slug or name; left out, it is
|
|
|
97
102
|
this folder's Agent. Every read takes `--json`.
|
|
98
103
|
|
|
99
104
|
```
|
|
100
|
-
tab list every Agent: state,
|
|
105
|
+
tab list every Agent: state, spent of cap, window, plans (claude, codex), project
|
|
101
106
|
tab close my-project tab open my-project the kill switch (402 tab_closed on the next call)
|
|
102
107
|
tab cap my-project 25 --window week cap in dollars, and the window
|
|
103
108
|
tab rename my-project "My project (nightly)" the slug stays
|
|
@@ -142,6 +147,6 @@ proxy http://127.0.0.1:8787 (self-hosted) up
|
|
|
142
147
|
flock Hanamori Labs
|
|
143
148
|
folder flocktab-codex
|
|
144
149
|
plan team
|
|
145
|
-
|
|
150
|
+
plans none; every harness on the metered tab
|
|
146
151
|
byok provider keys on this box
|
|
147
152
|
```
|
package/dist/cli.js
CHANGED
|
@@ -7,8 +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
|
|
11
|
-
* tab
|
|
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
|
|
12
13
|
* tab alias setup codex make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
|
|
13
14
|
* tab alias remove codex undo that; `tab alias list` shows them
|
|
14
15
|
* tab status which flock, which Agent here, is the proxy up
|
|
@@ -27,7 +28,7 @@ import { hostname } from "node:os";
|
|
|
27
28
|
import { createInterface } from "node:readline/promises";
|
|
28
29
|
import { stdin, stdout } from "node:process";
|
|
29
30
|
import { bold, box, cyan, dim, green, heading, line, rows, underline, yellow } from "./ui.js";
|
|
30
|
-
import { envFor, knownClients } from "./clients.js";
|
|
31
|
+
import { clientFor, envFor, knownClients } from "./clients.js";
|
|
31
32
|
import { prepareCodexHome } from "./codex-home.js";
|
|
32
33
|
import { ConsoleApiError, createAgent, issueAgentKey, listAgents } from "./console-api.js";
|
|
33
34
|
import { consoleUrlFor, DeviceLoginError, startDeviceLogin, waitForApproval } from "./device-login.js";
|
|
@@ -55,9 +56,9 @@ function usage() {
|
|
|
55
56
|
cmd(others.join(" | tab "), "the same, for those agents"),
|
|
56
57
|
cmd("<command> [args]", "any other agent, both APIs pointed at the tab"),
|
|
57
58
|
cmd("use", "pick or change the Agent this folder runs as (.flocktab)"),
|
|
58
|
-
cmd("
|
|
59
|
-
cmd("
|
|
60
|
-
cmd("quota [agent]", "what the
|
|
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"),
|
|
61
62
|
cmd("alias setup <name>...", "make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)"),
|
|
62
63
|
cmd("alias remove <name>...", "undo that; `tab alias list` shows them"),
|
|
63
64
|
cmd("status", "which flock, which Agent here, is the proxy up"),
|
|
@@ -485,7 +486,8 @@ async function status() {
|
|
|
485
486
|
}
|
|
486
487
|
else {
|
|
487
488
|
pairs.push(["plan", me.flock.plan]);
|
|
488
|
-
|
|
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")]);
|
|
489
491
|
const byok = !me.byok
|
|
490
492
|
? dim("no provider key on this flock")
|
|
491
493
|
: me.unlock === "none"
|
|
@@ -512,20 +514,20 @@ async function runAgent(name, args) {
|
|
|
512
514
|
const agent = await resolveAgent(config);
|
|
513
515
|
if (!agent)
|
|
514
516
|
return 1;
|
|
515
|
-
//
|
|
516
|
-
//
|
|
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;
|
|
517
523
|
const me = await whoami(config.proxyUrl, { key: agent.key, unlock: config.unlock });
|
|
518
|
-
const auth = !("error" in me) && me.
|
|
524
|
+
const auth = vendor && !("error" in me) && (me.subscriptions ?? []).includes(vendor) ? "subscription" : "key";
|
|
519
525
|
// On a subscription there is no provider key to unlock; the bare key rides in the URL.
|
|
520
526
|
const key = auth === "subscription" ? agent.key : presentedKey({ key: agent.key, unlock: config.unlock });
|
|
521
527
|
reportFolder(config.proxyUrl, key);
|
|
522
528
|
const { spec, env } = envFor({ name, proxyUrl: config.proxyUrl, presentedKey: key, auth });
|
|
523
529
|
if (auth === "subscription") {
|
|
524
|
-
|
|
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.`));
|
|
530
|
+
say(dim(`${spec.label} runs on your own ${vendor === "openai" ? "ChatGPT" : "Claude"} login; FlockTab keeps the record and the kill switch.`));
|
|
529
531
|
}
|
|
530
532
|
// An alias shim named like the agent must not be what we spawn.
|
|
531
533
|
env.PATH = pathWithoutShims();
|
|
@@ -607,39 +609,15 @@ function pathHint() {
|
|
|
607
609
|
say(` ${bold(pathLine())}`);
|
|
608
610
|
return 0;
|
|
609
611
|
}
|
|
610
|
-
/**
|
|
611
|
-
|
|
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) {
|
|
612
|
+
/** `tab use`: pick or change the Agent for this folder. */
|
|
613
|
+
async function use() {
|
|
616
614
|
const config = await ensureLogin();
|
|
617
615
|
if (!config)
|
|
618
616
|
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
|
-
}
|
|
626
617
|
if (!(await ensureProxy(config)))
|
|
627
618
|
return 1;
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
}
|
|
619
|
+
const agent = await resolveAgent(config, { pick: true });
|
|
620
|
+
return agent ? 0 : 1;
|
|
643
621
|
}
|
|
644
622
|
/**
|
|
645
623
|
* First `tab up` on a box: the provider keys the proxy will spend, typed
|
|
@@ -765,6 +743,10 @@ async function managed(command, rest) {
|
|
|
765
743
|
return await manage.live(config, flags);
|
|
766
744
|
case "quota":
|
|
767
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);
|
|
768
750
|
default:
|
|
769
751
|
return await manage.web(config, flags);
|
|
770
752
|
}
|
|
@@ -790,7 +772,7 @@ async function main(argv) {
|
|
|
790
772
|
ok("Forgot the session and keys on this machine.");
|
|
791
773
|
return 0;
|
|
792
774
|
case "use":
|
|
793
|
-
return use(
|
|
775
|
+
return use();
|
|
794
776
|
case "alias":
|
|
795
777
|
return alias(rest);
|
|
796
778
|
case "unalias":
|
|
@@ -815,6 +797,8 @@ async function main(argv) {
|
|
|
815
797
|
case "outside":
|
|
816
798
|
case "live":
|
|
817
799
|
case "quota":
|
|
800
|
+
case "subscribe":
|
|
801
|
+
case "unsubscribe":
|
|
818
802
|
case "web":
|
|
819
803
|
return managed(command, rest);
|
|
820
804
|
case "status":
|
package/dist/manage.js
CHANGED
|
@@ -59,7 +59,12 @@ export function parseFlags(argv) {
|
|
|
59
59
|
}
|
|
60
60
|
return flags;
|
|
61
61
|
}
|
|
62
|
-
|
|
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
|
+
};
|
|
63
68
|
async function tabsOf(call) {
|
|
64
69
|
return (await call("GET", "/api/cli/tabs")).tabs;
|
|
65
70
|
}
|
|
@@ -93,7 +98,7 @@ export async function list(config, flags) {
|
|
|
93
98
|
const tabs = await tabsOf(api(config));
|
|
94
99
|
emit(flags.json, { tabs }, () => tabs.length === 0
|
|
95
100
|
? dim("No Agents yet. Run tab claude or tab codex in a project folder.")
|
|
96
|
-
: table(["agent", "state", "
|
|
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] }));
|
|
97
102
|
return 0;
|
|
98
103
|
}
|
|
99
104
|
export async function setState(config, flags, next) {
|
|
@@ -141,25 +146,35 @@ export async function archive(config, flags, ask) {
|
|
|
141
146
|
return 0;
|
|
142
147
|
}
|
|
143
148
|
/**
|
|
144
|
-
* `tab
|
|
145
|
-
* `tab
|
|
146
|
-
*
|
|
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.
|
|
149
156
|
*/
|
|
150
|
-
export async function
|
|
157
|
+
export async function subscribe(config, flags, on) {
|
|
151
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]' : ""}`);
|
|
152
163
|
const slug = await resolveSlug(call, given);
|
|
153
|
-
const
|
|
164
|
+
const path = `/api/cli/tabs/${encodeURIComponent(slug)}/subscriptions/${encodeURIComponent(harness.toLowerCase())}`;
|
|
165
|
+
const body = {};
|
|
154
166
|
if (flags.opts.plan !== undefined)
|
|
155
|
-
body.
|
|
167
|
+
body.plan = flags.opts.plan;
|
|
156
168
|
if (flags.opts.price !== undefined)
|
|
157
|
-
body.
|
|
158
|
-
const { tab } = await call("
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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.`);
|
|
163
178
|
return 0;
|
|
164
179
|
}
|
|
165
180
|
/** `tab quota [agent]`: what the vendor said the plan has left, on the last call. */
|
|
@@ -168,8 +183,8 @@ export async function quota(config, flags) {
|
|
|
168
183
|
const slug = await resolveSlug(call, flags.args[0]);
|
|
169
184
|
const { tab, quota: windows } = await call("GET", `/api/cli/tabs/${encodeURIComponent(slug)}/quota`);
|
|
170
185
|
emit(flags.json, { tab, quota: windows }, () => {
|
|
171
|
-
if (tab.
|
|
172
|
-
return `${bold(tab.name)}
|
|
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.`;
|
|
173
188
|
if (windows.length === 0)
|
|
174
189
|
return `${bold(tab.name)}: no quota reported yet. It shows after the first call through tab claude or tab codex.`;
|
|
175
190
|
const left = (w) => {
|
|
@@ -178,7 +193,7 @@ export async function quota(config, flags) {
|
|
|
178
193
|
return paint(`${pctLeft}% left`);
|
|
179
194
|
};
|
|
180
195
|
const when = (iso) => (iso ? new Date(iso).toLocaleString() : dim("-"));
|
|
181
|
-
return (`${bold(tab.name)}${
|
|
196
|
+
return (`${bold(tab.name)} ${plans(tab)}\n` +
|
|
182
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] }));
|
|
183
198
|
});
|
|
184
199
|
return 0;
|
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",
|