@hanamorilabs/tab 0.1.5 → 0.1.6

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
@@ -38,6 +38,25 @@ is keyed, and keeps it in the config file so `tab claude` is one command.
38
38
  Set `FLOCKTAB_PROXY_URL`, `FLOCKTAB_KEY` and `FLOCKTAB_UNLOCK` instead when
39
39
  you would rather not have a file or a folder, for example in CI.
40
40
 
41
+ ## On a subscription (Claude Max, ChatGPT)
42
+
43
+ ```
44
+ tab use --subscription --plan "Claude Max 5x" --price 100
45
+ tab claude # your own Claude login through the tab
46
+ tab codex # your own ChatGPT login (codex login once, inside tab codex)
47
+ tab quota # 5h / 7d windows the vendor reported on the last call
48
+ tab use --api-key # back to the metered tab
49
+ ```
50
+
51
+ The plan pays for the tokens; the tab still gates every call (kill switch,
52
+ velocity, model and tool policy) and records it at list price as a
53
+ `SUBSCRIPTION` row that never touches the cap. Claude Code gets
54
+ `ANTHROPIC_BASE_URL=<proxy>/t/<key>/anthropic` and no API key; Codex gets a
55
+ home whose `chatgpt_base_url` is `<proxy>/t/<key>/openai/backend-api/`, with
56
+ your existing `~/.codex/auth.json` copied in once (never written back). The
57
+ mode is the Agent's, read on every launch, so the console's **Runs on** panel
58
+ and `tab use` agree. `docs/subscriptions.md` has the wire.
59
+
41
60
  ## Self-hosted
42
61
 
43
62
  The proxy runs on your box and holds the provider keys; the ledger, tabs and
@@ -59,6 +78,8 @@ Codex shows `FlockTab - Self-hosted` as its provider.
59
78
  tab login approve this machine in the console, once
60
79
  tab <agent> [args] claude, codex, grok, kimi, gemini, or any command, on the tab
61
80
  tab use pick or change the Agent this folder runs as (.flocktab)
81
+ tab use --subscription | --api-key this Agent on your Claude Max / ChatGPT plan, or back on the meter
82
+ tab quota [agent] what the subscription has left: 5h and 7d windows
62
83
  tab alias setup <name>... make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
63
84
  tab alias remove <name>... undo that; `tab alias list` shows them
64
85
  tab status flock, folder Agent, proxy health, provider key state
@@ -76,7 +97,7 @@ session from `tab login`. An Agent is named by slug or name; left out, it is
76
97
  this folder's Agent. Every read takes `--json`.
77
98
 
78
99
  ```
79
- tab list every Agent: state, spent of cap, window, project
100
+ tab list every Agent: state, mode (meter | plan), spent of cap, window, project
80
101
  tab close my-project tab open my-project the kill switch (402 tab_closed on the next call)
81
102
  tab cap my-project 25 --window week cap in dollars, and the window
82
103
  tab rename my-project "My project (nightly)" the slug stays
@@ -121,5 +142,6 @@ proxy http://127.0.0.1:8787 (self-hosted) up
121
142
  flock Hanamori Labs
122
143
  folder flocktab-codex
123
144
  plan team
145
+ mode metered (the flock's provider key, charged to the tab)
124
146
  byok provider keys on this box
125
147
  ```
package/dist/cli.js CHANGED
@@ -7,6 +7,8 @@
7
7
  * tab codex [...args] Codex on the tab
8
8
  * tab <cmd> [...args] anything else, both APIs pointed at the tab
9
9
  * tab use pick (or change) the Agent this folder runs as
10
+ * tab use --subscription this folder's Agent runs on your Claude Max / ChatGPT plan
11
+ * tab quota what the plan has left (5h / 7d windows) for this Agent
10
12
  * tab alias setup codex make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
11
13
  * tab alias remove codex undo that; `tab alias list` shows them
12
14
  * tab status which flock, which Agent here, is the proxy up
