@hanamorilabs/tab 0.1.7 → 0.1.8
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 +29 -23
- package/dist/cli.js +37 -28
- package/dist/clients.js +24 -2
- package/dist/logins.js +77 -0
- package/dist/manage.js +58 -48
- package/dist/proxy-bin.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -38,28 +38,34 @@ 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)
|
|
41
|
+
## On a subscription (Claude Max, ChatGPT, SuperGrok, Kimi)
|
|
42
|
+
|
|
43
|
+
An Agent is **api** (the flock's provider key, metered) or **subscription**
|
|
44
|
+
(the harnesses' own logins), chosen when it is made: the first `tab claude`
|
|
45
|
+
in a folder asks, or
|
|
42
46
|
|
|
43
47
|
```
|
|
44
|
-
tab
|
|
45
|
-
tab
|
|
46
|
-
tab claude
|
|
47
|
-
tab codex
|
|
48
|
-
tab grok
|
|
49
|
-
tab
|
|
50
|
-
tab
|
|
48
|
+
tab agent create "me@joseairosa.com" --subscription
|
|
49
|
+
tab use # this folder runs as it
|
|
50
|
+
tab claude # your own Claude login through the tab
|
|
51
|
+
tab codex # your own ChatGPT login (codex login once, inside tab codex)
|
|
52
|
+
tab grok # your SuperGrok login
|
|
53
|
+
tab kimi # your Kimi Code login
|
|
54
|
+
tab accounts # accounts seen: plan, seat price, 30 days at list, what is left
|
|
55
|
+
tab agent kind me@joseairosa.com api # back to the meter
|
|
51
56
|
```
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
The plan pays for the tokens; the tab still gates every call (kill switch,
|
|
59
|
+
velocity, model and tool policy) and records it at list price as a
|
|
60
|
+
`SUBSCRIPTION` row that never touches the cap. The account is not declared:
|
|
61
|
+
the vendor names it on every reply (Anthropic's organization id, ChatGPT's
|
|
62
|
+
account id, the login token for xAI and Kimi), FlockTab files the call under
|
|
63
|
+
it, learns the email and plan tier, and prices the seat from the tier. Four
|
|
64
|
+
Max accounts are four accounts; `claude /login` to another one mid-session
|
|
65
|
+
and the next call lands on it. Claude Code gets
|
|
66
|
+
`ANTHROPIC_BASE_URL=<proxy>/t/<key>/anthropic` and no API key; Codex a home
|
|
67
|
+
whose `chatgpt_base_url` is the passthrough; Grok Build `GROK_XAI_API_BASE_URL`;
|
|
68
|
+
Kimi Code `KIMI_BASE_URL`. `docs/subscriptions.md` has the wire.
|
|
63
69
|
|
|
64
70
|
## Self-hosted
|
|
65
71
|
|
|
@@ -82,9 +88,9 @@ Codex shows `FlockTab - Self-hosted` as its provider.
|
|
|
82
88
|
tab login approve this machine in the console, once
|
|
83
89
|
tab <agent> [args] claude, codex, grok, kimi, gemini, or any command, on the tab
|
|
84
90
|
tab use pick or change the Agent this folder runs as (.flocktab)
|
|
85
|
-
tab
|
|
86
|
-
tab
|
|
87
|
-
tab
|
|
91
|
+
tab agent create <name> --subscription|--api a new Agent of that kind (--cap 50)
|
|
92
|
+
tab agent kind <agent> api|subscription change it
|
|
93
|
+
tab accounts accounts your subscription Agents were seen on: plan, price, quota left
|
|
88
94
|
tab alias setup <name>... make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
|
|
89
95
|
tab alias remove <name>... undo that; `tab alias list` shows them
|
|
90
96
|
tab status flock, folder Agent, proxy health, provider key state
|
|
@@ -102,7 +108,7 @@ session from `tab login`. An Agent is named by slug or name; left out, it is
|
|
|
102
108
|
this folder's Agent. Every read takes `--json`.
|
|
103
109
|
|
|
104
110
|
```
|
|
105
|
-
tab list every Agent: state, spent of cap, window,
|
|
111
|
+
tab list every Agent: kind, state, spent of cap, window, project
|
|
106
112
|
tab close my-project tab open my-project the kill switch (402 tab_closed on the next call)
|
|
107
113
|
tab cap my-project 25 --window week cap in dollars, and the window
|
|
108
114
|
tab rename my-project "My project (nightly)" the slug stays
|
|
@@ -147,6 +153,6 @@ proxy http://127.0.0.1:8787 (self-hosted) up
|
|
|
147
153
|
flock Hanamori Labs
|
|
148
154
|
folder flocktab-codex
|
|
149
155
|
plan team
|
|
150
|
-
|
|
156
|
+
kind api (the flock's provider key, metered on the tab)
|
|
151
157
|
byok provider keys on this box
|
|
152
158
|
```
|
package/dist/cli.js
CHANGED
|
@@ -7,9 +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
|
|
11
|
-
* tab
|
|
12
|
-
* tab quota what the plans have left (5h / 7d windows) for this Agent
|
|
10
|
+
* tab agent create <name> --subscription an Agent whose harnesses use your own logins
|
|
11
|
+
* tab accounts the accounts your subscription Agents were seen on, with quota left
|
|
13
12
|
* tab alias setup codex make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
|
|
14
13
|
* tab alias remove codex undo that; `tab alias list` shows them
|
|
15
14
|
* tab status which flock, which Agent here, is the proxy up
|
|
@@ -28,7 +27,8 @@ import { hostname } from "node:os";
|
|
|
28
27
|
import { createInterface } from "node:readline/promises";
|
|
29
28
|
import { stdin, stdout } from "node:process";
|
|
30
29
|
import { bold, box, cyan, dim, green, heading, line, rows, underline, yellow } from "./ui.js";
|
|
31
|
-
import {
|
|
30
|
+
import { envFor, knownClients, subscriptionVendorFor } from "./clients.js";
|
|
31
|
+
import { describeLogin, localLogin } from "./logins.js";
|
|
32
32
|
import { prepareCodexHome } from "./codex-home.js";
|
|
33
33
|
import { ConsoleApiError, createAgent, issueAgentKey, listAgents } from "./console-api.js";
|
|
34
34
|
import { consoleUrlFor, DeviceLoginError, startDeviceLogin, waitForApproval } from "./device-login.js";
|
|
@@ -56,9 +56,9 @@ function usage() {
|
|
|
56
56
|
cmd(others.join(" | tab "), "the same, for those agents"),
|
|
57
57
|
cmd("<command> [args]", "any other agent, both APIs pointed at the tab"),
|
|
58
58
|
cmd("use", "pick or change the Agent this folder runs as (.flocktab)"),
|
|
59
|
-
cmd("
|
|
60
|
-
cmd("
|
|
61
|
-
cmd("
|
|
59
|
+
cmd("agent create <name>", "a new Agent: --subscription (your own logins) or --api (the flock's key, metered)"),
|
|
60
|
+
cmd("agent kind <agent> api|subscription", "change it"),
|
|
61
|
+
cmd("accounts", "accounts your subscription Agents were seen on: plan, price, quota left"),
|
|
62
62
|
cmd("alias setup <name>...", "make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)"),
|
|
63
63
|
cmd("alias remove <name>...", "undo that; `tab alias list` shows them"),
|
|
64
64
|
cmd("status", "which flock, which Agent here, is the proxy up"),
|
|
@@ -398,14 +398,19 @@ async function resolveAgent(config, opts = {}) {
|
|
|
398
398
|
say(rows([
|
|
399
399
|
...options.map((a, i) => [
|
|
400
400
|
`${bold(String(i + 1))} ${a.name}`,
|
|
401
|
-
`${a.state === "open" ? green("open") : yellow(a.state)} ${dim(`$${(Number(a.capCents) / 100).toFixed(2)} cap`)}${agents[a.slug] ? dim(" keyed here") : a.hasKey ? dim(" keyed elsewhere") : ""}`,
|
|
401
|
+
`${a.state === "open" ? green("open") : yellow(a.state)} ${a.kind === "subscription" ? yellow("subscription") : dim("api")} ${dim(`$${(Number(a.capCents) / 100).toFixed(2)} cap`)}${agents[a.slug] ? dim(" keyed here") : a.hasKey ? dim(" keyed elsewhere") : ""}`,
|
|
402
402
|
]),
|
|
403
403
|
[`${bold("n")} New Agent`, dim(`named ${suggested}, $50.00 cap`)],
|
|
404
404
|
]));
|
|
405
405
|
const answer = await ask(`Choose 1-${options.length} or ${bold("n")} ${dim("[n]")}: `);
|
|
406
406
|
if (answer === "" || answer.toLowerCase() === "n") {
|
|
407
407
|
const name = (await ask(`${cyan("?")} Agent name ${dim(`[${suggested}]`)}: `)) || suggested;
|
|
408
|
-
|
|
408
|
+
say(rows([
|
|
409
|
+
[`${bold("1")} API key`, dim("the flock's provider key; every call metered against the cap")],
|
|
410
|
+
[`${bold("2")} Subscription`, dim("your own Claude Max / ChatGPT / SuperGrok / Kimi logins; gated and recorded, never charged")],
|
|
411
|
+
]));
|
|
412
|
+
const which = await ask(`Which kind? ${bold("1")} or ${bold("2")} ${dim("[1]")}: `);
|
|
413
|
+
created = { name, kind: which.trim() === "2" ? "subscription" : "api" };
|
|
409
414
|
}
|
|
410
415
|
else {
|
|
411
416
|
const index = Number.parseInt(answer, 10);
|
|
@@ -419,8 +424,8 @@ async function resolveAgent(config, opts = {}) {
|
|
|
419
424
|
let issued;
|
|
420
425
|
try {
|
|
421
426
|
if (created) {
|
|
422
|
-
issued = await createAgent(config.consoleUrl, config.token, { name: created.name });
|
|
423
|
-
ok(`Created Agent ${bold(issued.agent.name)} with a $50.00 cap. Change it in the console.`);
|
|
427
|
+
issued = await createAgent(config.consoleUrl, config.token, { name: created.name, kind: created.kind });
|
|
428
|
+
ok(`Created ${created.kind === "subscription" ? "subscription" : "API-key"} Agent ${bold(issued.agent.name)} with a $50.00 cap. Change it in the console.`);
|
|
424
429
|
}
|
|
425
430
|
else if (chosen) {
|
|
426
431
|
if (chosen.hasKey && !agents[chosen.slug]) {
|
|
@@ -486,8 +491,7 @@ async function status() {
|
|
|
486
491
|
}
|
|
487
492
|
else {
|
|
488
493
|
pairs.push(["plan", me.flock.plan]);
|
|
489
|
-
|
|
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")]);
|
|
494
|
+
pairs.push(["kind", me.kind === "subscription" ? `${bold("subscription")} ${dim("(your own logins; gated and recorded, never charged)")}` : `api ${dim("(the flock's provider key, metered on the tab)")}`]);
|
|
491
495
|
const byok = !me.byok
|
|
492
496
|
? dim("no provider key on this flock")
|
|
493
497
|
: me.unlock === "none"
|
|
@@ -514,20 +518,26 @@ async function runAgent(name, args) {
|
|
|
514
518
|
const agent = await resolveAgent(config);
|
|
515
519
|
if (!agent)
|
|
516
520
|
return 1;
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
// applies to the next run.
|
|
521
|
-
|
|
522
|
-
const vendor =
|
|
521
|
+
// The Agent's kind decides: a subscription Agent's harness brings its own
|
|
522
|
+
// login (Claude Code its Claude one, Codex ChatGPT, Grok SuperGrok, Kimi
|
|
523
|
+
// Code its Kimi plan), read fresh on every launch so a change in the
|
|
524
|
+
// console applies to the next run. An api Agent, or a harness with no
|
|
525
|
+
// consumer plan, is metered with the flock's key.
|
|
526
|
+
const vendor = subscriptionVendorFor(name);
|
|
523
527
|
const me = await whoami(config.proxyUrl, { key: agent.key, unlock: config.unlock });
|
|
524
|
-
const
|
|
528
|
+
const kind = !("error" in me) && me.kind === "subscription" ? "subscription" : "api";
|
|
529
|
+
const auth = kind === "subscription" && vendor ? "subscription" : "key";
|
|
525
530
|
// On a subscription there is no provider key to unlock; the bare key rides in the URL.
|
|
526
531
|
const key = auth === "subscription" ? agent.key : presentedKey({ key: agent.key, unlock: config.unlock });
|
|
527
532
|
reportFolder(config.proxyUrl, key);
|
|
528
533
|
const { spec, env } = envFor({ name, proxyUrl: config.proxyUrl, presentedKey: key, auth });
|
|
529
534
|
if (auth === "subscription") {
|
|
530
|
-
|
|
535
|
+
const login = describeLogin(await localLogin(vendor));
|
|
536
|
+
const vendorName = vendor === "openai" ? "ChatGPT" : vendor === "xai" ? "SuperGrok" : vendor === "kimi" ? "Kimi" : "Claude";
|
|
537
|
+
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.`));
|
|
538
|
+
}
|
|
539
|
+
else if (kind === "subscription") {
|
|
540
|
+
say(dim(`${spec.label} has no consumer plan FlockTab can pass through, so it runs metered on the flock's key.`));
|
|
531
541
|
}
|
|
532
542
|
// An alias shim named like the agent must not be what we spawn.
|
|
533
543
|
env.PATH = pathWithoutShims();
|
|
@@ -741,12 +751,11 @@ async function managed(command, rest) {
|
|
|
741
751
|
return await manage.outside(config, flags);
|
|
742
752
|
case "live":
|
|
743
753
|
return await manage.live(config, flags);
|
|
754
|
+
case "accounts":
|
|
744
755
|
case "quota":
|
|
745
|
-
return await manage.
|
|
746
|
-
case "
|
|
747
|
-
return await manage.
|
|
748
|
-
case "unsubscribe":
|
|
749
|
-
return await manage.subscribe(config, flags, false);
|
|
756
|
+
return await manage.accounts(config, flags);
|
|
757
|
+
case "agent":
|
|
758
|
+
return await manage.agent(config, flags);
|
|
750
759
|
default:
|
|
751
760
|
return await manage.web(config, flags);
|
|
752
761
|
}
|
|
@@ -797,8 +806,8 @@ async function main(argv) {
|
|
|
797
806
|
case "outside":
|
|
798
807
|
case "live":
|
|
799
808
|
case "quota":
|
|
800
|
-
case "
|
|
801
|
-
case "
|
|
809
|
+
case "accounts":
|
|
810
|
+
case "agent":
|
|
802
811
|
case "web":
|
|
803
812
|
return managed(command, rest);
|
|
804
813
|
case "status":
|
package/dist/clients.js
CHANGED
|
@@ -76,6 +76,19 @@ export function openaiBase(proxyUrl, provider) {
|
|
|
76
76
|
export function passthroughBase(proxyUrl, key, provider) {
|
|
77
77
|
return `${proxyUrl}/t/${key}/${provider}`;
|
|
78
78
|
}
|
|
79
|
+
/** Which vendor's login a harness carries: Claude Code its Claude one, Codex ChatGPT, Grok SuperGrok, Kimi Code a Kimi plan. */
|
|
80
|
+
export function subscriptionVendorFor(name) {
|
|
81
|
+
const spec = clientFor(name);
|
|
82
|
+
if (spec.shape === "anthropic")
|
|
83
|
+
return "anthropic";
|
|
84
|
+
if (spec.isolatedHome === "codex")
|
|
85
|
+
return "openai";
|
|
86
|
+
if (spec.provider === "xai")
|
|
87
|
+
return "xai";
|
|
88
|
+
if (spec.provider === "moonshot")
|
|
89
|
+
return "kimi";
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
79
92
|
export function envFor(input) {
|
|
80
93
|
const base = clientFor(input.name);
|
|
81
94
|
const known = input.name in KNOWN;
|
|
@@ -101,10 +114,19 @@ export function envFor(input) {
|
|
|
101
114
|
};
|
|
102
115
|
const setOpenAI = () => {
|
|
103
116
|
if (auth === "subscription") {
|
|
104
|
-
//
|
|
105
|
-
//
|
|
117
|
+
// Each harness carries its own login; a plain OpenAI key would bypass
|
|
118
|
+
// the plan, so none is set. Codex's own home points at the passthrough;
|
|
119
|
+
// Grok Build and Kimi Code read a base URL of their own.
|
|
106
120
|
delete env.OPENAI_BASE_URL;
|
|
107
121
|
delete env.OPENAI_API_KEY;
|
|
122
|
+
if (spec.provider === "xai") {
|
|
123
|
+
env.GROK_XAI_API_BASE_URL = `${passthroughBase(input.proxyUrl, input.presentedKey, "xai")}/v1`;
|
|
124
|
+
delete env.XAI_API_KEY;
|
|
125
|
+
}
|
|
126
|
+
if (spec.provider === "moonshot") {
|
|
127
|
+
env.KIMI_BASE_URL = `${passthroughBase(input.proxyUrl, input.presentedKey, "kimi")}/coding/v1`;
|
|
128
|
+
delete env.KIMI_API_KEY;
|
|
129
|
+
}
|
|
108
130
|
return;
|
|
109
131
|
}
|
|
110
132
|
env.OPENAI_BASE_URL = openaiBase(input.proxyUrl, spec.provider);
|
package/dist/logins.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which account each harness is signed in with on this machine, read from
|
|
3
|
+
* the files the harnesses keep themselves. Used for one thing: telling the
|
|
4
|
+
* person, before a subscription Agent launches, which login is about to be
|
|
5
|
+
* used. Nothing here is sent anywhere; tokens are never read, only the
|
|
6
|
+
* account fields beside them.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
async function jsonFile(file) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
14
|
+
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function str(value) {
|
|
21
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
22
|
+
}
|
|
23
|
+
/** The claims of a JWT, unverified; only the account fields are looked at. */
|
|
24
|
+
function jwtClaims(token) {
|
|
25
|
+
if (typeof token !== "string")
|
|
26
|
+
return undefined;
|
|
27
|
+
const parts = token.split(".");
|
|
28
|
+
if (parts.length !== 3)
|
|
29
|
+
return undefined;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
32
|
+
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Claude Code keeps the signed-in account (not the token) in `~/.claude.json`
|
|
40
|
+
* under `oauthAccount`; Codex keeps its ChatGPT login in `auth.json` under
|
|
41
|
+
* the home it runs with. Grok Build and Kimi Code are asked nothing: their
|
|
42
|
+
* accounts are read off the wire by the proxy.
|
|
43
|
+
*/
|
|
44
|
+
export async function localLogin(vendor, opts = {}) {
|
|
45
|
+
const home = opts.home ?? homedir();
|
|
46
|
+
if (vendor === "anthropic") {
|
|
47
|
+
const account = (await jsonFile(path.join(home, ".claude.json")))?.oauthAccount;
|
|
48
|
+
if (!account)
|
|
49
|
+
return undefined;
|
|
50
|
+
const email = str(account.emailAddress);
|
|
51
|
+
const plan = str(account.organizationRateLimitTier) ?? str(account.organizationType);
|
|
52
|
+
const accountId = str(account.organizationUuid);
|
|
53
|
+
return { ...(email ? { email } : {}), ...(plan ? { plan } : {}), ...(accountId ? { accountId } : {}) };
|
|
54
|
+
}
|
|
55
|
+
if (vendor === "openai") {
|
|
56
|
+
const auth = await jsonFile(path.join(opts.codexHome ?? path.join(home, ".codex"), "auth.json"));
|
|
57
|
+
const tokens = auth?.tokens;
|
|
58
|
+
if (!tokens)
|
|
59
|
+
return undefined;
|
|
60
|
+
const claims = jwtClaims(tokens.id_token);
|
|
61
|
+
const authClaims = claims?.["https://api.openai.com/auth"];
|
|
62
|
+
const email = str(claims?.email);
|
|
63
|
+
const plan = str(authClaims?.chatgpt_plan_type);
|
|
64
|
+
const accountId = str(tokens.account_id) ?? str(authClaims?.chatgpt_account_id);
|
|
65
|
+
return { ...(email ? { email } : {}), ...(plan ? { plan } : {}), ...(accountId ? { accountId } : {}) };
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
/** "me@x.com (Claude Max 20x)" or nothing worth saying. */
|
|
70
|
+
export function describeLogin(login) {
|
|
71
|
+
if (!login)
|
|
72
|
+
return undefined;
|
|
73
|
+
const plan = login.plan?.replace(/^default_/, "").replaceAll("_", " ");
|
|
74
|
+
if (login.email && plan)
|
|
75
|
+
return `${login.email} (${plan})`;
|
|
76
|
+
return login.email ?? plan ?? (login.accountId ? `account ${login.accountId}` : undefined);
|
|
77
|
+
}
|
package/dist/manage.js
CHANGED
|
@@ -59,12 +59,7 @@ export function parseFlags(argv) {
|
|
|
59
59
|
}
|
|
60
60
|
return flags;
|
|
61
61
|
}
|
|
62
|
-
|
|
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
|
+
const kindOf = (t) => (t.kind === "subscription" ? yellow("subscription") : "api");
|
|
68
63
|
async function tabsOf(call) {
|
|
69
64
|
return (await call("GET", "/api/cli/tabs")).tabs;
|
|
70
65
|
}
|
|
@@ -98,7 +93,7 @@ export async function list(config, flags) {
|
|
|
98
93
|
const tabs = await tabsOf(api(config));
|
|
99
94
|
emit(flags.json, { tabs }, () => tabs.length === 0
|
|
100
95
|
? dim("No Agents yet. Run tab claude or tab codex in a project folder.")
|
|
101
|
-
: table(["agent", "state", "spent", "cap", "used", "window", "
|
|
96
|
+
: table(["agent", "kind", "state", "spent", "cap", "used", "window", "project"], tabs.map((t) => [t.name === t.slug ? t.slug : `${t.name} ${dim(t.slug)}`, kindOf(t), state(t.state), money(t.spentCents), money(t.capCents), pct(t.spentCents, t.capCents), t.window, t.projectName ?? dim("-")]), { right: [3, 4, 5] }));
|
|
102
97
|
return 0;
|
|
103
98
|
}
|
|
104
99
|
export async function setState(config, flags, next) {
|
|
@@ -146,55 +141,70 @@ export async function archive(config, flags, ask) {
|
|
|
146
141
|
return 0;
|
|
147
142
|
}
|
|
148
143
|
/**
|
|
149
|
-
* `tab
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* list price as a shadow row, and the seat's monthly price sits beside the
|
|
155
|
-
* meter in the project rollup.
|
|
144
|
+
* `tab agent create <name> [--subscription|--api] [--cap 50]` makes an Agent
|
|
145
|
+
* of one of the two kinds; `tab agent kind <agent> api|subscription` changes
|
|
146
|
+
* it. A subscription Agent's harnesses (Claude Code, Codex, Grok, Kimi Code)
|
|
147
|
+
* bring their own logins: every call is still gated by the tab and recorded
|
|
148
|
+
* at list price, filed under the account the vendor names, never charged.
|
|
156
149
|
*/
|
|
157
|
-
export async function
|
|
150
|
+
export async function agent(config, flags) {
|
|
158
151
|
const call = api(config);
|
|
159
|
-
const verb =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
152
|
+
const [verb, ...rest] = flags.args;
|
|
153
|
+
if (verb === "create" || verb === "new") {
|
|
154
|
+
const name = rest.join(" ").trim();
|
|
155
|
+
if (!name)
|
|
156
|
+
throw new ManageError('tab agent create <name> [--subscription|--api] [--cap 50]');
|
|
157
|
+
const kind = flags.opts.subscription !== undefined ? "subscription" : "api";
|
|
158
|
+
const body = { name, kind };
|
|
159
|
+
if (flags.opts.cap)
|
|
160
|
+
body.capDollars = flags.opts.cap.replace(/^\$/, "");
|
|
161
|
+
const made = await call("POST", "/api/cli/agents", body);
|
|
162
|
+
// The key comes back once; it is this machine's, so keep it like tab use would.
|
|
163
|
+
const agents = { ...(config.agents ?? {}), [made.agent.slug]: { key: made.key, agentId: made.agent.id, agentName: made.agent.name } };
|
|
164
|
+
await saveConfig({ ...config, agents });
|
|
165
|
+
emit(flags.json, { agent: made.agent, kind }, () => `Created ${kind === "subscription" ? bold("subscription") : bold("API-key")} Agent ${bold(made.agent.name)} ${dim(`(${made.agent.slug})`)}; its key is saved on this machine.\n` +
|
|
166
|
+
dim(kind === "subscription"
|
|
167
|
+
? "Its harnesses use your own logins: tab claude your Claude one, tab codex ChatGPT, tab grok SuperGrok, tab kimi Kimi. Gated and recorded, never charged. tab use picks it for a folder."
|
|
168
|
+
: "Its calls use the flock's provider key and are metered against the cap. tab use picks it for a folder."));
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
if (verb === "kind" || verb === "type") {
|
|
172
|
+
const [given, wanted] = rest;
|
|
173
|
+
if (!given || !wanted)
|
|
174
|
+
throw new ManageError("tab agent kind <agent> api|subscription");
|
|
175
|
+
const slug = await resolveSlug(call, given);
|
|
176
|
+
const { tab } = await call("PATCH", `/api/cli/tabs/${encodeURIComponent(slug)}`, { kind: wanted });
|
|
177
|
+
emit(flags.json, { tab }, () => `${bold(tab.name)} is now ${tab.kind === "subscription" ? bold("a subscription Agent") + dim(": its harnesses use your own logins from the next run") : bold("an API-key Agent") + dim(": metered with the flock's key from the next run")}.`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
throw new ManageError("tab agent create <name> [--subscription|--api] | tab agent kind <agent> api|subscription");
|
|
179
181
|
}
|
|
180
|
-
/**
|
|
181
|
-
|
|
182
|
+
/**
|
|
183
|
+
* `tab accounts` (also `tab quota`): every account the flock's subscription
|
|
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 what the plan has left.
|
|
186
|
+
*/
|
|
187
|
+
export async function accounts(config, flags) {
|
|
182
188
|
const call = api(config);
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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.`;
|
|
189
|
+
const { accounts: list } = await call("GET", "/api/cli/accounts");
|
|
190
|
+
emit(flags.json, { accounts: list }, () => {
|
|
191
|
+
if (list.length === 0)
|
|
192
|
+
return dim("No accounts seen yet. Run tab claude or tab codex on a subscription Agent; the account shows after the first call.");
|
|
190
193
|
const left = (w) => {
|
|
191
194
|
const pctLeft = Math.max(0, 100 - w.usedPct);
|
|
192
195
|
const paint = pctLeft <= 10 ? red : pctLeft <= 30 ? yellow : green;
|
|
193
|
-
return paint(`${pctLeft}
|
|
196
|
+
return paint(`${pctLeft}%`);
|
|
194
197
|
};
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
return table(["vendor", "account", "plan", "/month", "30d at list", "calls", "left", "agents"], list.map((a) => [
|
|
199
|
+
a.vendor,
|
|
200
|
+
a.label ?? a.email ?? dim(a.id),
|
|
201
|
+
a.planLabel ?? dim("unknown"),
|
|
202
|
+
a.monthlyCents ? money(a.monthlyCents) : dim("-"),
|
|
203
|
+
money(a.listCents),
|
|
204
|
+
String(a.calls),
|
|
205
|
+
a.quota.length === 0 ? dim("-") : a.quota.map((q) => `${q.window} ${left(q)}`).join(" "),
|
|
206
|
+
a.agents.join(", "),
|
|
207
|
+
]), { right: [3, 4, 5] }) + `\n${dim("Set a price or label in the console, Control, Accounts.")}`;
|
|
198
208
|
});
|
|
199
209
|
return 0;
|
|
200
210
|
}
|
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.5";
|
|
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.8";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanamorilabs/tab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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.5",
|
|
26
|
+
"@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.5",
|
|
27
|
+
"@hanamorilabs/flocktab-proxy-linux-x64": "0.1.5",
|
|
28
|
+
"@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.5",
|
|
29
|
+
"@hanamorilabs/flocktab-proxy-win-x64": "0.1.5"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.18.6",
|