@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 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, writeProject } from "./project.js";
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
- const options = listed.agents;
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(`$${(Number(a.capCents) / 100).toFixed(2)} cap`)}${agents[a.slug] ? dim(" keyed here") : a.hasKey ? dim(" keyed elsewhere") : ""}`,
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 writeProject(await projectRoot(cwd), { agent: issued.agent.slug });
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 for this folder. */
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 agent = await resolveAgent(config, { pick: true });
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
- env.GROK_XAI_API_BASE_URL = `${passthroughBase(input.proxyUrl, input.presentedKey, "xai")}/v1`;
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") {
@@ -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), money(t.spentCents), money(t.capCents), pct(t.spentCents, t.capCents), t.window, t.projectName ?? dim("-")]), { right: [3, 4, 5] }));
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
- export async function readProject(cwd) {
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
- const agent = parsed && typeof parsed === "object" ? parsed.agent : undefined;
50
- if (typeof agent === "string" && /^[a-z0-9][a-z0-9-]*$/.test(agent))
51
- return { file, agent };
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.8";
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
- "Lists the flock's Agents (state, kind, cap) and lets you choose one or make a new one. A new Agent asks for its kind: API key or Subscription. The choice is written to a one-line .flocktab file at the git root (or this folder), and a key for that Agent is minted and kept on this machine.",
42
- "Running tab claude or tab codex in a folder with no .flocktab asks the same question, so tab use is only needed to change the answer.",
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 of a given kind, or change an Agent's kind.",
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 named worker with one tab. Its kind is the only thing anyone declares: api means the flock's provider key pays and the cap is the meter; subscription means its agents use your own plan logins and nothing is charged.",
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 named worker, such as billing-api or my-laptop. It has exactly one tab, one kind, and its own keys, policies and history. Plans count Agents." },
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.13";
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.13",
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.8",
26
- "@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.8",
27
- "@hanamorilabs/flocktab-proxy-linux-x64": "0.1.8",
28
- "@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.8",
29
- "@hanamorilabs/flocktab-proxy-win-x64": "0.1.8"
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",