@hanamorilabs/tab 0.1.8 → 0.1.9

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 CHANGED
@@ -94,6 +94,8 @@ tab accounts accounts your subscription Agents were seen on: plan, price
94
94
  tab alias setup <name>... make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
95
95
  tab alias remove <name>... undo that; `tab alias list` shows them
96
96
  tab status flock, folder Agent, proxy health, provider key state
97
+ tab log [-f] [--all] everything through the tab for this machine's Agents, one line per call
98
+ tab log --proxy [-f] self-hosted: the local proxy's own log (cached tokens, milliseconds)
97
99
  tab version tab and proxy versions
98
100
  tab up | down self-hosted: start or stop the local proxy
99
101
  tab update the newest tab and proxy from npm; restarts a running local proxy
package/dist/cli.js CHANGED
@@ -37,7 +37,7 @@ import { reportFolder } from "./folder.js";
37
37
  import { listAliases, pathLine, pathWithoutShims, removeAlias, shimDir, shimDirOnPath, validAliasName, writeAlias } from "./alias.js";
38
38
  import * as manage from "./manage.js";
39
39
  import { agentNameFor, projectRoot, readProject, writeProject } from "./project.js";
40
- import { downloadProxy, hasProxyEnv, packagedBinPath, proxyBinPath, proxyEnvPath, proxyInstalled, proxyLogPath, PROXY_VERSION, startProxy, stopProxy, tailProxyLog, writeProxyEnv, } from "./proxy-bin.js";
40
+ import { downloadProxy, hasProxyEnv, packagedBinPath, proxyBinPath, proxyEnvPath, proxyInstalled, proxyLogPath, hasProxyLog, PROXY_VERSION, startProxy, stopProxy, tailProxyLog, writeProxyEnv, } from "./proxy-bin.js";
41
41
  import { proxyHealth, waitHealthy } from "./selfhost.js";
42
42
  import { TAB_VERSION } from "./version.js";
43
43
  const say = (text) => console.error(text);
