@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 +22 -5
- package/dist/codex-home.js +20 -0
- package/dist/proxy-bin.js +1 -1
- package/dist/statusline.js +135 -13
- package/dist/tab-docs.js +8 -5
- package/dist/version.js +1 -1
- package/package.json +6 -6
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,
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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":
|
package/dist/codex-home.js
CHANGED
|
@@ -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.
|
|
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")
|
package/dist/statusline.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
99
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
|
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: [
|
|
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
|
|
354
|
-
details: ["Each Agent's pulse (
|
|
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.
|
|
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.
|
|
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.
|
|
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.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",
|