@hanamorilabs/tab 0.1.20 → 0.1.21

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
@@ -34,7 +34,7 @@ import { describeLogin, localLogin } from "./logins.js";
34
34
  import { prepareCodexHome } from "./codex-home.js";
35
35
  import { addMember, loadPool, memberEnv, memberHome, POOL_VENDORS, poolVendorFor, prepareClaudeMember, prepareSharedSessions, limitsFor, removeMember, savePool, setGuard, setThreshold, thresholdFor, usage as quotaUsage } from "./pool.js";
36
36
  import { runPooled } from "./pool-run.js";
37
- import { claudeSettingsArg, installStatusLine, statusline } from "./statusline.js";
37
+ import { claudeSettingsArg, composedStatusline, installStatusLine } from "./statusline.js";
38
38
  import { renderPool } from "./pool-view.js";
39
39
  import { renderHelp } from "./help.js";
40
40
  import { findTabCommand } from "./tab-docs.js";
@@ -588,7 +588,10 @@ async function poolIdle(config, vendor, agentName) {
588
588
  const recent = Date.now() - 20_000;
589
589
  return !(await flockAccounts(config)).some((a) => a.provider === vendor && new Date(a.lastSeenAt).getTime() > recent);
590
590
  }
591
+ /** `FLOCKTAB_TIMING=1`: one line per launch step on stderr, with the seconds since the process began. */
592
+ const timing = process.env.FLOCKTAB_TIMING ? (label) => process.stderr.write(` ⏱ ${(process.uptime()).toFixed(2)}s ${label}\n`) : () => undefined;
591
593
  async function runAgent(name, argv) {
594
+ timing("runAgent");
592
595
  const observationFlags = takeObserveFlag(argv);
593
596
  const { args, noPool, asked, pinned } = takePoolFlags(observationFlags.args);
594
597
  if (observationFlags.observe && name !== "claude" && name !== "codex") {
@@ -598,12 +601,15 @@ async function runAgent(name, argv) {
598
601
  const config = await ensureLogin();
599
602
  if (!config)
600
603
  return 2;
604
+ timing("login");
601
605
  // Hosted whoami below proves readiness and current Agent kind in one round trip.
602
606
  if (config.mode === "self-hosted" && !(await ensureProxy(config)))
603
607
  return 1;
608
+ timing("proxy");
604
609
  const agent = await resolveAgent(config, { harness: harnessOf(name) });
605
610
  if (!agent)
606
611
  return 1;
612
+ timing("agent");
607
613
  // The Agent's kind decides: a subscription Agent's harness brings its own
608
614
  // login (Claude Code its Claude one, Codex ChatGPT, Grok SuperGrok, Kimi
609
615
  // Code its Kimi plan), read fresh on every launch so a change in the
@@ -611,6 +617,7 @@ async function runAgent(name, argv) {
611
617
  // consumer plan, is metered with the flock's key.
612
618
  const vendor = subscriptionVendorFor(name);
613
619
  const me = await whoami(config.proxyUrl, { key: agent.key, unlock: config.unlock });
620
+ timing("whoami");
614
621
  if (config.mode === "hosted" && "error" in me) {
615
622
  fail(me.error);
616
623
  return 1;
@@ -640,6 +647,7 @@ async function runAgent(name, argv) {
640
647
  const { spec, env } = envFor({ name, proxyUrl: config.proxyUrl, presentedKey: key, auth });
641
648
  const poolVendor = auth === "subscription" ? poolVendorFor(name) : undefined;
642
649
  const pool = poolVendor ? await loadPool() : undefined;
650
+ timing("pool");
643
651
  const members = (poolVendor && pool?.members[poolVendor]) || [];
644
652
  const pooled = members.length > 0 && !noPool;
645
653
  if (asked && !pooled) {
@@ -697,6 +705,7 @@ async function runAgent(name, argv) {
697
705
  });
698
706
  return { done, stop: () => void child.kill("SIGTERM") };
699
707
  };
708
+ timing("home");
700
709
  if (!pooled || !poolVendor || !pool)
701
710
  return launch(args, env).done.finally(() => observation?.stop().catch(() => undefined));
702
711
  // Codex members each need their own home written before the first launch;
@@ -713,14 +722,20 @@ async function runAgent(name, argv) {
713
722
  await prepareCodexHome({ baseDir: member.dir, proxyUrl: config.proxyUrl, presentedKey: key, mode: config.mode, auth, userCodexHome: path.join(member.dir, "no-personal-login") });
714
723
  }
715
724
  }
725
+ timing("members");
716
726
  return runPooled(args, {
717
727
  harness: name,
718
728
  limits: limitsFor(pool, poolVendor),
719
729
  swap: pool.swap,
720
730
  ...(pinned ? { pinned } : {}),
721
- standings: () => poolStandings(config, poolVendor, members),
731
+ standings: () => poolStandings(config, poolVendor, members).finally(() => timing("standings")),
722
732
  idle: () => poolIdle(config, poolVendor, agent.agentName),
723
- start: (standing, launchArgs) => launch(launchArgs, { ...env, ...memberEnv(poolVendor, standing.member) }),
733
+ start: (standing, launchArgs) => {
734
+ timing("launch");
735
+ // The status line says which pool login this run is on; the harness passes its environment to it.
736
+ const poolEnv = { FLOCKTAB_POOL_LOGIN: standing.member.name, FLOCKTAB_POOL_SIZE: String(members.length) };
737
+ return launch(launchArgs, { ...env, ...memberEnv(poolVendor, standing.member), ...poolEnv });
738
+ },
724
739
  wait: (ms, until) => new Promise((resolve) => {
725
740
  const timer = setTimeout(() => resolve(true), ms);
726
741
  void until.then(() => {
@@ -944,7 +959,7 @@ async function statuslineInstall(args) {
944
959
  if (!/^y(es)?$/i.test(go))
945
960
  return 1;
946
961
  const done = await installStatusLine(vendor, home, process.env.FLOCKTAB_DEV ? "tabdev" : "tab");
947
- ok(done === "already" ? `${file} already has a status line; left as it is.` : `Added. It shows on the next ${word} launch.`);
962
+ ok(done === "already" ? `${file} already runs tab's status line.` : done === "composed" ? `Added after yours: the file is backed up next to it, and your own line still runs first. It shows on the next ${word} launch.` : `Added. It shows on the next ${word} launch.`);
948
963
  return 0;
949
964
  }
950
965
  /** `tab use [claude|codex|grok|kimi]`: pick or change the Agent this folder runs a harness as; bare, the folder's default. */
@@ -1187,7 +1202,9 @@ async function main(argv) {
1187
1202
  console.log("FlockTab · not logged in");
1188
1203
  return 0;
1189
1204
  }
1190
- console.log(await statusline(config, rest[0] && harnessOf(rest[0]) !== "any" ? harnessOf(rest[0]) : "any"));
1205
+ // The harness hands its state on stdin; the person's own status line gets the same, and runs first.
1206
+ const stdin = process.stdin.isTTY ? "" : await new Promise((resolve) => { let d = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (c) => { d += c; }); process.stdin.on("end", () => resolve(d)); process.stdin.on("error", () => resolve(d)); setTimeout(() => resolve(d), 500); });
1207
+ console.log(await composedStatusline(config, rest[0] && harnessOf(rest[0]) !== "any" ? harnessOf(rest[0]) : "any", stdin));
1191
1208
  return 0;
1192
1209
  }
1193
1210
  case "log":
@@ -56,6 +56,16 @@ export function codexSubscriptionParts(proxyUrl, presentedKey, model) {
56
56
  `requires_openai_auth = true`,
57
57
  `supports_websockets = false`,
58
58
  ].join("\n"),
59
+ // A session that started on an API-key Agent remembers one of the metered provider ids; on
60
+ // this Agent those names run the login through the tab as well.
61
+ ...["hosted", "self-hosted"].map((mode) => [
62
+ `[model_providers.${JSON.stringify(codexProviderId(mode))}]`,
63
+ `name = ${JSON.stringify(codexProviderId(mode))}`,
64
+ `base_url = ${JSON.stringify(codex)}`,
65
+ `wire_api = "responses"`,
66
+ `requires_openai_auth = true`,
67
+ `supports_websockets = false`,
68
+ ].join("\n")),
59
69
  ],
60
70
  };
61
71
  }
@@ -111,6 +121,16 @@ export function codexKeyParts(proxyUrl, model, mode = "hosted") {
111
121
  `env_key = "OPENAI_API_KEY"`,
112
122
  `wire_api = "responses"`,
113
123
  ].join("\n"),
124
+ // A session that ran on a subscription Agent remembers the `flocktab` provider. Here that name
125
+ // is the metered provider under another id, so the session resumes on this Agent's key
126
+ // instead of being refused at the passthrough.
127
+ [
128
+ `[model_providers.${CODEX_TAB_PROVIDER}]`,
129
+ `name = "OpenAI through FlockTab"`,
130
+ `base_url = ${JSON.stringify(`${proxyUrl}/v1`)}`,
131
+ `env_key = "OPENAI_API_KEY"`,
132
+ `wire_api = "responses"`,
133
+ ].join("\n"),
114
134
  ],
115
135
  };
116
136
  }
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.14";
16
+ export const PROXY_VERSION = "0.1.16";
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")
@@ -5,7 +5,9 @@
5
5
  * from the console with a short cache, so a status bar that polls every
6
6
  * few seconds costs one request a minute.
7
7
  */
8
- import { mkdir, readFile, writeFile } from "node:fs/promises";
8
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { spawn } from "node:child_process";
9
11
  import path from "node:path";
10
12
  import { configDir } from "./config.js";
11
13
  import { readProject } from "./project.js";
@@ -19,25 +21,38 @@ function money(cents) {
19
21
  const n = BigInt(cents);
20
22
  return `$${n / 100n}.${(n % 100n).toString().padStart(2, "0")}`;
21
23
  }
22
- /** "5h 36% · 7d 29%", shortest window first; nothing when the vendor said nothing yet. */
24
+ /**
25
+ * "5h 36% · 7d 29%", shortest window first; nothing before the first call. A vendor that
26
+ * names the login but reports no usage on the wire (SuperGrok, Kimi) gets the login and says so.
27
+ */
23
28
  export function planLine(accounts, now = new Date()) {
24
29
  // The account most recently seen with quota: the login in use.
25
30
  const withQuota = accounts.filter((a) => a.quota.length > 0);
26
- const account = withQuota[0];
31
+ const account = withQuota[0] ?? accounts[0];
27
32
  if (!account)
28
33
  return undefined;
34
+ if (account.quota.length === 0)
35
+ return `${account.email ?? `account ${account.externalId.slice(0, 8)}`} · usage not reported by the vendor`;
29
36
  const windows = [...account.quota]
30
37
  .filter((w) => !w.resetsAt || new Date(w.resetsAt).getTime() > now.getTime())
31
38
  .sort((a, b) => windowMinutes(a.window) - windowMinutes(b.window));
32
39
  const who = account.email ?? `account ${account.externalId.slice(0, 8)}`;
33
40
  return `${who} · ${windows.length === 0 ? "windows reset" : windows.map((w) => `${w.window} ${Math.round(w.usedPct)}%`).join(" · ")}`;
34
41
  }
35
- /** The line itself, from what the console said. */
36
- export function statusText(info, now = new Date()) {
42
+ /** The line itself, from what the console said, and the pool login this run is on when there is one. */
43
+ export function statusText(info, now = new Date(), pool) {
37
44
  const { tab } = info;
38
45
  const closed = tab.state !== "open";
39
46
  const head = `${tab.name}${closed ? " CLOSED" : ""}`;
40
47
  if (tab.kind === "subscription") {
48
+ if (pool) {
49
+ // The pool picked this login for the run: its own windows, not another login's that the
50
+ // console saw last on this Agent; the login is said once.
51
+ const mine = (info.accounts ?? []).filter((a) => a.email === pool.login || a.externalId === pool.login);
52
+ const plan = planLine(mine, now);
53
+ const windows = plan && plan.startsWith(`${pool.login} · `) ? plan.slice(pool.login.length + 3) : plan;
54
+ return `${head} · pool ${pool.login} (${pool.size}) · ${windows ?? "no call yet on this login"}`;
55
+ }
41
56
  return `${head} · ${planLine(info.accounts ?? [], now) ?? "no call yet"}`;
42
57
  }
43
58
  const spent = BigInt(tab.spentCents);
@@ -45,13 +60,22 @@ export function statusText(info, now = new Date()) {
45
60
  const pct = cap > 0n ? Number((spent * 100n) / cap) : 0;
46
61
  return `${head} · ${money(tab.spentCents)} of ${money(tab.capCents)} / ${tab.window} (${pct}%)`;
47
62
  }
63
+ /** The pool login a pooled launch put in the harness's environment, which it passes on to us. */
64
+ export function poolFromEnv(env = process.env) {
65
+ const login = env.FLOCKTAB_POOL_LOGIN?.trim();
66
+ if (!login)
67
+ return undefined;
68
+ const size = Number(env.FLOCKTAB_POOL_SIZE);
69
+ return { login, size: Number.isFinite(size) && size > 0 ? size : 1 };
70
+ }
48
71
  /** The tab of this folder's Agent for `harness`, from a one-minute cache, else the console. */
49
72
  export async function statusline(config, harness, cwd = process.cwd(), env = process.env) {
50
73
  const project = await readProject(cwd, harness);
51
74
  if (!project)
52
75
  return "FlockTab · no Agent here (tab use)";
53
76
  const login = config.agents?.[project.agent];
54
- const cacheFile = path.join(configDir(env), "cache", `status-${project.agent}.json`);
77
+ const pool = poolFromEnv(env);
78
+ const cacheFile = path.join(configDir(env), "cache", `status-${project.agent}${pool ? `-${pool.login.replace(/[^a-z0-9]+/gi, "_")}` : ""}.json`);
55
79
  try {
56
80
  const cached = JSON.parse(await readFile(cacheFile, "utf8"));
57
81
  if (Date.now() - cached.at < CACHE_MS)
@@ -63,7 +87,7 @@ export async function statusline(config, harness, cwd = process.cwd(), env = pro
63
87
  let text;
64
88
  try {
65
89
  const info = await api(config)("GET", `/api/cli/tabs/${encodeURIComponent(project.agent)}`);
66
- text = `FlockTab · ${statusText(info)}`;
90
+ text = `FlockTab · ${statusText(info, new Date(), poolFromEnv(env))}`;
67
91
  }
68
92
  catch {
69
93
  return `FlockTab · ${login?.agentName ?? project.agent} · console unreachable`;
@@ -77,6 +101,83 @@ export async function statusline(config, harness, cwd = process.cwd(), env = pro
77
101
  }
78
102
  return text;
79
103
  }
104
+ /**
105
+ * The status line command the person had before tab, if any. Their own files are read, never
106
+ * changed: Claude Code's `settings.local.json` then `settings.json`, Grok Build's `config.toml`,
107
+ * Kimi Code's `tui.toml`. When `tab statusline install` took the slot in a Grok or Kimi config,
108
+ * the original command it set aside under `<tab home>/statusline/` is what counts.
109
+ */
110
+ export async function theirStatusCommand(harness, env = process.env) {
111
+ const ours = (cmd) => /\btab(dev)? statusline\b/.test(cmd);
112
+ try {
113
+ const saved = (await readFile(path.join(configDir(env), "statusline", `${harness}.original`), "utf8")).trim();
114
+ if (saved && !ours(saved))
115
+ return saved;
116
+ }
117
+ catch {
118
+ // Nothing set aside.
119
+ }
120
+ const home = env.HOME || homedir();
121
+ if (harness === "claude") {
122
+ for (const file of ["settings.local.json", "settings.json"]) {
123
+ try {
124
+ const parsed = JSON.parse(await readFile(path.join(home, ".claude", file), "utf8"));
125
+ const cmd = parsed.statusLine?.type === "command" ? parsed.statusLine.command?.trim() : undefined;
126
+ if (cmd && !ours(cmd))
127
+ return cmd;
128
+ }
129
+ catch {
130
+ // No such file, or not JSON.
131
+ }
132
+ }
133
+ return undefined;
134
+ }
135
+ const file = harness === "grok" ? path.join(home, ".grok", "config.toml") : harness === "kimi" ? path.join(home, ".kimi-code", "tui.toml") : undefined;
136
+ if (!file)
137
+ return undefined;
138
+ try {
139
+ const cmd = statusCommandIn(await readFile(file, "utf8"), harness === "grok" ? "[ui.status_line]" : "[status_line]");
140
+ return cmd && !ours(cmd) ? cmd : undefined;
141
+ }
142
+ catch {
143
+ return undefined;
144
+ }
145
+ }
146
+ /** The `command = "…"` of one TOML table, read by line so no TOML parser is needed. */
147
+ export function statusCommandIn(toml, marker) {
148
+ const lines = toml.split("\n");
149
+ const start = lines.findIndex((l) => l.trim() === marker);
150
+ if (start < 0)
151
+ return undefined;
152
+ for (const line of lines.slice(start + 1)) {
153
+ if (/^\s*\[/.test(line))
154
+ break;
155
+ const m = /^\s*command\s*=\s*"((?:[^"\\]|\\.)*)"/.exec(line);
156
+ if (m)
157
+ return m[1].replace(/\\"/g, '"');
158
+ }
159
+ return undefined;
160
+ }
161
+ /** Run the person's own status command with the same stdin the harness gave us; all its lines, or nothing. */
162
+ export function runTheirs(command, stdin, env = process.env) {
163
+ return new Promise((resolve) => {
164
+ const child = spawn("/bin/sh", ["-c", command], { env, stdio: ["pipe", "pipe", "ignore"] });
165
+ let out = "";
166
+ const timer = setTimeout(() => { child.kill("SIGKILL"); resolve(undefined); }, 4000);
167
+ child.stdout.on("data", (d) => { out += d.toString(); });
168
+ child.on("error", () => { clearTimeout(timer); resolve(undefined); });
169
+ child.on("close", () => { clearTimeout(timer); const text = out.replace(/\s+$/, ""); resolve(text.trim() ? text : undefined); });
170
+ child.stdin.end(stdin);
171
+ });
172
+ }
173
+ /** tab's line added after the person's own (every line of it), never instead of it. */
174
+ export async function composedStatusline(config, harness, stdin, env = process.env) {
175
+ const mine = statusline(config, harness, process.cwd(), env);
176
+ const theirs = await theirStatusCommand(harness === "any" ? "claude" : harness, env);
177
+ const output = theirs ? await runTheirs(theirs, stdin, env) : undefined;
178
+ const ours = await mine;
179
+ return output ? `${output}\n${ours}` : ours;
180
+ }
80
181
  /**
81
182
  * How each harness is told to run `tab statusline`. Claude Code takes it on
82
183
  * the command line, so every `tab claude` has it. Grok Build and Kimi Code
@@ -95,10 +196,15 @@ export function grokStatusToml(tabBin = "tab") {
95
196
  export function kimiStatusToml(tabBin = "tab") {
96
197
  return `\n[status_line]\ncommand = "${tabBin} statusline kimi"\n`;
97
198
  }
98
- /** Add the status line to a harness home that does not have one. Returns what was done. */
99
- export async function installStatusLine(vendor, home, tabBin = "tab") {
199
+ /**
200
+ * Put tab's status line into a harness home. A status line already there is not lost: the file is
201
+ * copied to `<file>.bak-<date>` first and the original command is set aside under
202
+ * `<tab home>/statusline/<harness>.original`, which `tab statusline` runs and adds its own line to.
203
+ */
204
+ export async function installStatusLine(vendor, home, tabBin = "tab", tabHome = configDir()) {
100
205
  const file = vendor === "xai" ? path.join(home, "config.toml") : path.join(home, "tui.toml");
101
206
  const marker = vendor === "xai" ? "[ui.status_line]" : "[status_line]";
207
+ const harness = vendor === "xai" ? "grok" : "kimi";
102
208
  let current = "";
103
209
  try {
104
210
  current = await readFile(file, "utf8");
@@ -106,9 +212,25 @@ export async function installStatusLine(vendor, home, tabBin = "tab") {
106
212
  catch {
107
213
  // No file yet.
108
214
  }
109
- if (current.includes(marker))
110
- return "already";
111
215
  await mkdir(home, { recursive: true, mode: 0o700 });
112
- await writeFile(file, `${current.replace(/\s*$/, "\n")}${vendor === "xai" ? grokStatusToml(tabBin) : kimiStatusToml(tabBin)}`);
113
- return "written";
216
+ if (!current.includes(marker)) {
217
+ await writeFile(file, `${current.replace(/\s*$/, "\n")}${vendor === "xai" ? grokStatusToml(tabBin) : kimiStatusToml(tabBin)}`);
218
+ return "written";
219
+ }
220
+ const existing = statusCommandIn(current, marker);
221
+ if (!existing || /\btab(dev)? statusline\b/.test(existing))
222
+ return "already";
223
+ // Theirs stays: backed up whole, its command set aside, and run first by tab's line.
224
+ const stamp = new Date().toISOString().slice(0, 10);
225
+ await copyFile(file, `${file}.bak-${stamp}`);
226
+ await mkdir(path.join(tabHome, "statusline"), { recursive: true, mode: 0o700 });
227
+ await writeFile(path.join(tabHome, "statusline", `${harness}.original`), `${existing}\n`, { mode: 0o600 });
228
+ const lines = current.split("\n");
229
+ const start = lines.findIndex((l) => l.trim() === marker);
230
+ for (let i = start + 1; i < lines.length && !/^\s*\[/.test(lines[i]); i++) {
231
+ if (/^\s*command\s*=/.test(lines[i]))
232
+ lines[i] = `command = "${tabBin} statusline ${harness}"`;
233
+ }
234
+ await writeFile(file, lines.join("\n"));
235
+ return "composed";
114
236
  }
package/dist/tab-docs.js CHANGED
@@ -279,8 +279,8 @@ export const TAB_COMMANDS = [
279
279
  usage: ["statusline [claude|codex|grok|kimi]", "statusline install grok|kimi"],
280
280
  summary: "One line for a harness's status bar: the Agent and its tab, or the plan's windows.",
281
281
  details: [
282
- "Prints what this folder runs the harness as, then either spent of cap for an API-key Agent or, for a subscription, the login in use and each window's usage as the vendor last reported it (5h 36% · 7d 29%). Read from the console at most once a minute, so a status bar that refreshes every few seconds costs nothing.",
283
- "Claude Code shows it on every tab claude, passed on the command line so no file of yours is touched (your own --settings wins). Grok Build and Kimi Code read a status line only from their config; a pool member gets it (that home is tab's), and tab statusline install grok|kimi adds it to your own config after asking. Codex takes no command and already shows its own limits.",
282
+ "Prints what this folder runs the harness as, then either spent of cap for an API-key Agent or, for a subscription, the login in use and each window's usage as the vendor last reported it (5h 36% · 7d 29%). On a pooled run it names the pool login the run is on and how many logins the pool has (pool me@x.com (4)). Read from the console at most once a minute, so a status bar that refreshes every few seconds costs nothing.",
283
+ "Your own status line stays. tab runs it first (Claude Code's from settings.local.json or settings.json, Grok's and Kimi's from their config, with the same stdin the harness gives) and adds its own line after it, every line of yours kept. Claude Code shows the pair on every tab claude, passed on the command line so no file of yours is touched (your own --settings wins). Grok Build and Kimi Code read a status line only from their config; a pool member gets it (that home is tab's), and tab statusline install grok|kimi puts it in your own config after asking: the file is backed up next to it first and a command already there is set aside and keeps running first. Codex takes no command and already shows its own limits.",
284
284
  ],
285
285
  examples: [
286
286
  { cmd: "tab statusline claude", what: "the line, as Claude Code's status bar would show it" },
@@ -343,15 +343,18 @@ export const TAB_COMMANDS = [
343
343
  group: "watch",
344
344
  usage: ["outside"],
345
345
  summary: "The outside-spend connections and their totals per project.",
346
- details: ["Connections are made in the console (Control, Outside spend): provider admin APIs and cloud billing. They answer whether a dollar went through a tab or around it."],
346
+ details: [
347
+ "Connections are made in the console (Control, Outside spend): provider admin APIs and cloud billing. They answer whether a dollar went through a tab or around it.",
348
+ "The console pulls every connected source on its own, once an hour; the last sync shows per connection.",
349
+ ],
347
350
  see: ["spend", "project"],
348
351
  },
349
352
  {
350
353
  name: "live",
351
354
  group: "watch",
352
355
  usage: ["live"],
353
- summary: "Who is working right now.",
354
- details: ["Each Agent's pulse (working, recent, idle, stopped), calls per minute and last decision."],
356
+ summary: "Who is active right now.",
357
+ details: ["Each Agent's pulse (active, quiet, idle, stopped), calls per minute and last decision. Active means a call was admitted in the last minute. Quiet means calls in the last five minutes, but not the last minute."],
355
358
  options: [{ flag: "--watch N", what: "refresh every N seconds" }],
356
359
  },
357
360
  {
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.20";
2
+ export const TAB_VERSION = "0.1.21";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanamorilabs/tab",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
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.14",
26
- "@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.14",
27
- "@hanamorilabs/flocktab-proxy-linux-x64": "0.1.14",
28
- "@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.14",
29
- "@hanamorilabs/flocktab-proxy-win-x64": "0.1.14"
25
+ "@hanamorilabs/flocktab-proxy-darwin-arm64": "0.1.16",
26
+ "@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.16",
27
+ "@hanamorilabs/flocktab-proxy-linux-x64": "0.1.16",
28
+ "@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.16",
29
+ "@hanamorilabs/flocktab-proxy-win-x64": "0.1.16"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.18.6",