@@ -62,6 +62,8 @@ function usage() {
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"),
65
+ cmd("log [-f] [--all]", "everything through the tab on this machine's Agents, one line per call; -f follows"),
66
+ cmd("log --proxy [-f]", "self-hosted: the local proxy's own log (cached tokens, milliseconds; no keys, logins or prompts)"),
65
67
  cmd("version", "tab and proxy versions"),
66
68
  cmd("list", "every Agent: state, spent of cap, window, project"),
67
69
  cmd("close | open [agent]", "the kill switch; omit the Agent for this folder's"),
@@ -525,7 +527,24 @@ async function runAgent(name, args) {
525
527
  // consumer plan, is metered with the flock's key.
526
528
  const vendor = subscriptionVendorFor(name);
527
529
  const me = await whoami(config.proxyUrl, { key: agent.key, unlock: config.unlock });
528
- const kind = !("error" in me) && me.kind === "subscription" ? "subscription" : "api";
530
+ let kind = !("error" in me) && me.kind === "subscription" ? "subscription" : "api";
531
+ // A proxy older than 0.1.7 answers whoami without the kind. Guessing api
532
+ // would hand a subscription Agent's harness the tab key, so ask the console.
533
+ const { consoleUrl, token } = config;
534
+ if (!("error" in me) && me.kind === undefined && config.mode === "self-hosted") {
535
+ // Not restarted here: other harnesses may be streaming through it.
536
+ warn(`The local proxy is older than this tab. When nothing is running through it: ${bold("tab down && tab up")}`);
537
+ }
538
+ if (consoleUrl && token && ("error" in me || me.kind === undefined)) {
539
+ try {
540
+ const listed = await listAgents(consoleUrl, token);
541
+ if (listed.agents.find((a) => a.id === agent.agentId)?.kind === "subscription")
542
+ kind = "subscription";
543
+ }
544
+ catch {
545
+ // Console unreachable: stay on what the proxy said.
546
+ }
547
+ }
529
548
  const auth = kind === "subscription" && vendor ? "subscription" : "key";
530
549
  // On a subscription there is no provider key to unlock; the bare key rides in the URL.
531
550
  const key = auth === "subscription" ? agent.key : presentedKey({ key: agent.key, unlock: config.unlock });
@@ -695,6 +714,29 @@ ${installed}` : "Updated.");
695
714
  await stopProxy();
696
715
  return (await bringUp(config.proxyUrl)) ? 0 : 1;
697
716
  }
717
+ /**
718
+ * `tab logs`: the local proxy's log. One line per call: kind, vendor, Agent,
719
+ * model, outcome, status, tokens (cached apart) and duration. The proxy never
720
+ * writes a key, a login, a prompt or an email there. `-f` follows it.
721
+ */
722
+ async function logs(args) {
723
+ const flags = manage.parseFlags(args);
724
+ const follow = args.includes("-f") || flags.opts.follow !== undefined;
725
+ const lines = Math.min(5000, Math.max(1, Number(flags.opts.lines ?? 50) || 50));
726
+ const file = proxyLogPath();
727
+ if (!(await hasProxyLog())) {
728
+ warn(`No local proxy log yet (${file}). It is written by the self-hosted proxy: ${bold("tab up")}.`);
729
+ return 1;
730
+ }
731
+ const [cmd, cmdArgs] = process.platform === "win32"
732
+ ? ["powershell", ["-NoProfile", "-Command", `Get-Content -Path '${file}' -Tail ${lines}${follow ? " -Wait" : ""}`]]
733
+ : ["tail", [`-n`, String(lines), ...(follow ? ["-f"] : []), file]];
734
+ const child = spawn(cmd, cmdArgs, { stdio: "inherit" });
735
+ return new Promise((resolve) => {
736
+ child.on("error", () => resolve(1));
737
+ child.on("close", (code) => resolve(code ?? 0));
738
+ });
739
+ }
698
740
  async function selfHosted(action) {
699
741
  const config = await loadConfig();
700
742
  const proxyUrl = config?.mode === "self-hosted" ? config.proxyUrl : LOCAL_PROXY;
@@ -751,6 +793,8 @@ async function managed(command, rest) {
751
793
  return await manage.outside(config, flags);
752
794
  case "live":
753
795
  return await manage.live(config, flags);
796
+ case "log":
797
+ return await manage.log(config, manage.parseFlags(rest.filter((a) => a !== "-f")), rest.includes("-f") || flags.opts.follow !== undefined);
754
798
  case "accounts":
755
799
  case "quota":
756
800
  return await manage.accounts(config, flags);
@@ -812,6 +856,12 @@ async function main(argv) {
812
856
  return managed(command, rest);
813
857
  case "status":
814
858
  return status();
859
+ case "log":
860
+ if (rest.includes("--proxy"))
861
+ return logs(rest.filter((a) => a !== "--proxy"));
862
+ return managed("log", rest);
863
+ case "logs":
864
+ return logs(rest);
815
865
  case "up":
816
866
  return selfHosted("up");
817
867
  case "down":
package/dist/clients.js CHANGED
@@ -107,10 +107,14 @@ export function envFor(input) {
107
107
  return;
108
108
  }
109
109
  env.ANTHROPIC_BASE_URL = input.proxyUrl;
110
- env.ANTHROPIC_API_KEY = input.presentedKey;
111
- // Claude Code prefers an auth token when one is set; make sure a stale
112
- // one cannot win over the tab key.
113
- delete env.ANTHROPIC_AUTH_TOKEN;
110
+ // The tab key goes in as the auth token, not the API key. Claude Code
111
+ // asks a person to approve an `ANTHROPIC_API_KEY` once and remembers a
112
+ // No, after which it silently uses its own Claude login and the call
113
+ // arrives without the tab key. `ANTHROPIC_AUTH_TOKEN` is checked first,
114
+ // never asks, and switches the Claude login off for the session. It is
115
+ // sent as `Authorization: Bearer`, which the proxy reads like `x-api-key`.
116
+ env.ANTHROPIC_AUTH_TOKEN = input.presentedKey;
117
+ delete env.ANTHROPIC_API_KEY;
114
118
  };
115
119
  const setOpenAI = () => {
116
120
  if (auth === "subscription") {
package/dist/manage.js CHANGED
@@ -298,6 +298,55 @@ export async function key(config, flags) {
298
298
  (flags.opts.show !== undefined ? `\n${issued.key}` : `\n${dim("Add --show to print it once.")}`));
299
299
  return 0;
300
300
  }
301
+ /** One call, one condensed line: when, which Agent, how it went, what it cost or was worth, what it used. */
302
+ export function logLine(r) {
303
+ const shadow = r.status === "shadow";
304
+ const status = shadow ? "SUBSCRIPTION" : (r.status ?? r.decision).toUpperCase();
305
+ const paint = status === "BLOCKED" || status === "CLOSED" ? red : status === "PENDING" ? yellow : green;
306
+ return `${dim(r.at)} ${bold(r.agent)} ${paint(status)} ${money(r.cents)}${shadow ? dim(" at list") : ""} ${dim(r.reason)}`;
307
+ }
308
+ /**
309
+ * `tab log`: everything that went through the tab for the Agents on this
310
+ * machine, one condensed line per call, hosted or self-hosted alike because
311
+ * it is the ledger's own record. `-f` follows it; `--all` is the whole flock.
312
+ */
313
+ export async function log(config, flags, follow) {
314
+ const call = api(config);
315
+ const limit = Math.min(500, Math.max(1, Number(flags.opts.lines ?? 30) || 30));
316
+ const mine = new Set(Object.values(config.agents ?? {}).map((a) => a.agentName.toLowerCase()));
317
+ const wanted = (r) => flags.opts.all !== undefined || mine.size === 0 || mine.has(r.agent.toLowerCase());
318
+ const fetchRows = async () => (await call("GET", `/api/cli/ledger?limit=${flags.opts.all !== undefined ? limit : limit * 4}`)).rows.filter(wanted).slice(0, limit);
319
+ const first = await fetchRows();
320
+ if (flags.json && !follow) {
321
+ out(JSON.stringify({ rows: first }, null, 2));
322
+ return 0;
323
+ }
324
+ if (first.length === 0 && !follow)
325
+ out(dim("Nothing yet. Run tab claude or tab codex in a project folder."));
326
+ const seen = new Set();
327
+ for (const r of [...first].reverse()) {
328
+ seen.add(r.id);
329
+ out(flags.json ? JSON.stringify(r) : logLine(r));
330
+ }
331
+ if (!follow)
332
+ return 0;
333
+ for (;;) {
334
+ await new Promise((resolve) => setTimeout(resolve, 2000));
335
+ let rows;
336
+ try {
337
+ rows = await fetchRows();
338
+ }
339
+ catch {
340
+ continue;
341
+ }
342
+ for (const r of [...rows].reverse()) {
343
+ if (seen.has(r.id))
344
+ continue;
345
+ seen.add(r.id);
346
+ out(flags.json ? JSON.stringify(r) : logLine(r));
347
+ }
348
+ }
349
+ }
301
350
  export async function ledger(config, flags) {
302
351
  const call = api(config);
303
352
  const params = new URLSearchParams();
@@ -311,9 +360,11 @@ export async function ledger(config, flags) {
311
360
  emit(flags.json, { timezone, rows: list }, () => list.length === 0
312
361
  ? dim("No decisions yet.")
313
362
  : table(["time", "agent", "action", "amount", "status", "reason"], list.map((r) => {
314
- const status = (r.status ?? r.decision).toUpperCase();
363
+ // A subscription call is recorded at list price; nothing was held, so there is no "of".
364
+ const shadow = r.status === "shadow";
365
+ const status = shadow ? "SUBSCRIPTION" : (r.status ?? r.decision).toUpperCase();
315
366
  const paint = status === "BLOCKED" || status === "CLOSED" ? red : status === "PENDING" ? yellow : green;
316
- const held = r.reservedCents && r.reservedCents !== r.cents ? dim(` of ${money(r.reservedCents)}`) : "";
367
+ const held = !shadow && r.reservedCents && r.reservedCents !== r.cents ? dim(` of ${money(r.reservedCents)}`) : "";
317
368
  return [r.at, r.agent, r.action.length > 28 ? `${r.action.slice(0, 27)}…` : r.action, `${money(r.cents)}${held}`, paint(status), r.reason];
318
369
  }), { right: [3] }) + `\n${dim(`times in ${timezone}`)}`);
319
370
  return 0;
package/dist/proxy-bin.js CHANGED
@@ -8,12 +8,12 @@
8
8
  * runtime beyond the binary itself.
9
9
  */
10
10
  import { spawn } from "node:child_process";
11
- import { chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
11
+ import { access, chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
12
12
  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.5";
16
+ export const PROXY_VERSION = "0.1.7";
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")
@@ -214,3 +214,13 @@ export async function tailProxyLog(lines = 8, env = process.env) {
214
214
  return [];
215
215
  }
216
216
  }
217
+ /** Whether the local proxy has written a log yet. */
218
+ export async function hasProxyLog(env = process.env) {
219
+ try {
220
+ await access(proxyLogPath(env));
221
+ return true;
222
+ }
223
+ catch {
224
+ return false;
225
+ }
226
+ }
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.8";
2
+ export const TAB_VERSION = "0.1.9";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanamorilabs/tab",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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.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"
25
+ "@hanamorilabs/flocktab-proxy-darwin-arm64": "0.1.7",
26
+ "@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.7",
27
+ "@hanamorilabs/flocktab-proxy-linux-x64": "0.1.7",
28
+ "@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.7",
29
+ "@hanamorilabs/flocktab-proxy-win-x64": "0.1.7"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.18.6",