@@ -53,6 +55,9 @@ function usage() {
53
55
  cmd(others.join(" | tab "), "the same, for those agents"),
54
56
  cmd("<command> [args]", "any other agent, both APIs pointed at the tab"),
55
57
  cmd("use", "pick or change the Agent this folder runs as (.flocktab)"),
58
+ cmd("use --subscription", "this Agent runs on your Claude Max / ChatGPT plan (--plan \"Max 5x\" --price 100)"),
59
+ cmd("use --api-key", "back to the metered tab with the flock's provider key"),
60
+ cmd("quota [agent]", "what the subscription has left: 5h and 7d windows"),
56
61
  cmd("alias setup <name>...", "make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)"),
57
62
  cmd("alias remove <name>...", "undo that; `tab alias list` shows them"),
58
63
  cmd("status", "which flock, which Agent here, is the proxy up"),
@@ -480,6 +485,7 @@ async function status() {
480
485
  }
481
486
  else {
482
487
  pairs.push(["plan", me.flock.plan]);
488
+ pairs.push(["mode", me.authMode === "subscription" ? `${bold("subscription")} ${dim("(your Claude Max / ChatGPT login; FlockTab keeps the record)")}` : `metered ${dim("(the flock's provider key, charged to the tab)")}`]);
483
489
  const byok = !me.byok
484
490
  ? dim("no provider key on this flock")
485
491
  : me.unlock === "none"
@@ -506,9 +512,21 @@ async function runAgent(name, args) {
506
512
  const agent = await resolveAgent(config);
507
513
  if (!agent)
508
514
  return 1;
509
- const key = presentedKey({ key: agent.key, unlock: config.unlock });
515
+ // The Agent's mode is the console's call, read fresh on every launch so a
516
+ // switch there (or `tab use --subscription`) applies to the next run.
517
+ const me = await whoami(config.proxyUrl, { key: agent.key, unlock: config.unlock });
518
+ const auth = !("error" in me) && me.authMode === "subscription" ? "subscription" : "key";
519
+ // On a subscription there is no provider key to unlock; the bare key rides in the URL.
520
+ const key = auth === "subscription" ? agent.key : presentedKey({ key: agent.key, unlock: config.unlock });
510
521
  reportFolder(config.proxyUrl, key);
511
- const { spec, env } = envFor({ name, proxyUrl: config.proxyUrl, presentedKey: key });
522
+ const { spec, env } = envFor({ name, proxyUrl: config.proxyUrl, presentedKey: key, auth });
523
+ if (auth === "subscription") {
524
+ if (spec.shape === "openai" && spec.isolatedHome !== "codex") {
525
+ fail(`${spec.label} has no subscription route. Only Claude Code and Codex can run on a plan; switch this Agent back with ${bold("tab use --api-key")}.`);
526
+ return 2;
527
+ }
528
+ say(dim(`${agent.agentName} runs on your own ${spec.isolatedHome === "codex" ? "ChatGPT" : "Claude"} login; FlockTab keeps the record and the kill switch.`));
529
+ }
512
530
  // An alias shim named like the agent must not be what we spawn.
513
531
  env.PATH = pathWithoutShims();
514
532
  if (spec.isolatedHome === "codex") {
@@ -517,6 +535,7 @@ async function runAgent(name, args) {
517
535
  proxyUrl: config.proxyUrl,
518
536
  presentedKey: key,
519
537
  mode: config.mode,
538
+ auth,
520
539
  });
521
540
  }
522
541
  const child = spawn(spec.command, [...(spec.args ?? []), ...args], { stdio: "inherit", env });
@@ -588,15 +607,39 @@ function pathHint() {
588
607
  say(` ${bold(pathLine())}`);
589
608
  return 0;
590
609
  }
591
- /** `tab use`: pick or change the Agent for this folder. */
592
- async function use() {
610
+ /**
611
+ * `tab use`: pick or change the Agent for this folder. With `--subscription`
612
+ * or `--api-key` the folder's Agent (picked first if there is none) is
613
+ * switched in the console; `--plan` and `--price` label the seat.
614
+ */
615
+ async function use(args) {
593
616
  const config = await ensureLogin();
594
617
  if (!config)
595
618
  return 2;
619
+ const flags = manage.parseFlags(args);
620
+ const wantsSubscription = flags.opts.subscription !== undefined;
621
+ const wantsKey = flags.opts["api-key"] !== undefined || flags.opts.apikey !== undefined;
622
+ if (wantsSubscription && wantsKey) {
623
+ fail("Pick one: tab use --subscription or tab use --api-key.");
624
+ return 2;
625
+ }
596
626
  if (!(await ensureProxy(config)))
597
627
  return 1;
598
- const agent = await resolveAgent(config, { pick: true });
599
- return agent ? 0 : 1;
628
+ if (!wantsSubscription && !wantsKey) {
629
+ const agent = await resolveAgent(config, { pick: true });
630
+ return agent ? 0 : 1;
631
+ }
632
+ const agent = await resolveAgent(config);
633
+ if (!agent)
634
+ return 1;
635
+ try {
636
+ const project = await readProject(process.cwd());
637
+ return await manage.setAuthMode(config, flags, project?.agent ?? agent.agentName, wantsSubscription ? "subscription" : "api_key");
638
+ }
639
+ catch (err) {
640
+ fail(err instanceof Error ? err.message : String(err));
641
+ return 1;
642
+ }
600
643
  }
601
644
  /**
602
645
  * First `tab up` on a box: the provider keys the proxy will spend, typed
@@ -720,6 +763,8 @@ async function managed(command, rest) {
720
763
  return await manage.outside(config, flags);
721
764
  case "live":
722
765
  return await manage.live(config, flags);
766
+ case "quota":
767
+ return await manage.quota(config, flags);
723
768
  default:
724
769
  return await manage.web(config, flags);
725
770
  }
@@ -745,7 +790,7 @@ async function main(argv) {
745
790
  ok("Forgot the session and keys on this machine.");
746
791
  return 0;
747
792
  case "use":
748
- return use();
793
+ return use(rest);
749
794
  case "alias":
750
795
  return alias(rest);
751
796
  case "unalias":
@@ -769,6 +814,7 @@ async function main(argv) {
769
814
  case "spend":
770
815
  case "outside":
771
816
  case "live":
817
+ case "quota":
772
818
  case "web":
773
819
  return managed(command, rest);
774
820
  case "status":
package/dist/clients.js CHANGED
@@ -72,11 +72,27 @@ export function openaiBase(proxyUrl, provider) {
72
72
  * overridden on purpose: a stray `OPENAI_BASE_URL` from another tool would
73
73
  * otherwise route around the tab, which is the one thing this must not do.
74
74
  */
75
+ /** `<proxy>/t/<key>/<provider>`: the subscription passthrough base for a vendor. */
76
+ export function passthroughBase(proxyUrl, key, provider) {
77
+ return `${proxyUrl}/t/${key}/${provider}`;
78
+ }
75
79
  export function envFor(input) {
76
- const spec = clientFor(input.name);
80
+ const base = clientFor(input.name);
77
81
  const known = input.name in KNOWN;
82
+ const auth = input.auth ?? "key";
78
83
  const env = { ...(input.base ?? process.env) };
84
+ // Codex: pinned to the API key on a metered tab, to the ChatGPT login on a subscription.
85
+ const spec = base.isolatedHome === "codex"
86
+ ? { ...base, args: ["-c", auth === "subscription" ? 'preferred_auth_method="chatgpt"' : 'preferred_auth_method="apikey"'] }
87
+ : base;
79
88
  const setAnthropic = () => {
89
+ if (auth === "subscription") {
90
+ // Claude Code signs in with its own Claude account; FlockTab only moves the base URL.
91
+ env.ANTHROPIC_BASE_URL = passthroughBase(input.proxyUrl, input.presentedKey, "anthropic");
92
+ delete env.ANTHROPIC_API_KEY;
93
+ delete env.ANTHROPIC_AUTH_TOKEN;
94
+ return;
95
+ }
80
96
  env.ANTHROPIC_BASE_URL = input.proxyUrl;
81
97
  env.ANTHROPIC_API_KEY = input.presentedKey;
82
98
  // Claude Code prefers an auth token when one is set; make sure a stale
@@ -84,6 +100,13 @@ export function envFor(input) {
84
100
  delete env.ANTHROPIC_AUTH_TOKEN;
85
101
  };
86
102
  const setOpenAI = () => {
103
+ if (auth === "subscription") {
104
+ // Only Codex has a subscription route (its own home points at it); a
105
+ // plain OpenAI key would bypass the plan, so none is set.
106
+ delete env.OPENAI_BASE_URL;
107
+ delete env.OPENAI_API_KEY;
108
+ return;
109
+ }
87
110
  env.OPENAI_BASE_URL = openaiBase(input.proxyUrl, spec.provider);
88
111
  env.OPENAI_API_KEY = input.presentedKey;
89
112
  };
@@ -99,5 +122,6 @@ export function envFor(input) {
99
122
  }
100
123
  // So the child, and anything it spawns, can tell it is on a tab.
101
124
  env.FLOCKTAB_PROXY_URL = input.proxyUrl;
125
+ env.FLOCKTAB_AUTH_MODE = auth;
102
126
  return { spec, env };
103
127
  }
@@ -10,8 +10,10 @@
10
10
  *
11
11
  * The person's own `~/.codex` is never read or written.
12
12
  */
13
- import { chmod, mkdir, writeFile } from "node:fs/promises";
13
+ import { chmod, copyFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
14
+ import { homedir } from "node:os";
14
15
  import path from "node:path";
16
+ import { passthroughBase } from "./clients.js";
15
17
  export function codexAuthJson(presentedKey) {
16
18
  return `${JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: presentedKey })}\n`;
17
19
  }
@@ -19,6 +21,20 @@ export function codexAuthJson(presentedKey) {
19
21
  export function codexProviderId(mode) {
20
22
  return mode === "self-hosted" ? "FlockTab - Self-hosted" : "FlockTab - Hosted";
21
23
  }
24
+ /**
25
+ * On a ChatGPT subscription Codex talks to `chatgpt_base_url` with its own
26
+ * login, so that is what moves: to the passthrough, where the tab keeps the
27
+ * record. No provider block; the default OpenAI provider is the ChatGPT one.
28
+ */
29
+ export function codexSubscriptionToml(proxyUrl, presentedKey, model) {
30
+ const lines = [
31
+ `preferred_auth_method = "chatgpt"`,
32
+ `chatgpt_base_url = ${JSON.stringify(`${passthroughBase(proxyUrl, presentedKey, "openai")}/backend-api/`)}`,
33
+ ...(model ? [`model = ${JSON.stringify(model)}`] : []),
34
+ ``,
35
+ ];
36
+ return lines.join("\n");
37
+ }
22
38
  export function codexConfigToml(proxyUrl, model, mode = "hosted") {
23
39
  const id = codexProviderId(mode);
24
40
  const lines = [
@@ -41,9 +57,46 @@ export async function prepareCodexHome(input) {
41
57
  await chmod(home, 0o700);
42
58
  const auth = path.join(home, "auth.json");
43
59
  const config = path.join(home, "config.toml");
44
- await writeFile(auth, codexAuthJson(input.presentedKey), { mode: 0o600 });
45
- await writeFile(config, codexConfigToml(input.proxyUrl, input.model, input.mode ?? "hosted"), { mode: 0o600 });
46
- await chmod(auth, 0o600);
60
+ if (input.auth === "subscription") {
61
+ await adoptChatGptLogin(auth, input.userCodexHome ?? path.join(homedir(), ".codex"));
62
+ await writeFile(config, codexSubscriptionToml(input.proxyUrl, input.presentedKey, input.model), { mode: 0o600 });
63
+ }
64
+ else {
65
+ await writeFile(auth, codexAuthJson(input.presentedKey), { mode: 0o600 });
66
+ await chmod(auth, 0o600);
67
+ await writeFile(config, codexConfigToml(input.proxyUrl, input.model, input.mode ?? "hosted"), { mode: 0o600 });
68
+ }
47
69
  await chmod(config, 0o600);
48
70
  return home;
49
71
  }
72
+ /**
73
+ * The ChatGPT login for the tab's Codex home. A login already there (from
74
+ * `codex login` run through `tab codex`) stays. Otherwise the person's own
75
+ * `~/.codex/auth.json` is copied, owner-only: same machine, same person,
76
+ * and `~/.codex` itself is still never written. A leftover API-key auth
77
+ * file from metered mode is replaced; with nothing to copy, Codex asks the
78
+ * person to sign in and keeps that login in the tab home.
79
+ */
80
+ async function adoptChatGptLogin(auth, userCodexHome) {
81
+ const isApiKey = async (file) => {
82
+ try {
83
+ const parsed = JSON.parse(await readFile(file, "utf8"));
84
+ return parsed.auth_mode === "apikey" || (typeof parsed.OPENAI_API_KEY === "string" && parsed.OPENAI_API_KEY.startsWith("ft_"));
85
+ }
86
+ catch {
87
+ return undefined;
88
+ }
89
+ };
90
+ const existing = await isApiKey(auth);
91
+ if (existing === false)
92
+ return "kept";
93
+ if (existing === true)
94
+ await unlink(auth).catch(() => undefined);
95
+ const theirs = path.join(userCodexHome, "auth.json");
96
+ if ((await isApiKey(theirs)) === false) {
97
+ await copyFile(theirs, auth);
98
+ await chmod(auth, 0o600);
99
+ return "copied";
100
+ }
101
+ return "none";
102
+ }
package/dist/manage.js CHANGED
@@ -59,6 +59,7 @@ export function parseFlags(argv) {
59
59
  }
60
60
  return flags;
61
61
  }
62
+ const mode = (t) => (t.authMode === "subscription" ? `${yellow("plan")}${t.subscriptionPlan ? dim(` ${t.subscriptionPlan}`) : ""}` : "meter");
62
63
  async function tabsOf(call) {
63
64
  return (await call("GET", "/api/cli/tabs")).tabs;
64
65
  }
@@ -92,7 +93,7 @@ export async function list(config, flags) {
92
93
  const tabs = await tabsOf(api(config));
93
94
  emit(flags.json, { tabs }, () => tabs.length === 0
94
95
  ? dim("No Agents yet. Run tab claude or tab codex in a project folder.")
95
- : table(["agent", "state", "spent", "cap", "used", "window", "project"], tabs.map((t) => [t.name === t.slug ? t.slug : `${t.name} ${dim(t.slug)}`, state(t.state), money(t.spentCents), money(t.capCents), pct(t.spentCents, t.capCents), t.window, t.projectName ?? dim("-")]), { right: [2, 3, 4] }));
96
+ : table(["agent", "state", "mode", "spent", "cap", "used", "window", "project"], tabs.map((t) => [t.name === t.slug ? t.slug : `${t.name} ${dim(t.slug)}`, state(t.state), mode(t), money(t.spentCents), money(t.capCents), pct(t.spentCents, t.capCents), t.window, t.projectName ?? dim("-")]), { right: [3, 4, 5] }));
96
97
  return 0;
97
98
  }
98
99
  export async function setState(config, flags, next) {
@@ -139,6 +140,49 @@ export async function archive(config, flags, ask) {
139
140
  emit(flags.json, { tab, archived: true }, () => `${bold(tab.name)} archived.`);
140
141
  return 0;
141
142
  }
143
+ /**
144
+ * `tab use --subscription [--plan "Claude Max 5x"] [--price 100]` and
145
+ * `tab use --api-key`: which login the Agent runs on. On a subscription the
146
+ * vendor's plan pays; the tab still gates every call and records it at list
147
+ * price as a shadow row, and the seat's monthly price sits beside the meter
148
+ * in the project rollup.
149
+ */
150
+ export async function setAuthMode(config, flags, given, authMode) {
151
+ const call = api(config);
152
+ const slug = await resolveSlug(call, given);
153
+ const body = { authMode };
154
+ if (flags.opts.plan !== undefined)
155
+ body.subscriptionPlan = flags.opts.plan;
156
+ if (flags.opts.price !== undefined)
157
+ body.subscriptionMonthlyDollars = flags.opts.price.replace(/^\$/, "");
158
+ const { tab } = await call("PATCH", `/api/cli/tabs/${encodeURIComponent(slug)}`, body);
159
+ emit(flags.json, { tab }, () => authMode === "subscription"
160
+ ? `${bold(tab.name)} runs on a subscription${tab.subscriptionPlan ? ` (${tab.subscriptionPlan}${tab.subscriptionMonthlyCents ? `, ${money(tab.subscriptionMonthlyCents)}/mo` : ""})` : ""}.\n` +
161
+ `${dim("tab claude uses your own Claude login, tab codex your ChatGPT login. The kill switch and policies still apply; calls land in the ledger at list price, never on the tab's spend.")}`
162
+ : `${bold(tab.name)} is back on the metered tab: the flock's provider key, charged to the cap.`);
163
+ return 0;
164
+ }
165
+ /** `tab quota [agent]`: what the vendor said the plan has left, on the last call. */
166
+ export async function quota(config, flags) {
167
+ const call = api(config);
168
+ const slug = await resolveSlug(call, flags.args[0]);
169
+ const { tab, quota: windows } = await call("GET", `/api/cli/tabs/${encodeURIComponent(slug)}/quota`);
170
+ emit(flags.json, { tab, quota: windows }, () => {
171
+ if (tab.authMode !== "subscription")
172
+ return `${bold(tab.name)} is on the metered tab; ${dim("tab use --subscription")} puts it on a plan.`;
173
+ if (windows.length === 0)
174
+ return `${bold(tab.name)}: no quota reported yet. It shows after the first call through tab claude or tab codex.`;
175
+ const left = (w) => {
176
+ const pctLeft = Math.max(0, 100 - w.usedPct);
177
+ const paint = pctLeft <= 10 ? red : pctLeft <= 30 ? yellow : green;
178
+ return paint(`${pctLeft}% left`);
179
+ };
180
+ const when = (iso) => (iso ? new Date(iso).toLocaleString() : dim("-"));
181
+ return (`${bold(tab.name)}${tab.subscriptionPlan ? dim(` ${tab.subscriptionPlan}`) : ""}\n` +
182
+ table(["provider", "window", "used", "left", "resets", "seen"], windows.map((w) => [w.provider, w.window, `${w.usedPct}%`, left(w), when(w.resetsAt), when(w.observedAt)]), { right: [2] }));
183
+ });
184
+ return 0;
185
+ }
142
186
  function listOpt(raw) {
143
187
  if (raw === undefined)
144
188
  return undefined;
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.1";
16
+ export const PROXY_VERSION = "0.1.3";
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/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.5";
2
+ export const TAB_VERSION = "0.1.6";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanamorilabs/tab",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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.1",
26
- "@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.1",
27
- "@hanamorilabs/flocktab-proxy-linux-x64": "0.1.1",
28
- "@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.1",
29
- "@hanamorilabs/flocktab-proxy-win-x64": "0.1.1"
25
+ "@hanamorilabs/flocktab-proxy-darwin-arm64": "0.1.3",
26
+ "@hanamorilabs/flocktab-proxy-darwin-x64": "0.1.3",
27
+ "@hanamorilabs/flocktab-proxy-linux-x64": "0.1.3",
28
+ "@hanamorilabs/flocktab-proxy-linux-arm64": "0.1.3",
29
+ "@hanamorilabs/flocktab-proxy-win-x64": "0.1.3"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.18.6",