@lanes-sh/link 0.2.2 → 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.
Files changed (143) hide show
  1. package/README.md +22 -8
  2. package/instructions/skills/lanes-link/SKILL.md +42 -14
  3. package/package.json +1 -1
  4. package/src/cli/argv.ts +50 -0
  5. package/src/cli/brand.ts +178 -0
  6. package/src/cli/callback-page.ts +108 -128
  7. package/src/cli/commands/connect/accounts.ts +5 -0
  8. package/src/cli/commands/connect/assertion.ts +187 -0
  9. package/src/cli/commands/connect/authorise.ts +61 -17
  10. package/src/cli/commands/connect/client.ts +37 -9
  11. package/src/cli/commands/connect/discover.ts +94 -0
  12. package/src/cli/commands/connect/family.ts +72 -0
  13. package/src/cli/commands/connect/index.ts +113 -115
  14. package/src/cli/commands/connect/method.ts +237 -0
  15. package/src/cli/commands/connect/outcome.ts +42 -1
  16. package/src/cli/commands/connect/pasted-token.ts +66 -0
  17. package/src/cli/commands/connect/requirements.ts +60 -8
  18. package/src/cli/commands/connect/setup.ts +16 -5
  19. package/src/cli/commands/connect/target-note.ts +34 -0
  20. package/src/cli/commands/identity.ts +258 -0
  21. package/src/cli/commands/knowledge/index.ts +390 -0
  22. package/src/cli/commands/knowledge/migrate.ts +180 -0
  23. package/src/cli/commands/knowledge/setup.ts +144 -0
  24. package/src/cli/commands/knowledge.ts +10 -0
  25. package/src/cli/commands/mcp/harnesses.ts +16 -2
  26. package/src/cli/commands/mcp/register.ts +9 -1
  27. package/src/cli/commands/mcp/stdio.ts +21 -0
  28. package/src/cli/commands/operate/dashboard.ts +107 -0
  29. package/src/cli/commands/operate/findings.ts +151 -0
  30. package/src/cli/commands/operate/inspect.ts +63 -164
  31. package/src/cli/commands/operate/outputs.ts +38 -11
  32. package/src/cli/commands/operate/policy.ts +7 -7
  33. package/src/cli/commands/operate/serve.ts +3 -0
  34. package/src/cli/commands/operate/status.ts +108 -1
  35. package/src/cli/commands/operate/token.ts +1 -1
  36. package/src/cli/commands/operate.ts +2 -0
  37. package/src/cli/commands/profile/declare.ts +154 -0
  38. package/src/cli/commands/profile/removal.ts +17 -0
  39. package/src/cli/commands/profile/remove.ts +5 -5
  40. package/src/cli/commands/profile.ts +83 -35
  41. package/src/cli/commands/secrets.ts +6 -6
  42. package/src/cli/commands/setup.ts +22 -6
  43. package/src/cli/commands/sync.ts +262 -0
  44. package/src/cli/commands/target.ts +65 -83
  45. package/src/cli/config-edit.ts +53 -144
  46. package/src/cli/config-repair.ts +186 -0
  47. package/src/cli/dashboard-page.ts +284 -0
  48. package/src/cli/dashboard-shell.ts +125 -0
  49. package/src/cli/dispatch-owner.ts +93 -0
  50. package/src/cli/identity.ts +12 -1
  51. package/src/cli/main.ts +90 -61
  52. package/src/cli/nearest.ts +45 -0
  53. package/src/cli/oauth-callback.ts +187 -0
  54. package/src/cli/oauth-exchange.ts +57 -15
  55. package/src/cli/oauth.ts +67 -177
  56. package/src/cli/output.ts +21 -5
  57. package/src/cli/provider-marks.ts +45 -0
  58. package/src/cli/runtime/open.ts +74 -51
  59. package/src/cli/runtime/registry.ts +60 -2
  60. package/src/cli/runtime/select.ts +26 -13
  61. package/src/cli/runtime/vault.ts +61 -0
  62. package/src/cli/runtime.ts +2 -1
  63. package/src/cli/selection.ts +365 -0
  64. package/src/cli/usage.ts +42 -11
  65. package/src/connectivity/auth/README.md +7 -1
  66. package/src/connectivity/auth/basic/index.ts +1 -1
  67. package/src/connectivity/auth/index.ts +14 -0
  68. package/src/connectivity/auth/oauth-authcode/broker.ts +26 -0
  69. package/src/connectivity/auth/oauth-authcode/index.ts +16 -2
  70. package/src/connectivity/auth/oauth-authcode/provider.ts +1 -1
  71. package/src/connectivity/auth/oauth-authcode/refresh.ts +3 -3
  72. package/src/connectivity/auth/oauth-jwt/README.md +33 -0
  73. package/src/connectivity/auth/oauth-jwt/index.ts +237 -0
  74. package/src/connectivity/auth/oauth-jwt/key.ts +148 -0
  75. package/src/connectivity/auth/resolve.ts +1 -1
  76. package/src/connectivity/auth/token.ts +11 -0
  77. package/src/connectivity/index.ts +2 -0
  78. package/src/connectivity/manifest/auth.ts +99 -2
  79. package/src/connectivity/manifest/identity.ts +12 -0
  80. package/src/connectivity/manifest/index.ts +3 -1
  81. package/src/connectivity/manifest/provider.ts +37 -8
  82. package/src/connectivity/manifest/requirements.ts +109 -6
  83. package/src/deployments/adapters/filesystem.ts +10 -1
  84. package/src/deployments/adapters/github-api.ts +106 -0
  85. package/src/deployments/adapters/github-commit.ts +103 -0
  86. package/src/deployments/adapters/github-repo.ts +356 -0
  87. package/src/deployments/adapters/github-testing.ts +258 -0
  88. package/src/deployments/adapters/github.ts +125 -0
  89. package/src/deployments/deploy.ts +94 -114
  90. package/src/deployments/discover.ts +103 -0
  91. package/src/deployments/driver.ts +8 -1
  92. package/src/deployments/gcp/driver.ts +3 -1
  93. package/src/deployments/knowledge.ts +119 -0
  94. package/src/deployments/prepare.ts +12 -6
  95. package/src/deployments/report.ts +117 -0
  96. package/src/deployments/servable.ts +82 -0
  97. package/src/deployments/serving.ts +165 -0
  98. package/src/deployments/sync-apply.ts +276 -0
  99. package/src/deployments/sync.ts +136 -0
  100. package/src/deployments/target.ts +3 -2
  101. package/src/deployments/upload.ts +19 -12
  102. package/src/dispatch/dispatch.ts +1 -1
  103. package/src/profile/deployments.ts +80 -0
  104. package/src/profile/identity.ts +60 -0
  105. package/src/profile/index.ts +23 -5
  106. package/src/profile/knowledge.ts +124 -0
  107. package/src/profile/load.ts +17 -5
  108. package/src/profile/primitives.ts +24 -1
  109. package/src/profile/schema.ts +81 -3
  110. package/src/profile/targets.ts +122 -109
  111. package/src/profile/workspace.ts +139 -79
  112. package/src/providers/google/calendar/index.ts +2 -0
  113. package/src/providers/google/contacts/index.ts +2 -0
  114. package/src/providers/google/docs/index.ts +2 -0
  115. package/src/providers/google/drive/index.ts +2 -0
  116. package/src/providers/google/gmail/index.ts +2 -0
  117. package/src/providers/google/gmail-imap/index.ts +125 -0
  118. package/src/providers/google/index.ts +2 -1
  119. package/src/providers/google/shared/oauth.ts +18 -6
  120. package/src/providers/google/shared/service-account.ts +110 -0
  121. package/src/providers/google/shared/setup.ts +21 -3
  122. package/src/providers/google/sheets/index.ts +2 -0
  123. package/src/providers/google/tasks/index.ts +2 -0
  124. package/src/providers/identity/provider.ts +166 -0
  125. package/src/providers/index.ts +3 -0
  126. package/src/providers/owner.ts +10 -2
  127. package/src/providers/scopes.ts +2 -0
  128. package/src/providers/setup/plan.ts +31 -9
  129. package/src/providers/setup/provider.ts +23 -0
  130. package/src/providers/slack/index.ts +81 -33
  131. package/src/providers/slack/oauth.ts +103 -0
  132. package/src/providers/slack/scopes.ts +37 -0
  133. package/src/server/container.ts +18 -1
  134. package/src/server/cors.ts +252 -0
  135. package/src/server/dashboard.ts +208 -0
  136. package/src/server/endpoint.ts +45 -1
  137. package/src/server/generations.ts +11 -2
  138. package/src/server/harness.ts +7 -0
  139. package/src/server/index.ts +45 -6
  140. package/src/server/mcp/index.ts +1 -0
  141. package/src/server/mcp/instructions.ts +28 -1
  142. package/src/server/mcp/visibility.ts +33 -0
  143. package/src/stores/blobs/route.ts +123 -0
