@hanamorilabs/tab 0.1.17 → 0.1.19
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 +10 -2
- package/dist/codex-home.js +96 -33
- package/dist/tab-docs.js +4 -6
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -365,9 +365,17 @@ async function resolveAgent(config, opts = {}) {
|
|
|
365
365
|
let created;
|
|
366
366
|
if (project) {
|
|
367
367
|
chosen = byName.get(project.agent);
|
|
368
|
-
if (chosen &&
|
|
368
|
+
if (chosen && harness !== "any" && (chosen.harness ?? "any") !== "any" && chosen.harness !== harness) {
|
|
369
|
+
// An older one-Agent-per-folder file (or a hand edit) points this harness at an Agent tied to
|
|
370
|
+
// another one. The proxies would refuse every call (wrong_harness), so pick the right Agent now;
|
|
371
|
+
// the answer is written for this harness only and the file's other harnesses keep theirs.
|
|
372
|
+
warn(`${project.file} names Agent ${bold(chosen.name)} for ${harness}, but it is a ${chosen.harness} Agent. Pick the Agent this folder runs ${harness} as.`);
|
|
373
|
+
chosen = undefined;
|
|
374
|
+
}
|
|
375
|
+
else if (chosen && cached) {
|
|
369
376
|
return cached;
|
|
370
|
-
|
|
377
|
+
}
|
|
378
|
+
else if (!chosen) {
|
|
371
379
|
warn(`${project.file} names Agent ${bold(project.agent)}, which ${listed.flock.name} no longer has (archived or deleted). Pick the Agent this folder runs as now.`);
|
|
372
380
|
}
|
|
373
381
|
}
|
package/dist/codex-home.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* therefore points `CODEX_HOME` at a directory it owns, with an API-key auth
|
|
9
9
|
* file and one provider: the tab, over the Responses wire the proxy serves.
|
|
10
10
|
*
|
|
11
|
-
* The person's own `~/.codex` is never
|
|
11
|
+
* The person's own `~/.codex` is never written. Its `config.toml` is read:
|
|
12
|
+
* the tab's config starts from it (approval policy, sandbox, model, MCP
|
|
13
|
+
* servers, trusted folders), with the tab's few keys layered on top.
|
|
12
14
|
*/
|
|
13
15
|
import { chmod, copyFile, lstat, mkdir, readdir, readFile, rename, rm, symlink, unlink, writeFile } from "node:fs/promises";
|
|
14
16
|
import { homedir } from "node:os";
|
|
@@ -24,40 +26,97 @@ export function codexProviderId(mode) {
|
|
|
24
26
|
/**
|
|
25
27
|
* On a ChatGPT subscription Codex talks to `chatgpt_base_url` with its own
|
|
26
28
|
* login, so that is what moves: to the passthrough, where the tab keeps the
|
|
27
|
-
* record.
|
|
29
|
+
* record. The provider is a ChatGPT-auth one of the tab's (`flocktab`), so
|
|
30
|
+
* Codex is told there is no WebSocket to open. Its id is what `/status` shows.
|
|
28
31
|
*/
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
export const CODEX_TAB_PROVIDER = "flocktab";
|
|
33
|
+
export function codexSubscriptionParts(proxyUrl, presentedKey, model) {
|
|
34
|
+
const codex = `${passthroughBase(proxyUrl, presentedKey, "openai")}/backend-api/codex`;
|
|
35
|
+
return {
|
|
36
|
+
top: [
|
|
37
|
+
`preferred_auth_method = "chatgpt"`,
|
|
38
|
+
`model_provider = ${JSON.stringify(CODEX_TAB_PROVIDER)}`,
|
|
39
|
+
`chatgpt_base_url = ${JSON.stringify(`${passthroughBase(proxyUrl, presentedKey, "openai")}/backend-api/`)}`,
|
|
40
|
+
// Codex 0.155+ sends the model calls of a ChatGPT login to `openai_base_url` (fixed to
|
|
41
|
+
// chatgpt.com/backend-api/codex unless set); `chatgpt_base_url` only covers the rest. Kept for a
|
|
42
|
+
// profile of the person's own that names the built-in "openai" provider: it still runs through the tab.
|
|
43
|
+
`openai_base_url = ${JSON.stringify(codex)}`,
|
|
44
|
+
...(model ? [`model = ${JSON.stringify(model)}`] : []),
|
|
45
|
+
],
|
|
38
46
|
// Codex 0.155+ opens a WebSocket for responses on the ChatGPT provider. The tab is plain HTTP
|
|
39
|
-
// (no upgrade on either proxy)
|
|
40
|
-
`
|
|
41
|
-
`responses_websockets = false`,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
// (no upgrade on either proxy). Codex 0.155.1 ignores these two flags, so the provider below
|
|
48
|
+
// says so as well (`supports_websockets = false`); the flags stay for the builds that read them.
|
|
49
|
+
features: [`responses_websockets = false`, `responses_websockets_v2 = false`],
|
|
50
|
+
tables: [
|
|
51
|
+
[
|
|
52
|
+
`[model_providers.${CODEX_TAB_PROVIDER}]`,
|
|
53
|
+
`name = "OpenAI through FlockTab"`,
|
|
54
|
+
`base_url = ${JSON.stringify(codex)}`,
|
|
55
|
+
`wire_api = "responses"`,
|
|
56
|
+
`requires_openai_auth = true`,
|
|
57
|
+
`supports_websockets = false`,
|
|
58
|
+
].join("\n"),
|
|
59
|
+
],
|
|
60
|
+
};
|
|
46
61
|
}
|
|
47
|
-
export function
|
|
62
|
+
export function codexSubscriptionToml(proxyUrl, presentedKey, model) {
|
|
63
|
+
const parts = codexSubscriptionParts(proxyUrl, presentedKey, model);
|
|
64
|
+
return [...parts.top, ``, `[features]`, ...parts.features, ``, ...parts.tables, ``].join("\n");
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The person's own config with the tab's keys on top. TOML forbids a key or
|
|
68
|
+
* table twice, so the lines the tab sets are dropped from theirs first; the
|
|
69
|
+
* tab's top-level keys go before any table, its `[features]` keys join an
|
|
70
|
+
* existing `[features]` table, and its own tables go last. Trust answers
|
|
71
|
+
* Codex wrote under the tab (`[projects."…"]`) are carried from the previous
|
|
72
|
+
* tab config when the person's own file does not name that folder.
|
|
73
|
+
*/
|
|
74
|
+
export function layerCodexConfig(input) {
|
|
75
|
+
const setKeys = new Set([...input.top, ...(input.features ?? [])].map((line) => line.split("=")[0].trim()));
|
|
76
|
+
const kept = input.theirs
|
|
77
|
+
.split("\n")
|
|
78
|
+
.filter((line) => {
|
|
79
|
+
const key = line.split("=")[0].trim().replace(/^features\./, "");
|
|
80
|
+
return !(setKeys.has(key) && /=/.test(line));
|
|
81
|
+
})
|
|
82
|
+
.join("\n")
|
|
83
|
+
.trimEnd();
|
|
84
|
+
const lead = [...input.top, ""].join("\n");
|
|
85
|
+
let body = kept;
|
|
86
|
+
if (input.features?.length) {
|
|
87
|
+
if (/^\[features\]\s*$/m.test(body))
|
|
88
|
+
body = body.replace(/^\[features\]\s*$/m, ["[features]", ...input.features].join("\n"));
|
|
89
|
+
else
|
|
90
|
+
body = `${body}\n\n[features]\n${input.features.join("\n")}`;
|
|
91
|
+
}
|
|
92
|
+
for (const table of input.tables ?? [])
|
|
93
|
+
body = `${body}\n\n${table}`;
|
|
94
|
+
let out = `${lead}\n${body.trim()}\n`;
|
|
95
|
+
for (const block of input.previous?.match(/^\[projects\."[^"\n]+"\]\n(?:(?!\[)[^\n]*\n?)*/gm) ?? []) {
|
|
96
|
+
const header = block.split("\n")[0];
|
|
97
|
+
if (!out.includes(header))
|
|
98
|
+
out = `${out}\n${block.trimEnd()}\n`;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
export function codexKeyParts(proxyUrl, model, mode = "hosted") {
|
|
48
103
|
const id = codexProviderId(mode);
|
|
49
|
-
|
|
50
|
-
`model_provider = ${JSON.stringify(id)}`,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
104
|
+
return {
|
|
105
|
+
top: [`model_provider = ${JSON.stringify(id)}`, ...(model ? [`model = ${JSON.stringify(model)}`] : [])],
|
|
106
|
+
tables: [
|
|
107
|
+
[
|
|
108
|
+
`[model_providers.${JSON.stringify(id)}]`,
|
|
109
|
+
`name = ${JSON.stringify(id)}`,
|
|
110
|
+
`base_url = ${JSON.stringify(`${proxyUrl}/v1`)}`,
|
|
111
|
+
`env_key = "OPENAI_API_KEY"`,
|
|
112
|
+
`wire_api = "responses"`,
|
|
113
|
+
].join("\n"),
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function codexConfigToml(proxyUrl, model, mode = "hosted") {
|
|
118
|
+
const parts = codexKeyParts(proxyUrl, model, mode);
|
|
119
|
+
return [...parts.top, ``, ...parts.tables, ``].join("\n");
|
|
61
120
|
}
|
|
62
121
|
/** Write the home and return its path. Files are owner-only. */
|
|
63
122
|
export async function prepareCodexHome(input) {
|
|
@@ -66,14 +125,18 @@ export async function prepareCodexHome(input) {
|
|
|
66
125
|
await chmod(home, 0o700);
|
|
67
126
|
const auth = path.join(home, "auth.json");
|
|
68
127
|
const config = path.join(home, "config.toml");
|
|
128
|
+
const theirs = await readFile(input.userConfig ?? path.join(homedir(), ".codex", "config.toml"), "utf8").catch(() => "");
|
|
129
|
+
const previous = await readFile(config, "utf8").catch(() => undefined);
|
|
69
130
|
if (input.auth === "subscription") {
|
|
70
131
|
await adoptChatGptLogin(auth, input.userCodexHome ?? path.join(homedir(), ".codex"));
|
|
71
|
-
|
|
132
|
+
const parts = codexSubscriptionParts(input.proxyUrl, input.presentedKey, input.model);
|
|
133
|
+
await writeFile(config, layerCodexConfig({ theirs, previous, top: parts.top, features: parts.features, tables: parts.tables }), { mode: 0o600 });
|
|
72
134
|
}
|
|
73
135
|
else {
|
|
74
136
|
await writeFile(auth, codexAuthJson(input.presentedKey), { mode: 0o600 });
|
|
75
137
|
await chmod(auth, 0o600);
|
|
76
|
-
|
|
138
|
+
const parts = codexKeyParts(input.proxyUrl, input.model, input.mode ?? "hosted");
|
|
139
|
+
await writeFile(config, layerCodexConfig({ theirs, previous, top: parts.top, tables: parts.tables }), { mode: 0o600 });
|
|
77
140
|
}
|
|
78
141
|
await chmod(config, 0o600);
|
|
79
142
|
await shareConversations(home, input.userCodexHome ?? path.join(homedir(), ".codex"));
|
package/dist/tab-docs.js
CHANGED
|
@@ -38,7 +38,7 @@ export const TAB_COMMANDS = [
|
|
|
38
38
|
usage: ["use", "use claude|codex|grok|kimi"],
|
|
39
39
|
summary: "Pick or change the Agent this folder runs a harness as.",
|
|
40
40
|
details: [
|
|
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
|
|
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
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
|
],
|
|
@@ -50,20 +50,19 @@ export const TAB_COMMANDS = [
|
|
|
50
50
|
aliases: ["codex", "grok", "kimi", "gemini", "aider", "cursor", "<command>"],
|
|
51
51
|
group: "run",
|
|
52
52
|
usage: ["claude [args]", "codex [args]", "grok [args]", "kimi [args]", "<any command> [args]"],
|
|
53
|
-
summary: "Run an agent on this folder's tab.
|
|
53
|
+
summary: "Run an agent on this folder's tab. Everything after the name goes to the agent untouched.",
|
|
54
54
|
details: [
|
|
55
55
|
"tab sets the environment that agent reads (its base URL and key, or for Codex a home folder of its own) and starts it. It does not wrap or parse the agent's traffic; the proxy does the gating.",
|
|
56
|
+
"Codex's tab home starts from your own ~/.codex/config.toml: approval policy, sandbox mode, model and effort, MCP servers, profiles and trusted folders all apply under tab codex. Only the tab's keys (where the calls go, the flocktab provider) are layered on top.",
|
|
56
57
|
"On an API-key Agent the agent presents the tab's key, the flock's provider key pays, and every call is held, settled and charged to the cap. A call that would pass the cap is refused before the provider with 402 tab_closed.",
|
|
57
58
|
"On a Subscription Agent the agent keeps its own login (Claude Max, ChatGPT, SuperGrok, Kimi). The tab key rides in the base URL, the same kill switch and policies apply before the vendor is reached, the call is recorded at list price under the account the vendor names, and nothing is charged to the cap.",
|
|
58
59
|
"Any other command works too: tab points both the OpenAI and the Anthropic variables at the proxy and runs it. An agent with no consumer plan on a Subscription Agent runs metered on the flock's key, and tab says so.",
|
|
59
60
|
"With logins in the pool for that vendor, a Subscription Agent runs as the login with most room. See tab pool.",
|
|
60
|
-
"Opt in with tab claude --observe or tab codex --observe to show native sessions, workers and observed messages in Observability. Review and trust the added hooks in the harness normally; collection is unavailable if hooks are skipped. Only identifiers, timestamps and tool outcomes leave the machine, never prompts, message bodies or transcripts. A successful send is not a read receipt. Collection is experimental: Claude 2.1.278+ in the 2.1 family and Codex 0.155.1+ in the 0.155 family, on macOS/Linux. Offline metadata is bounded and retried on another observed launch of the same Agent; inactive queues older than seven days are removed on the next observed launch, and overflow is counted as a coverage gap.",
|
|
61
61
|
],
|
|
62
62
|
options: [
|
|
63
63
|
{ flag: "--as <login>", what: "run as one pool login and never move off it" },
|
|
64
64
|
{ flag: "--no-pool", what: "ignore the pool and use the agent's usual login" },
|
|
65
65
|
{ flag: "--pool", what: "require the pool; fail rather than fall back when it cannot be used" },
|
|
66
|
-
{ flag: "--observe", what: "opt in to metadata-only native communication hooks for this Claude/Codex launch; put before --" },
|
|
67
66
|
],
|
|
68
67
|
examples: [
|
|
69
68
|
{ cmd: "tab claude", what: "Claude Code on this folder's tab" },
|
|
@@ -329,7 +328,7 @@ export const TAB_COMMANDS = [
|
|
|
329
328
|
summary: "What was spent through the tab, and outside it, grouped: by Agent, project, day, or model and effort.",
|
|
330
329
|
details: [
|
|
331
330
|
"Meter spend is what went through FlockTab. Outside spend is what the connected billing sources report (Team plan and up), so the two can be told apart.",
|
|
332
|
-
"tab spend model lists each model with the reasoning effort the requests asked for (OpenAI's reasoning effort, Anthropic's effort or thinking budget)
|
|
331
|
+
"tab spend model lists each model with the reasoning effort the requests asked for (OpenAI's reasoning effort, Anthropic's effort or thinking budget), how many calls, what they cost and how they were paid: charged to a tab, or priced at list on a plan. The console shows the same under Spend, Models.",
|
|
333
332
|
],
|
|
334
333
|
options: [
|
|
335
334
|
{ flag: "--by agent|project|day|model", what: "how to group" },
|
|
@@ -435,7 +434,6 @@ export const TAB_GLOSSARY = [
|
|
|
435
434
|
{ term: "Account switch", meaning: "An Agent's call landing on a different account of the same vendor than its previous one: a new login mid-session, another machine, or the pool moving. Shown on the Agent's page and under Control, Subscriptions." },
|
|
436
435
|
{ term: "Project", meaning: "A label grouping Agents for chargeback on FlockTab's own meter, optionally linked to a GitHub repository. Team plan and up." },
|
|
437
436
|
{ 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." },
|
|
438
|
-
{ term: "Tier", also: ["service tier", "fast mode", "speed"], meaning: "A speed the request asked for, in the request's own words: OpenAI's and xAI's service_tier (fast, priority, flex), Anthropic's speed (fast). Vendors price these apart, so it is kept on every call and shown beside the model on the ledger and under Spend, Models." },
|
|
439
437
|
{ term: "Effort", also: ["reasoning effort", "thinking budget"], meaning: "How hard a request asked the model to think, in the request's own words: OpenAI's reasoning effort (low, medium, high, xhigh), Anthropic's effort or a thinking budget in tokens. Kept on every call, shown on the ledger and under Spend, Models, because it is where the money goes." },
|
|
440
438
|
{ 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." },
|
|
441
439
|
{ 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." },
|
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.19";
|