@hanamorilabs/tab 0.1.13 → 0.1.15
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/dist/cli.js +41 -19
- package/dist/clients.js +4 -1
- package/dist/console-api.js +4 -0
- package/dist/manage.js +19 -3
- package/dist/project.js +46 -4
- package/dist/proxy-bin.js +1 -1
- package/dist/tab-docs-shared.js +9 -0
- package/dist/tab-docs.js +14 -9
- package/dist/version.js +1 -1
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -37,14 +37,15 @@ import { runPooled } from "./pool-run.js";
|
|
|
37
37
|
import { renderPool } from "./pool-view.js";
|
|
38
38
|
import { renderHelp } from "./help.js";
|
|
39
39
|
import { findTabCommand } from "./tab-docs.js";
|
|
40
|
-
import { ConsoleApiError, createAgent, issueAgentKey, listAgents } from "./console-api.js";
|
|
40
|
+
import { ConsoleApiError, createAgent, issueAgentKey, listAgents, setAgentHarness } from "./console-api.js";
|
|
41
41
|
import { consoleUrlFor, DeviceLoginError, startDeviceLogin, waitForApproval } from "./device-login.js";
|
|
42
42
|
import { clearConfig, configDir, configPath, HOSTED_PROXY, isLocalProxy, loadConfig, LOCAL_PROXY, normalizeProxyUrl, presentedKey, saveConfig, } from "./config.js";
|
|
43
43
|
import { reportFolder } from "./folder.js";
|
|
44
44
|
import { whoami } from "./proxy-identity.js";
|
|
45
45
|
import { listAliases, pathLine, pathWithoutShims, removeAlias, shimDir, shimDirOnPath, validAliasName, writeAlias } from "./alias.js";
|
|
46
46
|
import * as manage from "./manage.js";
|
|
47
|
-
import { agentNameFor, projectRoot, readProject
|
|
47
|
+
import { agentNameFor, assignProjectAgent, projectRoot, readProject } from "./project.js";
|
|
48
|
+
import { HARNESSES, agentNameFor as harnessAgentName } from "./tab-docs-shared.js";
|
|
48
49
|
import { downloadProxy, hasProxyEnv, packagedBinPath, proxyBinPath, proxyEnvPath, proxyInstalled, proxyLogPath, hasProxyLog, PROXY_VERSION, startProxy, stopProxy, tailProxyLog, writeProxyEnv, } from "./proxy-bin.js";
|
|
49
50
|
import { proxyHealth, waitHealthy } from "./selfhost.js";
|
|
50
51
|
import { TAB_VERSION } from "./version.js";
|
|
@@ -319,13 +320,23 @@ async function bringUp(proxyUrl) {
|
|
|
319
320
|
* holds its key. Missing either: ask (or, with `.flocktab` present, mint the
|
|
320
321
|
* key silently for this machine), save both, carry on.
|
|
321
322
|
*/
|
|
323
|
+
/** `claude`, `codex`, `grok`, `kimi`, else `any`: the harness a command is. */
|
|
324
|
+
function harnessOf(command) {
|
|
325
|
+
return HARNESSES.includes(command) && command !== "any" ? command : "any";
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The Agent this folder runs `harness` as. One harness on one project is
|
|
329
|
+
* one Agent, so `tab claude` and `tab codex` in a folder are two Agents
|
|
330
|
+
* (that is what the plan counts), each asked for once.
|
|
331
|
+
*/
|
|
322
332
|
async function resolveAgent(config, opts = {}) {
|
|
323
333
|
const agents = config.agents ?? {};
|
|
334
|
+
const harness = opts.harness ?? "any";
|
|
324
335
|
// CI and scripts: FLOCKTAB_KEY is the Agent, no folder involved.
|
|
325
336
|
if (agents.env && !opts.pick)
|
|
326
337
|
return agents.env;
|
|
327
338
|
const cwd = process.cwd();
|
|
328
|
-
const project = opts.pick ? undefined : await readProject(cwd);
|
|
339
|
+
const project = opts.pick ? undefined : await readProject(cwd, harness);
|
|
329
340
|
if (project && agents[project.agent])
|
|
330
341
|
return agents[project.agent];
|
|
331
342
|
if (!config.token || !config.consoleUrl) {
|
|
@@ -351,14 +362,15 @@ async function resolveAgent(config, opts = {}) {
|
|
|
351
362
|
}
|
|
352
363
|
if (!chosen) {
|
|
353
364
|
const root = await projectRoot(cwd);
|
|
354
|
-
const suggested = agentNameFor(root);
|
|
355
|
-
say(heading(`Which Agent does this folder run as?`));
|
|
356
|
-
say(dim(`${root} · flock ${listed.flock.name}`));
|
|
357
|
-
|
|
365
|
+
const suggested = harnessAgentName(agentNameFor(root), harness);
|
|
366
|
+
say(heading(harness === "any" ? "Which Agent does this folder run as?" : `Which Agent does this folder run ${harness} as?`));
|
|
367
|
+
say(dim(`${root} · flock ${listed.flock.name}${harness === "any" ? "" : ` · one harness per Agent: this one is for ${harness}`}`));
|
|
368
|
+
// Only Agents of this harness, plus untied ones (made before harnesses were told apart) that can be tied now.
|
|
369
|
+
const options = listed.agents.filter((a) => harness === "any" ? (a.harness ?? "any") === "any" : (a.harness ?? "any") === harness || (a.harness ?? "any") === "any");
|
|
358
370
|
say(rows([
|
|
359
371
|
...options.map((a, i) => [
|
|
360
372
|
`${bold(String(i + 1))} ${a.name}`,
|
|
361
|
-
`${a.state === "open" ? green("open") : yellow(a.state)} ${a.kind === "subscription" ? yellow("subscription") : dim("api")} ${dim(
|
|
373
|
+
`${a.state === "open" ? green("open") : yellow(a.state)} ${a.kind === "subscription" ? yellow("subscription") : dim("api")} ${dim(a.harness && a.harness !== "any" ? a.harness : "untied")}${a.kind === "api" ? dim(` $${(Number(a.capCents) / 100).toFixed(2)} cap`) : ""}${agents[a.slug] ? dim(" keyed here") : a.hasKey ? dim(" keyed elsewhere") : ""}`,
|
|
362
374
|
]),
|
|
363
375
|
[`${bold("n")} New Agent`, dim(`named ${suggested}, $50.00 cap`)],
|
|
364
376
|
]));
|
|
@@ -370,7 +382,7 @@ async function resolveAgent(config, opts = {}) {
|
|
|
370
382
|
[`${bold("2")} Subscription`, dim("your own Claude Max / ChatGPT / SuperGrok / Kimi logins; gated and recorded, never charged")],
|
|
371
383
|
]));
|
|
372
384
|
const which = await ask(`Which kind? ${bold("1")} or ${bold("2")} ${dim("[1]")}: `);
|
|
373
|
-
created = { name, kind: which.trim() === "2" ? "subscription" : "api" };
|
|
385
|
+
created = { name, kind: which.trim() === "2" ? "subscription" : "api", harness };
|
|
374
386
|
}
|
|
375
387
|
else {
|
|
376
388
|
const index = Number.parseInt(answer, 10);
|
|
@@ -384,8 +396,8 @@ async function resolveAgent(config, opts = {}) {
|
|
|
384
396
|
let issued;
|
|
385
397
|
try {
|
|
386
398
|
if (created) {
|
|
387
|
-
issued = await createAgent(config.consoleUrl, config.token, { name: created.name, kind: created.kind });
|
|
388
|
-
ok(`Created ${created.kind === "subscription" ? "subscription" : "API-key"} Agent ${bold(issued.agent.name)} with a $50.00 cap. Change it in the console
|
|
399
|
+
issued = await createAgent(config.consoleUrl, config.token, { name: created.name, kind: created.kind, harness: created.harness });
|
|
400
|
+
ok(`Created ${created.kind === "subscription" ? "subscription" : "API-key"} ${created.harness === "any" ? "" : `${created.harness} `}Agent ${bold(issued.agent.name)}${created.kind === "api" ? " with a $50.00 cap. Change it in the console." : "."}`);
|
|
389
401
|
}
|
|
390
402
|
else if (chosen) {
|
|
391
403
|
if (chosen.hasKey && !agents[chosen.slug]) {
|
|
@@ -395,6 +407,11 @@ async function resolveAgent(config, opts = {}) {
|
|
|
395
407
|
return undefined;
|
|
396
408
|
}
|
|
397
409
|
issued = await issueAgentKey(config.consoleUrl, config.token, chosen.id);
|
|
410
|
+
// An untied Agent chosen for a harness is tied to it from now on, so the proxies enforce it.
|
|
411
|
+
if (harness !== "any" && (chosen.harness ?? "any") === "any") {
|
|
412
|
+
await setAgentHarness(config.consoleUrl, config.token, chosen.slug, harness);
|
|
413
|
+
say(dim(`${chosen.name} is now a ${harness} Agent.`));
|
|
414
|
+
}
|
|
398
415
|
}
|
|
399
416
|
else {
|
|
400
417
|
return undefined;
|
|
@@ -415,8 +432,8 @@ async function resolveAgent(config, opts = {}) {
|
|
|
415
432
|
config.agents = next.agents ?? {};
|
|
416
433
|
if (next.unlock)
|
|
417
434
|
config.unlock = next.unlock;
|
|
418
|
-
const file = await
|
|
419
|
-
ok(`This folder runs as ${bold(issued.agent.name)} ${dim(`(${file})`)}`);
|
|
435
|
+
const file = await assignProjectAgent(await projectRoot(cwd), harness, issued.agent.slug);
|
|
436
|
+
ok(`This folder runs ${harness === "any" ? "" : `${harness} `}as ${bold(issued.agent.name)} ${dim(`(${file})`)}`);
|
|
420
437
|
return login;
|
|
421
438
|
}
|
|
422
439
|
/** `tab version`: this CLI, and the proxy it would run (packaged, downloaded, or neither). */
|
|
@@ -558,7 +575,7 @@ async function runAgent(name, argv) {
|
|
|
558
575
|
// Hosted whoami below proves readiness and current Agent kind in one round trip.
|
|
559
576
|
if (config.mode === "self-hosted" && !(await ensureProxy(config)))
|
|
560
577
|
return 1;
|
|
561
|
-
const agent = await resolveAgent(config);
|
|
578
|
+
const agent = await resolveAgent(config, { harness: harnessOf(name) });
|
|
562
579
|
if (!agent)
|
|
563
580
|
return 1;
|
|
564
581
|
// The Agent's kind decides: a subscription Agent's harness brings its own
|
|
@@ -770,7 +787,7 @@ async function poolCommand(argv) {
|
|
|
770
787
|
say(dim(`Signing ${harness} in for ${memberName}. Your browser opens; pick the account for this login.`));
|
|
771
788
|
// Plain launch: no proxy, no tab key. This only creates the login, in the harness's own store.
|
|
772
789
|
const loginEnv = { ...process.env, ...memberEnv(vendor, member), PATH: pathWithoutShims() };
|
|
773
|
-
for (const k of ["ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "OPENAI_BASE_URL", "OPENAI_API_KEY", "XAI_API_KEY", "GROK_XAI_API_BASE_URL", "KIMI_API_KEY", "KIMI_BASE_URL", "KIMI_CODE_BASE_URL"])
|
|
790
|
+
for (const k of ["ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "OPENAI_BASE_URL", "OPENAI_API_KEY", "XAI_API_KEY", "GROK_XAI_API_BASE_URL", "GROK_CLI_CHAT_PROXY_BASE_URL", "KIMI_API_KEY", "KIMI_BASE_URL", "KIMI_CODE_BASE_URL"])
|
|
774
791
|
delete loginEnv[k];
|
|
775
792
|
const run = (argv) => new Promise((resolve) => {
|
|
776
793
|
const child = spawn(harness, argv, { stdio: "inherit", env: loginEnv });
|
|
@@ -871,14 +888,19 @@ function pathHint() {
|
|
|
871
888
|
say(` ${bold(pathLine())}`);
|
|
872
889
|
return 0;
|
|
873
890
|
}
|
|
874
|
-
/** `tab use`: pick or change the Agent
|
|
875
|
-
async function use() {
|
|
891
|
+
/** `tab use [claude|codex|grok|kimi]`: pick or change the Agent this folder runs a harness as; bare, the folder's default. */
|
|
892
|
+
async function use(args) {
|
|
876
893
|
const config = await ensureLogin();
|
|
877
894
|
if (!config)
|
|
878
895
|
return 2;
|
|
879
896
|
if (!(await ensureProxy(config)))
|
|
880
897
|
return 1;
|
|
881
|
-
const
|
|
898
|
+
const word = args[0];
|
|
899
|
+
if (word && harnessOf(word) === "any") {
|
|
900
|
+
fail(`tab use [claude|codex|grok|kimi]: which harness to pick an Agent for; bare for the folder's default.`);
|
|
901
|
+
return 2;
|
|
902
|
+
}
|
|
903
|
+
const agent = await resolveAgent(config, { pick: true, harness: word ? harnessOf(word) : "any" });
|
|
882
904
|
return agent ? 0 : 1;
|
|
883
905
|
}
|
|
884
906
|
/**
|
|
@@ -1065,7 +1087,7 @@ async function main(argv) {
|
|
|
1065
1087
|
ok("Forgot the session and keys on this machine.");
|
|
1066
1088
|
return 0;
|
|
1067
1089
|
case "use":
|
|
1068
|
-
return use();
|
|
1090
|
+
return use(rest);
|
|
1069
1091
|
case "alias":
|
|
1070
1092
|
return alias(rest);
|
|
1071
1093
|
case "unalias":
|
package/dist/clients.js
CHANGED
|
@@ -124,7 +124,10 @@ export function envFor(input) {
|
|
|
124
124
|
delete env.OPENAI_BASE_URL;
|
|
125
125
|
delete env.OPENAI_API_KEY;
|
|
126
126
|
if (spec.provider === "xai") {
|
|
127
|
-
|
|
127
|
+
// On a SuperGrok login Grok Build talks to xAI's CLI chat proxy, chosen by this variable; the public-API
|
|
128
|
+
// variable only matters with an API key, which a subscription launch never has.
|
|
129
|
+
env.GROK_CLI_CHAT_PROXY_BASE_URL = `${passthroughBase(input.proxyUrl, input.presentedKey, "xai")}/v1`;
|
|
130
|
+
delete env.GROK_XAI_API_BASE_URL;
|
|
128
131
|
delete env.XAI_API_KEY;
|
|
129
132
|
}
|
|
130
133
|
if (spec.provider === "moonshot") {
|
package/dist/console-api.js
CHANGED
|
@@ -32,3 +32,7 @@ export async function createAgent(consoleUrl, token, input, fetchImpl = fetch) {
|
|
|
32
32
|
export async function issueAgentKey(consoleUrl, token, agentId, fetchImpl = fetch) {
|
|
33
33
|
return call(consoleUrl, token, `/api/cli/agents/${encodeURIComponent(agentId)}/key`, { method: "POST" }, fetchImpl);
|
|
34
34
|
}
|
|
35
|
+
/** Tie an untied Agent to a harness (or change it): `tab agent harness <agent> codex`. */
|
|
36
|
+
export async function setAgentHarness(consoleUrl, token, slug, harness, fetchImpl = fetch) {
|
|
37
|
+
return call(consoleUrl, token, `/api/cli/tabs/${encodeURIComponent(slug)}`, { method: "PATCH", body: JSON.stringify({ harness }) }, fetchImpl);
|
|
38
|
+
}
|
package/dist/manage.js
CHANGED
|
@@ -61,6 +61,10 @@ export function parseFlags(argv) {
|
|
|
61
61
|
return flags;
|
|
62
62
|
}
|
|
63
63
|
const kindOf = (t) => (t.kind === "subscription" ? yellow("subscription") : "api");
|
|
64
|
+
const harnessOf = (t) => (t.harness && t.harness !== "any" ? t.harness : dim("any"));
|
|
65
|
+
// A subscription never touches its cap: nothing to show but the kill switch.
|
|
66
|
+
const capOf = (t) => (t.kind === "subscription" ? dim("-") : money(t.capCents));
|
|
67
|
+
const usedOf = (t) => (t.kind === "subscription" ? dim("-") : pct(t.spentCents, t.capCents));
|
|
64
68
|
async function tabsOf(call) {
|
|
65
69
|
return (await call("GET", "/api/cli/tabs")).tabs;
|
|
66
70
|
}
|
|
@@ -94,7 +98,7 @@ export async function list(config, flags) {
|
|
|
94
98
|
const tabs = await tabsOf(api(config));
|
|
95
99
|
emit(flags.json, { tabs }, () => tabs.length === 0
|
|
96
100
|
? dim("No Agents yet. Run tab claude or tab codex in a project folder.")
|
|
97
|
-
: 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),
|
|
101
|
+
: table(["agent", "harness", "kind", "state", "spent", "cap", "used", "window", "project"], tabs.map((t) => [t.name === t.slug ? t.slug : `${t.name} ${dim(t.slug)}`, harnessOf(t), kindOf(t), state(t.state), t.kind === "subscription" ? dim("-") : money(t.spentCents), capOf(t), usedOf(t), t.window, t.projectName ?? dim("-")]), { right: [3, 4, 5] }));
|
|
98
102
|
return 0;
|
|
99
103
|
}
|
|
100
104
|
export async function setState(config, flags, next) {
|
|
@@ -156,7 +160,7 @@ export async function agent(config, flags) {
|
|
|
156
160
|
if (!name)
|
|
157
161
|
throw new ManageError('tab agent create <name> [--subscription|--api] [--cap 50]');
|
|
158
162
|
const kind = flags.opts.subscription !== undefined ? "subscription" : "api";
|
|
159
|
-
const body = { name, kind };
|
|
163
|
+
const body = { name, kind, ...(flags.opts.harness ? { harness: flags.opts.harness.toLowerCase() } : {}) };
|
|
160
164
|
if (flags.opts.cap)
|
|
161
165
|
body.capDollars = flags.opts.cap.replace(/^\$/, "");
|
|
162
166
|
const made = await call("POST", "/api/cli/agents", body);
|
|
@@ -169,6 +173,18 @@ export async function agent(config, flags) {
|
|
|
169
173
|
: "Its calls use the flock's provider key and are metered against the cap. tab use picks it for a folder."));
|
|
170
174
|
return 0;
|
|
171
175
|
}
|
|
176
|
+
if (verb === "harness") {
|
|
177
|
+
const [given, wanted] = rest;
|
|
178
|
+
const harness = (wanted ?? "").toLowerCase();
|
|
179
|
+
if (!given || !["claude", "codex", "grok", "kimi", "any"].includes(harness))
|
|
180
|
+
throw new ManageError("tab agent harness <agent> claude|codex|grok|kimi|any");
|
|
181
|
+
const slug = await resolveSlug(call, given);
|
|
182
|
+
const { tab } = await call("PATCH", `/api/cli/tabs/${encodeURIComponent(slug)}`, { harness });
|
|
183
|
+
emit(flags.json, { tab }, () => harness === "any"
|
|
184
|
+
? `${bold(tab.name)} is tied to no harness: it is metered on the tab and cannot pass a login through.`
|
|
185
|
+
: `${bold(tab.name)} is a ${bold(harness)} Agent. A ${harness} login passes through it; any other harness is refused before the vendor.`);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
172
188
|
if (verb === "kind" || verb === "type") {
|
|
173
189
|
const [given, wanted] = rest;
|
|
174
190
|
if (!given || !wanted)
|
|
@@ -178,7 +194,7 @@ export async function agent(config, flags) {
|
|
|
178
194
|
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")}.`);
|
|
179
195
|
return 0;
|
|
180
196
|
}
|
|
181
|
-
throw new ManageError("tab agent create <name> [--subscription|--api] | tab agent kind <agent> api|subscription");
|
|
197
|
+
throw new ManageError("tab agent create <name> [--subscription|--api] [--harness claude|codex|grok|kimi] | tab agent kind <agent> api|subscription | tab agent harness <agent> claude|codex|grok|kimi|any");
|
|
182
198
|
}
|
|
183
199
|
/**
|
|
184
200
|
* `tab accounts` (also `tab quota`): every account the flock's subscription
|
package/dist/project.js
CHANGED
|
@@ -16,6 +16,7 @@ export function projectFileName(env = process.env) {
|
|
|
16
16
|
const name = env.FLOCKTAB_PROJECT_FILE?.trim();
|
|
17
17
|
return name && /^\.[A-Za-z0-9._-]{1,40}$/.test(name) ? name : PROJECT_FILE;
|
|
18
18
|
}
|
|
19
|
+
const SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
|
19
20
|
async function exists(file) {
|
|
20
21
|
try {
|
|
21
22
|
await access(file);
|
|
@@ -40,19 +41,49 @@ export async function findProjectFile(cwd) {
|
|
|
40
41
|
dir = parent;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
/** The whole file, cleaned: only valid slugs survive. */
|
|
45
|
+
export async function readProjectConfig(cwd) {
|
|
44
46
|
const file = await findProjectFile(cwd);
|
|
45
47
|
if (!file)
|
|
46
48
|
return undefined;
|
|
47
49
|
try {
|
|
48
50
|
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
if (!parsed || typeof parsed !== "object")
|
|
52
|
+
return { file, config: {} };
|
|
53
|
+
const raw = parsed;
|
|
54
|
+
const config = {};
|
|
55
|
+
if (typeof raw.agent === "string" && SLUG.test(raw.agent))
|
|
56
|
+
config.agent = raw.agent;
|
|
57
|
+
if (raw.agents && typeof raw.agents === "object") {
|
|
58
|
+
const agents = {};
|
|
59
|
+
for (const [harness, slug] of Object.entries(raw.agents)) {
|
|
60
|
+
if (typeof slug === "string" && SLUG.test(slug) && /^[a-z]{1,16}$/.test(harness))
|
|
61
|
+
agents[harness] = slug;
|
|
62
|
+
}
|
|
63
|
+
if (Object.keys(agents).length > 0)
|
|
64
|
+
config.agents = agents;
|
|
65
|
+
}
|
|
66
|
+
return { file, config };
|
|
52
67
|
}
|
|
53
68
|
catch {
|
|
54
69
|
// Unreadable: treat as absent and ask again.
|
|
70
|
+
return undefined;
|
|
55
71
|
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The Agent this folder runs `harness` as: its own entry, else the file's
|
|
75
|
+
* single `agent` (an older file, or a folder that made one Agent for
|
|
76
|
+
* everything before harnesses were told apart).
|
|
77
|
+
*/
|
|
78
|
+
export async function readProject(cwd, harness = "any") {
|
|
79
|
+
const found = await readProjectConfig(cwd);
|
|
80
|
+
if (!found)
|
|
81
|
+
return undefined;
|
|
82
|
+
const own = found.config.agents?.[harness];
|
|
83
|
+
if (own)
|
|
84
|
+
return { file: found.file, agent: own, own: true };
|
|
85
|
+
if (found.config.agent)
|
|
86
|
+
return { file: found.file, agent: found.config.agent, own: false };
|
|
56
87
|
return undefined;
|
|
57
88
|
}
|
|
58
89
|
/** Where a new `.flocktab` goes: the git root above `cwd` when there is one, else `cwd`. */
|
|
@@ -72,6 +103,17 @@ export async function writeProject(dir, config) {
|
|
|
72
103
|
await writeFile(file, `${JSON.stringify(config)}\n`);
|
|
73
104
|
return file;
|
|
74
105
|
}
|
|
106
|
+
/** Set which Agent this folder runs `harness` as, keeping the rest of the file. */
|
|
107
|
+
export async function assignProjectAgent(dir, harness, slug) {
|
|
108
|
+
const existing = (await readProjectConfig(dir))?.config ?? {};
|
|
109
|
+
const agents = { ...(existing.agents ?? {}) };
|
|
110
|
+
if (harness === "any") {
|
|
111
|
+
// The folder's default: also what older files meant by `agent`.
|
|
112
|
+
return writeProject(dir, { agent: slug, ...(Object.keys(agents).length > 0 ? { agents } : {}) });
|
|
113
|
+
}
|
|
114
|
+
agents[harness] = slug;
|
|
115
|
+
return writeProject(dir, { ...(existing.agent ? { agent: existing.agent } : {}), agents });
|
|
116
|
+
}
|
|
75
117
|
/** A default Agent name for a folder: its basename, kebab-cased. */
|
|
76
118
|
export function agentNameFor(dir) {
|
|
77
119
|
return path
|
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.10";
|
|
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")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness vocabulary, the same as `@flocktab/shared`'s. `tab` ships to
|
|
3
|
+
* npm alone, so it carries a copy rather than the dependency.
|
|
4
|
+
*/
|
|
5
|
+
export const HARNESSES = ["claude", "codex", "grok", "kimi", "any"];
|
|
6
|
+
/** `dash-f4f` + `claude` -> `dash-f4f-claude`; `any` keeps the folder's name. */
|
|
7
|
+
export function agentNameFor(project, harness) {
|
|
8
|
+
return harness === "any" ? project : `${project}-${harness}`;
|
|
9
|
+
}
|
package/dist/tab-docs.js
CHANGED
|
@@ -35,11 +35,11 @@ export const TAB_COMMANDS = [
|
|
|
35
35
|
{
|
|
36
36
|
name: "use",
|
|
37
37
|
group: "start",
|
|
38
|
-
usage: ["use"],
|
|
39
|
-
summary: "Pick or change the Agent this folder runs as.",
|
|
38
|
+
usage: ["use", "use claude|codex|grok|kimi"],
|
|
39
|
+
summary: "Pick or change the Agent this folder runs a harness as.",
|
|
40
40
|
details: [
|
|
41
|
-
"
|
|
42
|
-
"
|
|
41
|
+
"An Agent is one harness on one project. tab claude and tab codex in the same folder are two Agents (that is what the plan counts), each asked for once: the first run of a harness in a folder lists the flock's Agents of that harness (and untied ones it can tie), or makes a new one named <folder>-<harness> and asks its kind. The choice goes into .flocktab at the git root, one entry per harness, and a key for that Agent is minted and kept on this machine.",
|
|
42
|
+
"tab use codex changes the answer for one harness; bare tab use sets the folder's default, which every other command (tab python …) runs as. An older .flocktab with a single Agent applies to every harness until one is set apart.",
|
|
43
43
|
"An Agent that already holds a key on another machine can be used here too, but minting a key here replaces the one there. tab asks before doing it.",
|
|
44
44
|
],
|
|
45
45
|
see: ["agent", "key", "list"],
|
|
@@ -91,18 +91,22 @@ export const TAB_COMMANDS = [
|
|
|
91
91
|
name: "agent",
|
|
92
92
|
overview: "agent create | kind",
|
|
93
93
|
group: "agents",
|
|
94
|
-
usage: ["agent create <name> --subscription|--api [--cap <dollars>]", "agent kind <agent> api|subscription"],
|
|
95
|
-
summary: "Make an Agent
|
|
94
|
+
usage: ["agent create <name> --subscription|--api [--harness claude|codex|grok|kimi] [--cap <dollars>]", "agent kind <agent> api|subscription", "agent harness <agent> claude|codex|grok|kimi|any"],
|
|
95
|
+
summary: "Make an Agent, or change its kind or harness.",
|
|
96
96
|
details: [
|
|
97
|
-
"An Agent is one
|
|
97
|
+
"An Agent is one harness on one project, with one tab. Two things are declared: its kind (api: the flock's provider key pays and the cap is the meter; subscription: your own plan logins, nothing charged) and its harness (claude, codex, grok, kimi, or any for scripts). The proxies enforce the harness: a Claude Agent's key cannot pass a Codex login through (403 wrong_harness), so Claude and Codex on one folder are two Agents and count as two.",
|
|
98
|
+
"A subscription Agent's cap is never touched, so tab list shows none; its tab is the kill switch.",
|
|
98
99
|
"Changing the kind applies to the next launch. tab claude reads it fresh every time.",
|
|
99
100
|
],
|
|
100
101
|
options: [
|
|
101
102
|
{ flag: "--subscription | --api", what: "the kind of the new Agent (api when neither is given)" },
|
|
102
103
|
{ flag: "--cap <dollars>", what: "the new Agent's cap; 50 when left out" },
|
|
104
|
+
{ flag: "--harness claude|codex|grok|kimi", what: "which harness this Agent is; any when left out" },
|
|
103
105
|
],
|
|
104
106
|
examples: [
|
|
105
107
|
{ cmd: "tab agent create billing-api --api --cap 20", what: "a metered Agent with a $20 cap" },
|
|
108
|
+
{ cmd: "tab agent create dash-f4f-codex --subscription --harness codex", what: "a Codex Agent for the dash folder" },
|
|
109
|
+
{ cmd: "tab agent harness dash-f4f codex", what: "tie an untied Agent to Codex" },
|
|
106
110
|
{ cmd: "tab agent create my-laptop --subscription", what: "an Agent for your own plan logins" },
|
|
107
111
|
{ cmd: "tab agent kind billing-api subscription", what: "switch an existing Agent" },
|
|
108
112
|
],
|
|
@@ -375,7 +379,8 @@ export const TAB_COMMANDS = [
|
|
|
375
379
|
/** What is what. Alphabetical in the docs; grouped by subject here. */
|
|
376
380
|
export const TAB_GLOSSARY = [
|
|
377
381
|
{ term: "Flock", meaning: "Your workspace: many Agents, one plan, one ledger. Each console login has its own." },
|
|
378
|
-
{ term: "Agent", meaning: "One
|
|
382
|
+
{ term: "Agent", meaning: "One harness on one project, such as dash-f4f-claude. It has exactly one tab, one kind, one harness, and its own keys, policies and history. Claude and Codex on the same folder are two Agents. Plans count Agents." },
|
|
383
|
+
{ term: "Harness", also: ["claude", "codex", "grok", "kimi", "any"], meaning: "Which coding agent an Agent is for. Set when the Agent is made; the proxies refuse a call from another harness before the vendor (403 wrong_harness). any is a script or unknown agent: metered only, never a subscription passthrough." },
|
|
379
384
|
{ term: "Tab", meaning: "An Agent's spend account: a hard dollar cap, a window, and a state, open or closed. Closing the tab is the kill switch." },
|
|
380
385
|
{ term: "Kind", also: ["api", "subscription"], meaning: "What pays for an Agent's tokens. api: the flock's provider key, every call charged to the cap. subscription: your own plan login, gated and recorded but never charged. Chosen when the Agent is made; it can be changed." },
|
|
381
386
|
{ term: "Cap", meaning: "The most an Agent may spend in one window, in dollars. Hard: a call that would pass it is refused before the provider. There is no overage." },
|
|
@@ -407,7 +412,6 @@ export const TAB_GLOSSARY = [
|
|
|
407
412
|
{ term: "Outside spend", meaning: "Money that did not go through a tab, read from provider admin APIs and cloud billing, so it can be told apart from metered spend. Team plan and up." },
|
|
408
413
|
{ term: "Ledger", meaning: "Every decision, one row per call: allowed, settled, refunded, blocked and why. Money is whole cents, never a fraction. If the ledger cannot be reached, no call goes out." },
|
|
409
414
|
{ term: "Fail closed", meaning: "When FlockTab cannot be sure a call fits (the ledger is down, the key is unknown, the unlock is missing) the call is refused rather than let through." },
|
|
410
|
-
{ term: "Harness", meaning: "The coding agent you run: Claude Code, Codex, Grok Build, Kimi Code, or any command. tab calls them agents in its output." },
|
|
411
415
|
{ term: ".flocktab", meaning: "A one-line file at a project's root naming the Agent that folder runs as. Safe to commit: it holds no key." },
|
|
412
416
|
{ term: "~/.flocktab", meaning: "This machine's tab folder: the session and Agent keys (config.json), the pool (pool.json), the self-hosted proxy's keys, log and binary, and the alias shims. Owner-only." },
|
|
413
417
|
];
|
|
@@ -419,6 +423,7 @@ export const TAB_ERRORS = [
|
|
|
419
423
|
{ status: 403, code: "model_blocked", when: "The model is not on the Agent's allowlist.", fix: "tab policy --models ..., or none to allow any." },
|
|
420
424
|
{ status: 403, code: "agent_frozen", when: "The Agent was frozen from the console.", fix: "Frozen by FlockTab support or an admin rule; write to support." },
|
|
421
425
|
{ status: 403, code: "not_subscription", when: "A passthrough (/t/...) call for an API-key Agent.", fix: "tab agent kind <agent> subscription, or run it metered." },
|
|
426
|
+
{ status: 403, code: "wrong_harness", when: "A passthrough for another harness than the Agent's: a Claude Agent's key with a Codex login, or an untied Agent with any login.", fix: "Run tab <harness> in the folder and pick or make that harness's Agent; or tab agent harness <agent> <harness>." },
|
|
422
427
|
{ status: 429, code: "velocity", when: "More calls this minute than the policy allows.", fix: "Wait, or tab policy --velocity N." },
|
|
423
428
|
{ status: 401, code: "key_revoked", when: "The key is unknown, rotated or missing.", fix: "tab key rotate mints a fresh one on this machine." },
|
|
424
429
|
{ status: 503, code: "ledger_unavailable", when: "FlockTab could not reach its ledger. Nothing was sent to the provider.", fix: "Retry. This is fail closed on purpose." },
|
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.15";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanamorilabs/tab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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.10",
|
|
26
|
+
"@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.10",
|
|
27
|
+
"@hanamorilabs/flocktab-proxy-linux-x64": "0.1.10",
|
|
28
|
+
"@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.10",
|
|
29
|
+
"@hanamorilabs/flocktab-proxy-win-x64": "0.1.10"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.18.6",
|