@cardelli/ambit 0.3.0 → 0.3.1

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.
@@ -12,9 +12,9 @@ import { isPublicTld } from "../../../util/guard.js";
12
12
  import { createFlyProvider } from "../../../providers/fly.js";
13
13
  import { createTailscaleProvider, } from "../../../providers/tailscale.js";
14
14
  import { getCredentialStore } from "../../../util/credentials.js";
15
- import { FLY_PRIVATE_SUBNET, TAILSCALE_API_KEY_PREFIX, } from "../../../util/constants.js";
15
+ import { TAILSCALE_API_KEY_PREFIX } from "../../../util/constants.js";
16
16
  import { resolveOrg } from "../../../util/resolve.js";
17
- import { assertAdditivePatch, isAutoApproverConfigured, isTagOwnerConfigured, patchAutoApprover, patchTagOwner, } from "../../../util/tailscale-local.js";
17
+ import { assertAdditivePatch, isAutoApproverConfigured, isTagOwnerConfigured, patchTagOwner, } from "../../../util/tailscale-local.js";
18
18
  import { createTransition, hydrateCreate, reportSkipped, } from "./machine.js";
19
19
  // =============================================================================
20
20
  // Stage 1: Fly.io Configuration
@@ -107,40 +107,21 @@ const stageTailscaleConfig = async (out, opts) => {
107
107
  else {
108
108
  tagOwnerSpinner.success(`${opts.tag} Found in Tailscale ACL`);
109
109
  }
110
- if (!opts.manual) {
111
- const hasApprover = isAutoApproverConfigured(policy, opts.tag);
112
- if (!hasApprover && policy) {
113
- const beforeApprover = policy;
114
- policy = patchAutoApprover(policy, opts.tag, FLY_PRIVATE_SUBNET);
115
- assertAdditivePatch(beforeApprover, policy);
116
- const validateApprover = await tailscale.acl.validatePolicy(policy);
117
- if (!validateApprover.ok) {
118
- return handleAclSetFailure(out, validateApprover, `Validating autoApprover patch for ${opts.tag}`);
119
- }
120
- const approverSpinner = out.spinner(`Adding autoApprover for ${opts.tag}`);
121
- const result = await tailscale.acl.setPolicy(policy);
122
- if (!result.ok) {
123
- approverSpinner.fail(`Adding autoApprover for ${opts.tag}`);
124
- return handleAclSetFailure(out, result, `Adding autoApprover for ${opts.tag}`);
125
- }
126
- approverSpinner.success(`Added autoApprover for ${opts.tag}`);
127
- }
128
- }
129
- else if (opts.json) {
110
+ if (opts.manual && opts.json) {
130
111
  const approverSpinner = out.spinner(`Checking autoApprovers for ${opts.tag}`);
131
112
  const hasApprover = isAutoApproverConfigured(policy, opts.tag);
132
113
  if (!hasApprover) {
133
114
  approverSpinner.fail(`Auto-approve Not Configured for ${opts.tag}`);
134
115
  out.blank()
135
- .text(" In JSON mode, ambit can't interactively approve the router's")
136
- .text(` network connections. You can set this up from the Tailscale dashboard:`)
116
+ .text(" In --manual --json mode, ambit can't interactively approve the")
117
+ .text(" router's subnet routes. Set up autoApprovers first:")
137
118
  .link(" https://login.tailscale.com/admin/acls/visual/auto-approvers")
138
- .dim(` Route: ${FLY_PRIVATE_SUBNET} Owner: ${opts.tag}`)
119
+ .dim(` Route: <subnet>/48 Owner: ${opts.tag}`)
139
120
  .blank()
140
- .dim(" Or you can do it manually with this JSON config:")
141
- .dim(` "autoApprovers": { "routes": { "${FLY_PRIVATE_SUBNET}": ["${opts.tag}"] } }`)
121
+ .dim(" Or in the ACL file:")
122
+ .dim(` "autoApprovers": { "routes": { "<subnet>/48": ["${opts.tag}"] } }`)
142
123
  .blank();
143
- return out.die(`Set Up Auto-approve for ${opts.tag} to Use --json`);
124
+ return out.die(`Set Up Auto-approve for ${opts.tag} to Use --manual --json`);
144
125
  }
145
126
  approverSpinner.success(`Auto-approve Configured for ${opts.tag}`);
146
127
  }
@@ -334,6 +315,7 @@ ${bold("EXAMPLES")}
334
315
  region,
335
316
  tag,
336
317
  shouldApprove,
318
+ manual,
337
319
  });
338
320
  };
339
321
  // =============================================================================
@@ -3,7 +3,7 @@ import { Result } from "../../../lib/result.js";
3
3
  import { type FlyProvider } from "../../../providers/fly.js";
