@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 +2 -0
- package/dist/cli.js +52 -2
- package/dist/clients.js +8 -4
- package/dist/manage.js +53 -2
- package/dist/proxy-bin.js +12 -2
- package/dist/version.js +1 -1
- package/package.json +6 -6
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
|
-
|
|
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
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.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",
|