package/README.md CHANGED
@@ -48,8 +48,8 @@ Needs [Bun](https://bun.com) 1.3.11+. Nothing else — no account anywhere.
48
48
 
49
49
  ```console
50
50
  $ bun install -g @lanes-sh/link # puts `lanes` on your PATH
51
- $ lanes link profile add personal --default
52
- $ lanes link start
51
+ $ lanes link profile add personal --target local
52
+ $ lanes link start --profile personal --target local
53
53
  ok serving http://127.0.0.1:7337/mcp
54
54
  profiles: personal
55
55
  ```
@@ -57,7 +57,7 @@ ok serving http://127.0.0.1:7337/mcp
57
57
  Then, in another shell:
58
58
 
59
59
  ```console
60
- $ lanes link mcp add # every agent installed; or name one: claude, codex
60
+ $ lanes link mcp add --profile personal --target local # every agent installed; or name one: claude, codex
61
61
  ok registered lanes-link with Claude Code (user scope)
62
62
  ok registered lanes-link with Codex
63
63
  ```
@@ -78,6 +78,17 @@ calendar are the next step. **[Full quickstart →](docs/quickstart.md)**
78
78
  Memory and skills are plain Markdown files, so a text editor and an agent reach the same bytes. All
79
79
  four belong to one profile: what you add under `work` is invisible under `personal`.
80
80
 
81
+ Keep those two in a private GitHub repository instead of on this machine, and get history, diffs,
82
+ and the same notes from anywhere you run this:
83
+
84
+ ```console
85
+ $ lanes link knowledge use github --repo <owner/name> --migrate
86
+ ```
87
+
88
+ It moves what you have already stored, in one commit, and `lanes link knowledge use local
89
+ --migrate` brings it back. Nothing else moves — your credentials and your vault stay where they
90
+ are, and there is no setting that would put them in a repository.
91
+
81
92
  ## Connect an account
82
93
 
83
94
  One command per account. Run it again to add a second mailbox, a second calendar, a second anything.
@@ -99,15 +110,17 @@ One command per account. Run it again to add a second mailbox, a second calendar
99
110
  | Linear | `lanes link connect linear` |
100
111
  | GitHub | `lanes link connect github` |
101
112
  | Slack | `lanes link connect slack` |
113
+ | Gmail (IMAP, app password) | `lanes link connect gmail_imap` |
102
114
  | Gmail (Google MCP) | `lanes link connect gmail_mcp` |
103
115
  | Drive (Google MCP) | `lanes link connect drive_mcp` |
104
116
 
105
117
  Three things worth knowing up front. `lanes link connect icloud` sets up Mail, Calendar, and
106
- Contacts together, because one app-specific password covers all three. Google needs no OAuth client
107
- of your own: `lanes link connect gmail` authorises against the one Lanes operates, so there is no
108
- Cloud console to visit add `--own-client` if you would rather register your own. And GitHub and
109
- Slack take a token you paste rather than a browser sign-in, because neither will register a client
110
- for us; for Slack that means creating a Slack app once, which is the one console visit left here.
118
+ Contacts together, because one app-specific password covers all three. Google and Slack need no
119
+ OAuth client of your own: both authorise against the one Lanes operates, so there is no console to
120
+ visit for Google, add `--own-client` if you would rather register your own, or take a service
121
+ account key or an app password over IMAP where you would rather nothing expired. And GitHub takes a
122
+ token you paste rather than a browser sign-in, because it will not register a client for us; that
123
+ is the one console visit left here.
111
124
 
112
125
  Full guide — what each one gives your agent, what it needs, and adding your own:
113
126
  **[docs/connect.md](docs/connect.md)**.
@@ -137,6 +150,7 @@ than being rebuilt.
137
150
  - **[Connect your accounts](docs/connect.md)** — every provider, and what each one needs
138
151
  - **[Add it to your agent](docs/clients.md)** — Claude Code, Codex, Claude Desktop, claude.ai, ChatGPT
139
152
  - **[Deploy to your own cloud](docs/deploy.md)** — five commands to a URL
153
+ - **[Every command](docs/detailed/commands.md)** — arguments and flags, one entry each
140
154
  - **[Full reference](docs/detailed/)** — architecture, configuration, the CLI, the security model,
141
155
  writing a provider, and the decision records
142
156
 
@@ -24,6 +24,13 @@ is listed first.** Quietly picking one crosses the line the profile exists to
24
24
  draw. There is no "current profile" to switch — the choice is made per call, and
25
25
  `lanes link profile list` shows what exists.
26
26
 
27
+ **Every `lanes link` command names its profile and its target.** Both are required
28
+ flags with no default, no environment variable, and nothing in a config file
29
+ behind them, so a command missing either refuses rather than acting somewhere
30
+ else. When you write one out for the owner, either fill both in or leave them as
31
+ `<name>` for them to complete — never drop them. `lanes link target list
32
+ --profile <name>` shows what a profile declares.
33
+
27
34
  A `connection` names an account within that profile. One profile may hold
28
35
  several of the same kind, and naming a connection belonging to a *different*
29
36
  profile is refused rather than guessed at.
@@ -37,7 +44,7 @@ try more than one wording before deciding it is not there.
37
44
  Writing is a separate grant, and it should be. What you write is served back to
38
45
  every later session, including to a different agent, so **write when you are
39
46
  asked to remember something, not as a habit.** The owner reaches the same
40
- entries with `lanes link memory list` and a text editor.
47
+ entries with `lanes link memory list --profile <name> --target <name>` and a text editor.
41
48
 
42
49
  ## Skills are theirs, not yours
43
50
 
@@ -47,7 +54,7 @@ cannot read a skill's body; that is deliberate, not a gap to work around.
47
54
 
48
55
  So when a task has a skill for it, **say the skill exists and let them invoke
49
56
  it** rather than improvising your own version of their procedure. They manage
50
- these with `lanes link skills list` and `lanes link skills show <name>`.
57
+ these with `lanes link skills list --profile <name> --target <name>` and `lanes link skills show <skill> --profile <name> --target <name>`.
51
58
 
52
59
  ## Vault values are credentials
53
60
 
@@ -55,6 +62,27 @@ Use one to do the thing that needs it. Never quote it back, summarise it, echo
55
62
  it into a file, or paste it into a command whose output you will show. If a
56
63
  value has been printed by accident, say so.
57
64
 
65
+ ## Who you are writing as is declared, not inferred
66
+
67
+ A profile may declare the names, addresses and handles its owner wants used when
68
+ something is written as them. Call `identity_list` for the profile in play
69
+ before signing a message, choosing an address to send from, or attributing work
70
+ to a handle — **do not** read a name off the conversation, off a previous
71
+ message's signature, or off the account label on a connection. That label is the
72
+ identity a provider reports for a mailbox; it is not necessarily what they sign
73
+ with.
74
+
75
+ A profile may declare several of a kind on purpose. The first is the default and
76
+ each carries a note saying when to prefer it, so read the notes rather than
77
+ picking the first unconditionally. If none of them fits what you are doing, ask
78
+ — do not combine two, and do not carry one profile's name into another. That
79
+ crossing is the specific mistake this exists to prevent.
80
+
81
+ If the tool is not there, the profile has declared nothing. Ask rather than
82
+ inventing something; they add one with `lanes link identity add <kind> <value> --profile <profile>
83
+ --target <target>` — both flags, because neither has a fallback.
84
+ Nothing you can call writes here, deliberately.
85
+
58
86
  ## Attachments are named, not carried
59
87
 
60
88
  Where a tool takes `attachments`, each entry names **one** source and the endpoint
@@ -84,7 +112,7 @@ its content.
84
112
 
85
113
  **If the endpoint is not on the same machine as the file**, `path` names the
86
114
  *server's* filesystem rather than theirs, and will not find it. Ask them to run
87
- `lanes link attach <file> --connection <provider>.<account>`, which prints a
115
+ `lanes link attach <file> --profile <name> --target <name> --connection <provider>.<account>`, which prints a
88
116
  handle to use instead.
89
117
 
90
118
  **`draft_only: true`** saves instead of sending, where they should see it before
@@ -104,9 +132,9 @@ granted, and retrying will not reveal it. A call that *is* refused was refused b
104
132
  policy on purpose.
105
133
 
106
134
  Report it plainly and let the owner decide whether to widen the grant —
107
- `lanes link policy list` shows the rules, `lanes link policy allow <capability>`
135
+ `lanes link policy list --profile <name>` shows the rules, `lanes link policy allow <capability> --profile <name> --target <name>`
108
136
  changes them, and that is their call, not yours. **Do not look for another route
109
- to the same data.** Every call is audited either way; `lanes link audit tail`
137
+ to the same data.** Every call is audited either way; `lanes link audit tail --profile <name> --target <name>`
110
138
  shows what was attempted, refusals included.
111
139
 
112
140
  ## Setting something up
@@ -125,9 +153,9 @@ credential — those are `lanes link` in a terminal, deliberately. What you can
125
153
  know exactly what to hand over, and say what it will ask for before they start.
126
154
 
127
155
  If you have a shell, you can run it yourself for anything that does *not* need a
128
- browser: `lanes link setup plan <provider> --json` lists what to store,
129
- `lanes link secrets set <ref>` takes the value on stdin, and
130
- `lanes link connect <provider> --id <name> --non-interactive --json` finishes.
156
+ browser: `lanes link setup plan <provider> --profile <name> --target <name> --json` lists what to store,
157
+ `lanes link secrets set <ref> --profile <name> --target <name>` takes the value on stdin, and
158
+ `lanes link connect <provider> --profile <name> --target <name> --id <id> --non-interactive --json` finishes.
131
159
  Anything with a browser sign-in belongs to whoever owns the account — give them
132
160
  the line.
133
161
 
@@ -139,10 +167,10 @@ straight after connecting will still not show it. Say so rather than retrying.
139
167
 
140
168
  ## Registering it, and re-registering it
141
169
 
142
- `lanes link mcp add` runs each harness's own registration command and installs
170
+ `lanes link mcp add --profile <name> --target <name>` runs each harness's own registration command and installs
143
171
  this skill where that harness keeps them. With no argument it does every harness
144
172
  installed; name one (`claude`, `codex`) to be specific. Run it again after
145
- `lanes link token rotate` — add `--force`, since Claude Code stores the token as
173
+ `lanes link token rotate --profile <name> --target <name>` — add `--force`, since Claude Code stores the token as
146
174
  a value rather than a command.
147
175
 
148
176
  **Never paste the token.** There is a right way and a wrong way, and the
@@ -151,7 +179,7 @@ difference matters:
151
179
  ```bash
152
180
  # RIGHT — the token goes from the CLI to the harness. You never see it.
153
181
  claude mcp add --transport http lanes-link http://127.0.0.1:7337/mcp \
154
- --header "Authorization: Bearer $(lanes link token show --raw)"
182
+ --header "Authorization: Bearer $(lanes link token show --raw --profile <name> --target <name>)"
155
183
 
156
184
  # WRONG — the token is now in your context, and in the transcript, forever.
157
185
  lanes link token show --show # then copying the value into the command
@@ -159,9 +187,9 @@ lanes link token show --show # then copying the value into the command
159
187
 
160
188
  The token reaches every account of every profile the endpoint serves. Use the
161
189
  substitution form. If you have already printed one by accident, say so and offer
162
- `lanes link token rotate`.
190
+ `lanes link token rotate --profile <name> --target <name>`.
163
191
 
164
- Prefer `lanes link mcp add` to writing the command yourself: it checks the
192
+ Prefer `lanes link mcp add --profile <name> --target <name>` to writing the command yourself: it checks the
165
193
  endpoint is reachable, refuses to silently shadow an existing registration, and
166
194
  cannot mistype the token. For a harness it does not know, take the command from
167
195
  `lanes link outputs` rather than writing it blind — that command checks whether
@@ -173,7 +201,7 @@ If you register Codex, tell the user to export the token — Codex stores only t
173
201
  variable name, so nothing works until it is set:
174
202
 
175
203
  ```bash
176
- export LANES_LINK_TOKEN="$(lanes link token show --raw)"
204
+ export LANES_LINK_TOKEN="$(lanes link token show --raw --profile <name> --target <name>)"
177
205
  ```
178
206
 
179
207
  One registration covers every profile. Do not add one per profile; they share a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanes-sh/link",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "A self-hostable MCP gateway for all your connections, memory, skills, and secrets",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://lanes.sh/link",
package/src/cli/argv.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { GlobalFlags } from './runtime.ts';
2
2
  import type { OwnerFlags } from './commands/owner.ts';
3
+ import type { KnowledgeFlags } from './commands/knowledge.ts';
3
4
 
4
5
  /**
5
6
  * Turning argv into a command path and a flag bag.
@@ -63,6 +64,32 @@ export function text(flags: Flags, name: string): string | undefined {
63
64
  return typeof value === 'string' ? value : undefined;
64
65
  }
65
66
 
67
+ /**
68
+ * Every value given for a flag, in order.
69
+ *
70
+ * `profile add --target local --target cloud` is the one command where a flag
71
+ * legitimately repeats: it is declaring a list, not selecting one thing. The
72
+ * parser keeps the last value in `flags`, so this re-reads argv rather than
73
+ * changing that shape for every other flag's sake.
74
+ */
75
+ export function all(argv: readonly string[], name: string): string[] {
76
+ const values: string[] = [];
77
+
78
+ for (let i = 0; i < argv.length; i++) {
79
+ const token = argv[i]!;
80
+ if (token === `--${name}`) {
81
+ const next = argv[i + 1];
82
+ if (next !== undefined && !next.startsWith('--')) values.push(next);
83
+ continue;
84
+ }
85
+ if (token.startsWith(`--${name}=`)) values.push(token.slice(name.length + 3));
86
+ }
87
+
88
+ // Comma-separated is the other spelling people reach for, and refusing it
89
+ // would be a refusal with no reason behind it.
90
+ return values.flatMap((value) => value.split(',').map((part) => part.trim())).filter(Boolean);
91
+ }
92
+
66
93
  /** The flags every command accepts. */
67
94
  export function globalFlags(flags: Flags): GlobalFlags {
68
95
  return {
@@ -92,3 +119,26 @@ export function ownerFlags(flags: Flags): OwnerFlags {
92
119
  yes: flags['yes'] === true,
93
120
  };
94
121
  }
122
+
123
+ /**
124
+ * `lanes link knowledge`'s flags.
125
+ *
126
+ * Here for the reason `ownerFlags` is: the kebab-case spellings belong in the
127
+ * one place that parses argv, so `--allow-public` is written once rather than
128
+ * once per reader.
129
+ */
130
+ export function knowledgeFlags(flags: Flags): KnowledgeFlags {
131
+ return {
132
+ ...globalFlags(flags),
133
+ repo: text(flags, 'repo'),
134
+ branch: text(flags, 'branch'),
135
+ path: text(flags, 'path'),
136
+ // `--migrate`, `--no-migrate`, or neither — see `KnowledgeFlags.migrate`.
137
+ migrate: flags['migrate'] === true ? true : flags['no-migrate'] === true ? false : undefined,
138
+ keep: flags['keep'] === true,
139
+ allowPublic: flags['allow-public'] === true,
140
+ replace: flags['replace'] === true,
141
+ yes: flags['yes'] === true,
142
+ json: flags['json'] === true,
143
+ };
144
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The Lanes design tokens, and the pieces every page this repository serves is
3
+ * built from.
4
+ *
5
+ * Three surfaces render HTML — the authorization consent screen, the page a
6
+ * connect flow lands on, and the dashboard — and until this file existed each
7
+ * carried its own approximation of the brand. They had drifted: two greens and
8
+ * two ambers that are not tokens at all, nine different alphas standing in for
9
+ * one border colour, a destructive red with a dark variant the token does not
10
+ * have, and the system font stack where Geist belongs.
11
+ *
12
+ * Values are transcribed from https://lanes.sh/design/foundations. The rules
13
+ * they express, which the CSS below encodes rather than restates:
14
+ *
15
+ * - **Gold is the only accent.** "Gold for positive, neutral tokens otherwise."
16
+ * That is why there is no green here: a connection that works is `positive`,
17
+ * which is gold, and everything else is one of two neutrals. `--destructive`
18
+ * is for errors, not for a state that merely needs attention.
19
+ * - **Two weights, 400 and 500.** Nothing else is available in the loaded faces,
20
+ * so a heavier rule would silently synthesise.
21
+ * - **Lora heads, Geist speaks, Geist Mono for code and eyebrow labels.**
22
+ *
23
+ * `--border`, `--accent-gold` and `--destructive` are constant across themes.
24
+ * Only the five neutrals swap, which is why the dark block below is five lines
25
+ * rather than a second stylesheet.
26
+ */
27
+
28
+ /**
29
+ * Escape text for an HTML body.
30
+ *
31
+ * `escapeXml` in `connectivity/transports/dav/xml.ts` would do, and `cli` is
32
+ * allowed to import `connectivity` — but a page renderer reaching into a DAV
33
+ * transport for a string function is a dependency nobody would defend. Five
34
+ * lines is cheaper than the coupling.
35
+ *
36
+ * Load-bearing: these pages render a provider's `manifest.name`, and a custom
37
+ * provider supplies that from a YAML file of its own.
38
+ */
39
+ export function escapeHtml(value: string): string {
40
+ return value
41
+ .replace(/&/g, '&amp;')
42
+ .replace(/</g, '&lt;')
43
+ .replace(/>/g, '&gt;')
44
+ .replace(/"/g, '&quot;')
45
+ .replace(/'/g, '&#39;');
46
+ }
47
+
48
+ /**
49
+ * One request for all three faces.
50
+ *
51
+ * `preconnect` to both hosts because the stylesheet and the font files it names
52
+ * are served from different origins, and the second lookup is otherwise not
53
+ * started until the first has parsed.
54
+ */
55
+ export const FONTS =
56
+ '<link rel="preconnect" href="https://fonts.googleapis.com">\n' +
57
+ '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>\n' +
58
+ '<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500' +
59
+ '&amp;family=Geist:wght@400;500&amp;family=Lora:wght@400;500&amp;display=swap" rel="stylesheet">';
60
+
61
+ /** The one-row footer, identical to the one the Lanes API's own pages carry. */
62
+ export const FOOTER =
63
+ '<div class="footer"><p>Your unfair advantage in parallel AI coding. ' +
64
+ '<a href="https://lanes.sh/">lanes.sh</a></p></div>';
65
+
66
+ /**
67
+ * The tokens, and the base every page shares.
68
+ *
69
+ * The background is **painted**, which reverses what these pages used to do.
70
+ * The old note read "nothing is painted" — a transparent body under
71
+ * `color-scheme: light dark` so the browser supplied its own canvas — and it
72
+ * was written to fix a hardcoded `#0d1117` that showed as a black rectangle on
73
+ * a light machine. The design system settles that differently: it names a
74
+ * background for each mode, so the fix is to paint the right one rather than
75
+ * none. `color-scheme` stays, because form controls and scrollbars still need
76
+ * to know which mode they are in.
77
+ */
78
+ export const TOKENS = `
79
+ :root {
80
+ color-scheme: light dark;
81
+ --background: #EBEAE7;
82
+ --card: #F0EFEC;
83
+ --muted: #E2E1DD;
84
+ --foreground: #171717;
85
+ --muted-foreground: #202329;
86
+ --border: rgba(120,113,108,0.2);
87
+ --accent-gold: #A1845A;
88
+ --destructive: #A06060;
89
+ /* The two tints the badge variants need. Spelled as rgba rather than
90
+ color-mix because the consent screen opens in whatever browser a phone
91
+ happens to use, and both accents are constant across themes anyway. */
92
+ --gold-fill: rgba(161,132,90,0.1);
93
+ --gold-ring: rgba(161,132,90,0.25);
94
+ --destructive-fill: rgba(160,96,96,0.1);
95
+ --destructive-ring: rgba(160,96,96,0.25);
96
+ --sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
97
+ --serif: "Lora", Georgia, "Times New Roman", serif;
98
+ --mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
99
+ }
100
+ @media (prefers-color-scheme: dark) {
101
+ :root {
102
+ --background: #171717;
103
+ --card: #292524;
104
+ --muted: #202329;
105
+ --foreground: #EBEAE7;
106
+ --muted-foreground: #A8A29E;
107
+ }
108
+ }
109
+ * { box-sizing: border-box; }
110
+ body { margin: 0; font-family: var(--sans); font-weight: 400;
111
+ background: var(--background); color: var(--foreground); }
112
+ h1, h2, h3 { font-family: var(--serif); font-weight: 500; letter-spacing: -0.01em; margin: 0; }
113
+ h1 { font-size: 28px; line-height: 1.3; }
114
+ code, .mono { font-family: var(--mono); }
115
+ a { color: inherit; }
116
+
117
+ /* Eyebrow: 11px, uppercase, wide tracking, muted — and mono, which is where the
118
+ design system puts label type. */
119
+ .eyebrow { font-family: var(--mono); font-size: 11px; font-weight: 400;
120
+ letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted-foreground);
121
+ opacity: 0.75; }
122
+
123
+ /* Surfaces are dashed and 10px; controls are 6px; pills are round. */
124
+ .surface { background: var(--card); border: 1px dashed var(--border); border-radius: 10px; }
125
+
126
+ /* The four badge variants, as the design system defines them: gold for
127
+ positive, neutral tokens otherwise, destructive for an error only. The ring
128
+ is an inset shadow so it costs no layout, exactly as \`ring-inset\` does. */
129
+ .pill { display: inline-flex; align-items: center; flex: none; white-space: nowrap;
130
+ font-size: 11px; font-weight: 500; letter-spacing: 0.02em;
131
+ padding: 2px 8px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--border); }
132
+ .pill.positive { background: var(--gold-fill); color: var(--accent-gold);
133
+ box-shadow: inset 0 0 0 1px var(--gold-ring); }
134
+ .pill.neutral { background: var(--muted); color: var(--muted-foreground); }
135
+ .pill.quiet { background: transparent; color: var(--muted-foreground); opacity: 0.7; }
136
+ .pill.negative { background: var(--destructive-fill); color: var(--destructive);
137
+ box-shadow: inset 0 0 0 1px var(--destructive-ring); }
138
+
139
+ /* Ghost is the variant every button on these pages is: muted until it is
140
+ wanted, and never gold, because gold is reserved for saying a thing is good. */
141
+ .btn { font: inherit; font-size: 12px; font-weight: 500; line-height: 1;
142
+ padding: 6px 9px; border-radius: 6px; cursor: pointer;
143
+ background: transparent; color: var(--muted-foreground);
144
+ border: 1px solid var(--border); }
145
+ .btn:hover, .btn:focus-visible { color: var(--foreground); background: var(--muted); }
146
+
147
+ .footer { margin: 40px 0 0; text-align: center; }
148
+ .footer p { margin: 0; font-size: 12px; line-height: 1.5; color: var(--muted-foreground); opacity: 0.75; }
149
+ .footer a { text-decoration: underline; }
150
+ `.trim();
151
+
152
+ /**
153
+ * What every page these modules serve loads, and nothing else.
154
+ *
155
+ * The font stylesheet and the faces it names are the only two origins any of
156
+ * them reaches; `default-src 'none'` closes the rest. No `script-src`, because
157
+ * these pages have no script — the dashboard, which has one listener, extends
158
+ * this rather than replacing it.
159
+ */
160
+ export const PAGE_CSP =
161
+ "frame-ancestors 'none'; default-src 'none'; " +
162
+ "style-src 'unsafe-inline' https://fonts.googleapis.com; " +
163
+ 'font-src https://fonts.gstatic.com';
164
+
165
+ /**
166
+ * Headers every page here answers with.
167
+ *
168
+ * `frame-ancestors` in both spellings: one of these pages asks for the endpoint
169
+ * token and another lists the owner's accounts, and framing either is the cheap
170
+ * half of a UI-redress attack. The URLs carry a `client_id`, a `redirect_uri`,
171
+ * or a one-time key, none of which belongs in a Referer.
172
+ */
173
+ export const PAGE_HEADERS: Readonly<Record<string, string>> = {
174
+ 'content-type': 'text/html; charset=utf-8',
175
+ 'content-security-policy': PAGE_CSP,
176
+ 'x-frame-options': 'DENY',
177
+ 'referrer-policy': 'no-referrer',
178
+ };