4
4
  import type { TailscaleProvider } from "../../../providers/tailscale.js";
5
5
  import type { TailscaleDevice } from "../../../schemas/tailscale.js";
6
- export type CreatePhase = "create_app" | "deploy_router" | "await_device" | "approve_routes" | "configure_dns" | "accept_routes" | "complete";
6
+ export type CreatePhase = "create_app" | "deploy_router" | "approve_routes" | "configure_dns" | "accept_routes" | "complete";
7
7
  export type CreateResult = {
8
8
  network: string;
9
9
  router: {
@@ -22,6 +22,7 @@ export interface CreateCtx {
22
22
  region: string;
23
23
  tag: string;
24
24
  shouldApprove: boolean;
25
+ manual: boolean;
25
26
  appName: string;
26
27
  routerId: string;
27
28
  device?: TailscaleDevice;
@@ -1 +1 @@
1
- {"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/create/machine.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAQhD,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAS7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAOrE,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,eAAe,GACf,cAAc,GACd,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,UAAU,CAAC;AAMf,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,WAAW,CAAC;IACjB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD,eAAO,MAAM,aAAa,GACxB,KAAK,MAAM,CAAC,YAAY,CAAC,EACzB,YAAY,WAAW,SAMxB,CAAC;AAMF,eAAO,MAAM,aAAa,GACxB,KAAK,SAAS,KACb,OAAO,CAAC,WAAW,CA4BrB,CAAC;AAMF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,WAAW,EAClB,KAAK,SAAS,KACb,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAoH7B,CAAC"}
1
+ {"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/create/machine.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAQhD,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAW7E,OAAO,KAAK,EAAgB,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAOrE,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,UAAU,CAAC;AAMf,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,WAAW,CAAC;IACjB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAcD,eAAO,MAAM,aAAa,GACxB,KAAK,MAAM,CAAC,YAAY,CAAC,EACzB,YAAY,WAAW,SAMxB,CAAC;AAMF,eAAO,MAAM,aAAa,GACxB,KAAK,SAAS,KACb,OAAO,CAAC,WAAW,CA4BrB,CAAC;AAMF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,WAAW,EAClB,KAAK,SAAS,KACb,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CA6K7B,CAAC"}
@@ -7,7 +7,7 @@ import { extractSubnet } from "../../../util/fly-transforms.js";
7
7
  import { ROUTER_DOCKER_DIR, SECRET_NETWORK_NAME, SECRET_ROUTER_ID, SECRET_TAILSCALE_AUTHKEY, } from "../../../util/constants.js";
8
8
  import { FlyDeployError } from "../../../providers/fly.js";
9
9
  import { getRouterAppName } from "../../../util/naming.js";
10
- import { enableAcceptRoutes, isAcceptRoutesEnabled, isAutoApproverConfigured, isTailscaleInstalled, waitForDevice, } from "../../../util/tailscale-local.js";
10
+ import { assertAdditivePatch, enableAcceptRoutes, isAcceptRoutesEnabled, isAutoApproverConfigured, isTailscaleInstalled, patchAutoApprover, waitForDevice, } from "../../../util/tailscale-local.js";
11
11
  import { findRouterApp, getRouterMachineInfo } from "../../../util/discovery.js";
12
12
  // =============================================================================
13
13
  // Phase Labels
@@ -15,7 +15,6 @@ import { findRouterApp, getRouterMachineInfo } from "../../../util/discovery.js"
15
15
  const CREATE_PHASES = [
16
16
  { phase: "create_app", label: "Fly App Created" },
17
17
  { phase: "deploy_router", label: "Router Deployed" },
18
- { phase: "await_device", label: "Router in Tailnet" },
19
18
  { phase: "approve_routes", label: "Routes Approved" },
20
19
  { phase: "configure_dns", label: "Split DNS Configured" },
21
20
  { phase: "accept_routes", label: "Accept Routes Enabled" },
@@ -44,7 +43,7 @@ export const hydrateCreate = async (ctx) => {
44
43
  return "complete";
45
44
  const device = await ctx.tailscale.devices.getByHostname(router.appName);
46
45
  if (!device)
47
- return "await_device";
46
+ return "approve_routes";
48
47
  ctx.device = device;
49
48
  const routes = await ctx.tailscale.routes.get(device.id);
50
49
  if (!routes || routes.unapproved.length > 0)
@@ -70,15 +69,7 @@ export const createTransition = async (phase, ctx) => {
70
69
  return Result.ok("deploy_router");
71
70
  }
72
71
  case "deploy_router": {
73
- const authKey = await ctx.tailscale.auth.createKey({
74
- reusable: false,
75
- ephemeral: false,
76
- preauthorized: true,
77
- tags: [ctx.tag],
78
- });
79
- ctx.out.ok("Auth Key Created");
80
- await ctx.out.spin("Setting Secrets", () => ctx.fly.secrets.set(ctx.appName, {
81
- [SECRET_TAILSCALE_AUTHKEY]: authKey,
72
+ await ctx.out.spin("Staging Secrets", () => ctx.fly.secrets.set(ctx.appName, {
82
73
  [SECRET_NETWORK_NAME]: ctx.network,
83
74
  [SECRET_ROUTER_ID]: ctx.routerId,
84
75
  }, { stage: true }));
@@ -102,34 +93,84 @@ export const createTransition = async (phase, ctx) => {
102
93
  const m = machines.find((m) => m.private_ip);
103
94
  if (m?.private_ip)
104
95
  ctx.subnet = extractSubnet(m.private_ip);
105
- if (!ctx.shouldApprove)
106
- return Result.ok("complete");
107
- return Result.ok("await_device");
96
+ return Result.ok("approve_routes");
108
97
  }
109
- case "await_device": {
110
- ctx.device = await waitForDevice(ctx.tailscale, ctx.appName, 180000);
111
- ctx.out.ok(`Router Joined Tailnet: ${ctx.device.addresses[0]}`);
98
+ case "approve_routes": {
112
99
  if (!ctx.subnet) {
113
100
  const machines = await ctx.fly.machines.list(ctx.appName);
114
101
  const m = machines.find((m) => m.private_ip);
115
102
  if (m?.private_ip)
116
103
  ctx.subnet = extractSubnet(m.private_ip);
117
104
  }
118
- return Result.ok("approve_routes");
119
- }
120
- case "approve_routes": {
121
- if (!ctx.device || !ctx.subnet) {
122
- return Result.err("Missing Device or Subnet");
123
- }
124
- const policy = await ctx.tailscale.acl.getPolicy();
105
+ if (!ctx.subnet)
106
+ return Result.err("Missing Subnet");
107
+ let policy = await ctx.tailscale.acl.getPolicy();
125
108
  const hasAutoApprover = isAutoApproverConfigured(policy, ctx.tag);
126
- if (hasAutoApprover) {
127
- ctx.out.ok("Routes Auto-Approved via ACL Policy");
109
+ let approverReady = hasAutoApprover;
110
+ if (!hasAutoApprover && !ctx.manual && policy) {
111
+ const before = policy;
112
+ policy = patchAutoApprover(policy, ctx.tag, ctx.subnet);
113
+ assertAdditivePatch(before, policy);
114
+ const vr = await ctx.tailscale.acl.validatePolicy(policy);
115
+ if (!vr.ok) {
116
+ ctx.out.warn(`Could Not Validate autoApprover Patch: ${vr.error ?? `HTTP ${vr.status}`}`);
117
+ }
118
+ else {
119
+ const sr = await ctx.tailscale.acl.setPolicy(policy);
120
+ if (sr.ok) {
121
+ ctx.out.ok(`Added autoApprover for ${ctx.tag} → ${ctx.subnet}`);
122
+ approverReady = true;
123
+ }
124
+ else if (sr.status === 403) {
125
+ ctx.out.warn("API Token Lacks ACL Write Permission — Will Approve Routes Manually");
126
+ }
127
+ else {
128
+ ctx.out.warn(`Could Not Set autoApprover: ${sr.error ?? `HTTP ${sr.status}`}`);
129
+ }
130
+ }
128
131
  }
129
- else {
132
+ // If the device isn't in the tailnet yet, the router hasn't
133
+ // authenticated. Mint an auth key and deliver it — the non-staged
134
+ // secrets set triggers a Fly restart. The router boots with the key,
135
+ // authenticates, and advertises routes. With autoApprover in place,
136
+ // routes are auto-approved immediately.
137
+ if (!ctx.device) {
138
+ const existing = await ctx.tailscale.devices.getByHostname(ctx.appName);
139
+ if (existing) {
140
+ ctx.device = existing;
141
+ }
142
+ else {
143
+ const authKey = await ctx.tailscale.auth.createKey({
144
+ reusable: false,
145
+ ephemeral: false,
146
+ preauthorized: true,
147
+ tags: [ctx.tag],
148
+ });
149
+ ctx.out.ok("Auth Key Created");
150
+ const keySpinner = ctx.out.spinner("Delivering Auth Key (restarting router)");
151
+ await ctx.fly.secrets.set(ctx.appName, {
152
+ [SECRET_TAILSCALE_AUTHKEY]: authKey,
153
+ });
154
+ keySpinner.success("Auth Key Delivered");
155
+ if (!ctx.shouldApprove) {
156
+ ctx.out.dim(" Skipping Route Approval (--no-auto-approve)");
157
+ return Result.ok("complete");
158
+ }
159
+ ctx.device = await waitForDevice(ctx.tailscale, ctx.appName, 180000);
160
+ ctx.out.ok(`Router Joined Tailnet: ${ctx.device.addresses[0]}`);
161
+ }
162
+ }
163
+ const routes = await ctx.tailscale.routes.get(ctx.device.id);
164
+ if (routes && routes.unapproved.length > 0) {
165
+ if (approverReady) {
166
+ ctx.out.warn("Routes Not Auto-Approved Despite autoApprover — Approving Manually");
167
+ }
130
168
  await ctx.tailscale.routes.approve(ctx.device.id, [ctx.subnet]);
131
169
  ctx.out.ok("Subnet Routes Approved");
132
170
  }
171
+ else {
172
+ ctx.out.ok("Routes Auto-Approved via ACL Policy");
173
+ }
133
174
  return Result.ok("configure_dns");
134
175
  }
135
176
  case "configure_dns": {
package/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "@cardelli/ambit",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Deploy apps to the cloud that only you and your AI agents can reach",
5
5
  "license": "MIT",
6
6
  "tasks": {
@@ -5,7 +5,8 @@ set -e
5
5
  # ambit - Self-Configuring Tailscale Subnet Router
6
6
  # =============================================================================
7
7
  # State is persisted to /var/lib/tailscale via Fly volume.
8
- # On first run: authenticates with a pre-minted auth key, advertises routes.
8
+ # On first run: waits for TAILSCALE_AUTHKEY (delivered by the CLI after
9
+ # autoApprovers are in place), then authenticates and advertises routes.
9
10
  # On restart: reuses existing state, no new device created.
10
11
  # The router never receives the user's API token — only a single-use,
11
12
  # tag-scoped auth key that expires after 5 minutes.
@@ -41,10 +42,12 @@ if /usr/local/bin/tailscale status --json 2>/dev/null | jq -e '.BackendState ==
41
42
  --hostname="${FLY_APP_NAME:-ambit}" \
42
43
  --advertise-routes="${SUBNET}"
43
44
  else
44
- # First run - authenticate with pre-minted auth key
45
+ # First run auth key is delivered by the CLI via `fly secrets set` after
46
+ # autoApprovers are configured. If it's not here yet, wait for the restart
47
+ # that the non-staged secrets set triggers.
45
48
  if [ -z "${TAILSCALE_AUTHKEY}" ]; then
46
- echo "Router: ERROR - No TAILSCALE_AUTHKEY Provided"
47
- exit 1
49
+ echo "Router: Waiting for Auth Key (CLI will deliver via secrets)"
50
+ while true; do sleep 5; done
48
51
  fi
49
52
 
50
53
  echo "Router: Authenticating to Tailscale"
@@ -2,7 +2,6 @@ export declare const ROUTER_APP_PREFIX = "ambit-";
2
2
  export declare const DEFAULT_FLY_NETWORK = "default";
3
3
  export declare const ROUTER_DOCKER_DIR: string;
4
4
  export declare const SOCKS_PROXY_PORT = 1080;
5
- export declare const FLY_PRIVATE_SUBNET = "fdaa::/16";
6
5
  export declare const TAILSCALE_API_KEY_PREFIX = "tskey-api-";
7
6
  export declare const ENV_TAILSCALE_API_KEY = "TAILSCALE_API_KEY";
8
7
  export declare const FLYCTL_INSTALL_URL = "https://fly.io/docs/flyctl/install/";
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/util/constants.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAE1C,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C,eAAO,MAAM,iBAAiB,QACnB,CAAC;AAMZ,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAM9C,eAAO,MAAM,wBAAwB,eAAe,CAAC;AAErD,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAMzD,eAAO,MAAM,kBAAkB,wCAAwC,CAAC;AAMxE,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAM5C,eAAO,MAAM,2BAA2B,yBAAyB,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/util/constants.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAE1C,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C,eAAO,MAAM,iBAAiB,QACnB,CAAC;AAMZ,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAMrC,eAAO,MAAM,wBAAwB,eAAe,CAAC;AAErD,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAMzD,eAAO,MAAM,kBAAkB,wCAAwC,CAAC;AAMxE,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAM5C,eAAO,MAAM,2BAA2B,yBAAyB,CAAC"}
@@ -12,7 +12,6 @@ export const ROUTER_DOCKER_DIR = new URL("../router", globalThis[Symbol.for("imp
12
12
  // Networking
13
13
  // =============================================================================
14
14
  export const SOCKS_PROXY_PORT = 1080;
15
- export const FLY_PRIVATE_SUBNET = "fdaa::/16";
16
15
  // =============================================================================
17
16
  // Tailscale
18
17
  // =============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardelli/ambit",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Deploy apps to the cloud that only you and your AI agents can reach",
5
5
  "license": "MIT",
6
6
  "scripts": {},