@lanes-sh/link 0.2.2 → 0.3.0
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 +21 -8
- package/instructions/skills/lanes-link/SKILL.md +42 -14
- package/package.json +1 -1
- package/src/cli/argv.ts +50 -0
- package/src/cli/brand.ts +178 -0
- package/src/cli/callback-page.ts +108 -128
- package/src/cli/commands/connect/accounts.ts +5 -0
- package/src/cli/commands/connect/assertion.ts +187 -0
- package/src/cli/commands/connect/authorise.ts +61 -17
- package/src/cli/commands/connect/client.ts +37 -9
- package/src/cli/commands/connect/discover.ts +94 -0
- package/src/cli/commands/connect/family.ts +72 -0
- package/src/cli/commands/connect/index.ts +113 -115
- package/src/cli/commands/connect/method.ts +237 -0
- package/src/cli/commands/connect/outcome.ts +42 -1
- package/src/cli/commands/connect/pasted-token.ts +66 -0
- package/src/cli/commands/connect/requirements.ts +60 -8
- package/src/cli/commands/connect/setup.ts +16 -5
- package/src/cli/commands/connect/target-note.ts +34 -0
- package/src/cli/commands/identity.ts +258 -0
- package/src/cli/commands/knowledge/index.ts +390 -0
- package/src/cli/commands/knowledge/migrate.ts +180 -0
- package/src/cli/commands/knowledge/setup.ts +144 -0
- package/src/cli/commands/knowledge.ts +10 -0
- package/src/cli/commands/mcp/harnesses.ts +16 -2
- package/src/cli/commands/mcp/register.ts +9 -1
- package/src/cli/commands/mcp/stdio.ts +21 -0
- package/src/cli/commands/operate/dashboard.ts +107 -0
- package/src/cli/commands/operate/findings.ts +151 -0
- package/src/cli/commands/operate/inspect.ts +56 -158
- package/src/cli/commands/operate/outputs.ts +38 -11
- package/src/cli/commands/operate/serve.ts +3 -0
- package/src/cli/commands/operate/token.ts +1 -1
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/profile/declare.ts +154 -0
- package/src/cli/commands/profile/removal.ts +17 -0
- package/src/cli/commands/profile.ts +83 -35
- package/src/cli/commands/setup.ts +22 -6
- package/src/cli/commands/target.ts +65 -83
- package/src/cli/config-edit.ts +48 -144
- package/src/cli/config-repair.ts +186 -0
- package/src/cli/dashboard-page.ts +284 -0
- package/src/cli/dashboard-shell.ts +125 -0
- package/src/cli/identity.ts +12 -1
- package/src/cli/main.ts +68 -4
- package/src/cli/oauth-callback.ts +187 -0
- package/src/cli/oauth-exchange.ts +57 -15
- package/src/cli/oauth.ts +67 -177
- package/src/cli/output.ts +21 -5
- package/src/cli/provider-marks.ts +45 -0
- package/src/cli/runtime/open.ts +67 -49
- package/src/cli/runtime/registry.ts +60 -2
- package/src/cli/runtime/select.ts +26 -13
- package/src/cli/runtime/vault.ts +61 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection.ts +357 -0
- package/src/cli/usage.ts +32 -9
- package/src/connectivity/auth/README.md +7 -1
- package/src/connectivity/auth/basic/index.ts +1 -1
- package/src/connectivity/auth/index.ts +14 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +26 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +16 -2
- package/src/connectivity/auth/oauth-authcode/provider.ts +1 -1
- package/src/connectivity/auth/oauth-authcode/refresh.ts +3 -3
- package/src/connectivity/auth/oauth-jwt/README.md +33 -0
- package/src/connectivity/auth/oauth-jwt/index.ts +237 -0
- package/src/connectivity/auth/oauth-jwt/key.ts +148 -0
- package/src/connectivity/auth/resolve.ts +1 -1
- package/src/connectivity/auth/token.ts +11 -0
- package/src/connectivity/index.ts +2 -0
- package/src/connectivity/manifest/auth.ts +99 -2
- package/src/connectivity/manifest/identity.ts +12 -0
- package/src/connectivity/manifest/index.ts +3 -1
- package/src/connectivity/manifest/provider.ts +37 -8
- package/src/connectivity/manifest/requirements.ts +109 -6
- package/src/deployments/adapters/filesystem.ts +10 -1
- package/src/deployments/adapters/github-api.ts +106 -0
- package/src/deployments/adapters/github-commit.ts +103 -0
- package/src/deployments/adapters/github-repo.ts +356 -0
- package/src/deployments/adapters/github-testing.ts +258 -0
- package/src/deployments/adapters/github.ts +125 -0
- package/src/deployments/deploy.ts +48 -19
- package/src/deployments/driver.ts +8 -1
- package/src/deployments/gcp/driver.ts +3 -1
- package/src/deployments/knowledge.ts +119 -0
- package/src/deployments/prepare.ts +2 -2
- package/src/deployments/servable.ts +81 -0
- package/src/deployments/target.ts +3 -2
- package/src/deployments/upload.ts +2 -1
- package/src/dispatch/dispatch.ts +1 -1
- package/src/profile/identity.ts +60 -0
- package/src/profile/index.ts +15 -5
- package/src/profile/knowledge.ts +124 -0
- package/src/profile/load.ts +17 -5
- package/src/profile/primitives.ts +24 -1
- package/src/profile/schema.ts +46 -3
- package/src/profile/targets.ts +74 -114
- package/src/profile/workspace.ts +71 -84
- package/src/providers/google/calendar/index.ts +2 -0
- package/src/providers/google/contacts/index.ts +2 -0
- package/src/providers/google/docs/index.ts +2 -0
- package/src/providers/google/drive/index.ts +2 -0
- package/src/providers/google/gmail/index.ts +2 -0
- package/src/providers/google/gmail-imap/index.ts +125 -0
- package/src/providers/google/index.ts +2 -1
- package/src/providers/google/shared/oauth.ts +18 -6
- package/src/providers/google/shared/service-account.ts +110 -0
- package/src/providers/google/shared/setup.ts +5 -2
- package/src/providers/google/sheets/index.ts +2 -0
- package/src/providers/google/tasks/index.ts +2 -0
- package/src/providers/identity/provider.ts +166 -0
- package/src/providers/index.ts +3 -0
- package/src/providers/owner.ts +10 -2
- package/src/providers/scopes.ts +2 -0
- package/src/providers/setup/plan.ts +31 -9
- package/src/providers/setup/provider.ts +23 -0
- package/src/providers/slack/index.ts +81 -33
- package/src/providers/slack/oauth.ts +103 -0
- package/src/providers/slack/scopes.ts +37 -0
- package/src/server/container.ts +18 -1
- package/src/server/cors.ts +252 -0
- package/src/server/dashboard.ts +208 -0
- package/src/server/endpoint.ts +45 -1
- package/src/server/generations.ts +11 -2
- package/src/server/harness.ts +7 -0
- package/src/server/index.ts +45 -6
- package/src/server/mcp/index.ts +1 -0
- package/src/server/mcp/instructions.ts +28 -1
- package/src/server/mcp/visibility.ts +33 -0
- package/src/stores/blobs/route.ts +123 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { PROVIDER_MANIFESTS } from '#providers/index.ts';
|
|
2
|
+
import { planAll, type ProviderPlan } from '#providers/setup/plan.ts';
|
|
3
|
+
import { PROVIDER_MARKS } from './provider-marks.ts';
|
|
4
|
+
import { escapeHtml } from './brand.ts';
|
|
5
|
+
import { shell } from './dashboard-shell.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The dashboard — one page answering what `status`, `setup plan` and
|
|
9
|
+
* `target list` answer between them.
|
|
10
|
+
*
|
|
11
|
+
* It renders and it does not act. Every state-changing thing on it is a command
|
|
12
|
+
* to paste, for two reasons that are not the same. Connecting is ADR-005: the
|
|
13
|
+
* consent belongs to whoever owns the browser, and the loopback listener that
|
|
14
|
+
* receives the code is the CLI's. Everything else is ADR-007: config is written
|
|
15
|
+
* from the control plane, and a page served by the endpoint is not it.
|
|
16
|
+
*
|
|
17
|
+
* So this is a reader, and the honest shape of a reader is a page that tells you
|
|
18
|
+
* the line to run. What it saves is not typing — it is knowing which line.
|
|
19
|
+
*
|
|
20
|
+
* The design is `callback-page.ts`'s, because that is the product's page and
|
|
21
|
+
* this is the same product: nothing painted (`color-scheme: light dark` over a
|
|
22
|
+
* transparent background, correct in either mode with no theme switch), Lora on
|
|
23
|
+
* the heading, and colour reserved for status rather than spent on emphasis.
|
|
24
|
+
* What differs is the width — a card centred at 460px is right for one sentence
|
|
25
|
+
* and wrong for four sections.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export interface DashboardConnection {
|
|
29
|
+
/** `provider.id`, the key everything else addresses this by. */
|
|
30
|
+
readonly key: string;
|
|
31
|
+
readonly provider: string;
|
|
32
|
+
readonly account: string;
|
|
33
|
+
/** `active`, `unauthorized`, `disabled`, or `not reconciled`. */
|
|
34
|
+
readonly state: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DashboardView {
|
|
38
|
+
/** The profile being rendered, which `?profile=` may have chosen. */
|
|
39
|
+
readonly profile: string;
|
|
40
|
+
/** Every profile this endpoint serves, for the switcher. */
|
|
41
|
+
readonly profiles: readonly string[];
|
|
42
|
+
/** The target whose adapters are open. */
|
|
43
|
+
readonly target: string;
|
|
44
|
+
/** Every target the profile declares, live or not. */
|
|
45
|
+
readonly targets: readonly string[];
|
|
46
|
+
readonly connections: readonly DashboardConnection[];
|
|
47
|
+
/** `oauth_apps` keys, so a profile with its own client is described as having one. */
|
|
48
|
+
readonly ownClients: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Every command this page renders, built in one place.
|
|
53
|
+
*
|
|
54
|
+
* `--profile` and `--target` on all of them, unconditionally. The shell a line
|
|
55
|
+
* is pasted into resolves both for itself — from `LANES_LINK_PROFILE` and
|
|
56
|
+
* `LANES_LINK_TARGET`, or from the workspace default — and a page that shows
|
|
57
|
+
* you one profile while handing you a command that silently acts on another is
|
|
58
|
+
* worse than one that shows nothing. It is also the rule `resolveSelection`
|
|
59
|
+
* already follows by refusing to guess.
|
|
60
|
+
*/
|
|
61
|
+
function command(view: DashboardView, rest: string): string {
|
|
62
|
+
return `lanes link ${rest} --profile ${view.profile} --target ${view.target}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A command, shown in full.
|
|
67
|
+
*
|
|
68
|
+
* Kept for the two places where the command *is* the label — the sign-in page
|
|
69
|
+
* and the footer — because there is no provider name to carry it. Everywhere
|
|
70
|
+
* else the line is on the button, not on the page; see `copyButton`.
|
|
71
|
+
*/
|
|
72
|
+
function commandLine(line: string): string {
|
|
73
|
+
const text = escapeHtml(line);
|
|
74
|
+
return (
|
|
75
|
+
`<div class="cmd"><code>${text}</code>` +
|
|
76
|
+
`<button class="btn copy" type="button" data-copy="${text}" aria-label="Copy command">copy</button></div>`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The command, on the button rather than beside it.
|
|
82
|
+
*
|
|
83
|
+
* What this trades away is worth naming: the line is no longer selectable by
|
|
84
|
+
* hand, so a browser with no clipboard API leaves no way to get it. That is
|
|
85
|
+
* narrower than it sounds — `navigator.clipboard` needs a secure context, and
|
|
86
|
+
* `http://127.0.0.1` is one by definition, which is the only address this page
|
|
87
|
+
* is ever served on. The `title` carries the text for anyone who wants to read
|
|
88
|
+
* before pasting, and `lanes link setup plan` prints the same lines.
|
|
89
|
+
*/
|
|
90
|
+
function copyButton(line: string, label: string): string {
|
|
91
|
+
const text = escapeHtml(line);
|
|
92
|
+
return (
|
|
93
|
+
`<button class="btn copy" type="button" data-copy="${text}" title="${text}" ` +
|
|
94
|
+
`aria-label="Copy the command that connects ${escapeHtml(label)}">copy</button>`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The provider's mark, or letters standing in for one.
|
|
100
|
+
*
|
|
101
|
+
* `aria-hidden` on both: every caller puts the name beside it, and a screen
|
|
102
|
+
* reader announcing "GitHub, GitHub" is worse than one announcing it once.
|
|
103
|
+
*
|
|
104
|
+
* The fallback takes the first letter of the family and two of the member,
|
|
105
|
+
* because the plain two collapse a catalogue holding four `icloud_*` entries
|
|
106
|
+
* and two `*_mcp` ones into a handful of identical marks — `ICA` and `ICO`
|
|
107
|
+
* rather than `IC` twice.
|
|
108
|
+
*/
|
|
109
|
+
function mark(id: string): string {
|
|
110
|
+
const path = PROVIDER_MARKS[id];
|
|
111
|
+
if (path) {
|
|
112
|
+
return (
|
|
113
|
+
'<svg class="glyph" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
|
|
114
|
+
`<path d="${path}"/></svg>`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const [family, member] = id.split('_');
|
|
119
|
+
const letters = member ? `${family![0]}${member.slice(0, 2)}` : id.slice(0, 2);
|
|
120
|
+
return `<span class="glyph letters" aria-hidden="true">${escapeHtml(letters.toUpperCase())}</span>`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The state, as one of the design system's badge variants.
|
|
125
|
+
*
|
|
126
|
+
* "Gold for positive, neutral tokens otherwise" — so `active` is the only gold
|
|
127
|
+
* one, and `unauthorized` is *neutral* rather than red. It is not an error: the
|
|
128
|
+
* credential is absent from this target's store, which is a thing to do, and
|
|
129
|
+
* `--destructive` is reserved for something having gone wrong. `disabled` is
|
|
130
|
+
* quieter still, being a row that config no longer declares.
|
|
131
|
+
*/
|
|
132
|
+
function statusPill(state: string): string {
|
|
133
|
+
const kind = state === 'active' ? 'positive' : state === 'disabled' ? 'quiet' : 'neutral';
|
|
134
|
+
return `<span class="pill ${kind}">${escapeHtml(state)}</span>`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function connectionsSection(view: DashboardView): string {
|
|
138
|
+
if (view.connections.length === 0) {
|
|
139
|
+
return '<p class="empty">Nothing connected in this profile yet. Pick one below.</p>';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const rows = view.connections
|
|
143
|
+
.map((connection) => {
|
|
144
|
+
// A connection that is not active is one the reader can act on, and the
|
|
145
|
+
// action is the same `connect` that made it — re-running against an
|
|
146
|
+
// existing key repairs it rather than adding a second account. An active
|
|
147
|
+
// one has nothing to do, so it carries no button.
|
|
148
|
+
const repair =
|
|
149
|
+
connection.state === 'active'
|
|
150
|
+
? ''
|
|
151
|
+
: copyButton(command(view, `connect ${connection.key}`), connection.key);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
'<div class="row">' +
|
|
155
|
+
mark(connection.provider) +
|
|
156
|
+
`<code class="key">${escapeHtml(connection.key)}</code>` +
|
|
157
|
+
statusPill(connection.state) +
|
|
158
|
+
`<span class="account">${escapeHtml(connection.account)}</span>` +
|
|
159
|
+
repair +
|
|
160
|
+
'</div>'
|
|
161
|
+
);
|
|
162
|
+
})
|
|
163
|
+
.join('\n');
|
|
164
|
+
|
|
165
|
+
return `<div class="rows">${rows}</div>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* One provider, one line: what it is called and the line that connects it.
|
|
170
|
+
*
|
|
171
|
+
* Everything else a card used to carry is gone — the description, whether it
|
|
172
|
+
* opens a browser, who operates its OAuth client. None of it is lost: it is all
|
|
173
|
+
* in `lanes link setup plan` and in the `setup_provider` capability, both of
|
|
174
|
+
* which exist to answer "what does connecting this involve" at the length that
|
|
175
|
+
* question deserves. This page answers a different one — what is there, and
|
|
176
|
+
* what would I paste — and a paragraph per provider buried it.
|
|
177
|
+
*
|
|
178
|
+
* `plan.command` is built by the same `planFor` those two render, so the line
|
|
179
|
+
* copied here and the line an agent suggests cannot drift.
|
|
180
|
+
*/
|
|
181
|
+
function providerCard(plan: ProviderPlan): string {
|
|
182
|
+
return (
|
|
183
|
+
'<div class="row">' +
|
|
184
|
+
mark(plan.id) +
|
|
185
|
+
`<span class="name">${escapeHtml(plan.name)}</span>` +
|
|
186
|
+
copyButton(plan.command, plan.name) +
|
|
187
|
+
'</div>'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function catalogue(view: DashboardView): { available: string; another: string } {
|
|
192
|
+
const plans = planAll(PROVIDER_MANIFESTS, {
|
|
193
|
+
profile: view.profile,
|
|
194
|
+
connections: view.connections.map((connection) => connection.key),
|
|
195
|
+
ownClients: view.ownClients,
|
|
196
|
+
target: view.target,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// The same split `setup_overview` makes: never connected is an invitation,
|
|
200
|
+
// and already connected is only an invitation when a second account means
|
|
201
|
+
// something (ADR — `multiAccount` is the credential test, not a preference).
|
|
202
|
+
const available = plans.filter((plan) => plan.connected.length === 0);
|
|
203
|
+
const more = plans.filter((plan) => plan.connected.length > 0 && plan.multiAccount);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
available: available.map(providerCard).join('\n'),
|
|
207
|
+
another: more.map(providerCard).join('\n'),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function switcher(view: DashboardView): string {
|
|
212
|
+
// Links, not a form: choosing a profile here changes what this page shows and
|
|
213
|
+
// nothing else. The workspace default is config, and config is written from
|
|
214
|
+
// the CLI.
|
|
215
|
+
const profiles = view.profiles
|
|
216
|
+
.map((name) => {
|
|
217
|
+
const current = name === view.profile;
|
|
218
|
+
const label = escapeHtml(name);
|
|
219
|
+
return current
|
|
220
|
+
? `<span class="chip on">${label}</span>`
|
|
221
|
+
: `<a class="chip" href="?profile=${encodeURIComponent(name)}">${label}</a>`;
|
|
222
|
+
})
|
|
223
|
+
.join('');
|
|
224
|
+
|
|
225
|
+
// Every declared target, with the one whose adapters are actually open marked.
|
|
226
|
+
// The others are real — they are where `--target` would write — but nothing on
|
|
227
|
+
// this page is reading from them.
|
|
228
|
+
const targets = view.targets
|
|
229
|
+
.map((name) =>
|
|
230
|
+
name === view.target
|
|
231
|
+
? `<span class="chip on">${escapeHtml(name)}</span>`
|
|
232
|
+
: `<span class="chip off" title="declared, but not the one this endpoint opened">${escapeHtml(name)}</span>`,
|
|
233
|
+
)
|
|
234
|
+
.join('');
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
'<div class="switch">' +
|
|
238
|
+
`<div class="group"><span class="eyebrow">profile</span>${profiles}</div>` +
|
|
239
|
+
`<div class="group"><span class="eyebrow">target</span>${targets}</div>` +
|
|
240
|
+
'</div>'
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function dashboardPage(view: DashboardView): Response {
|
|
245
|
+
const { available, another } = catalogue(view);
|
|
246
|
+
|
|
247
|
+
const body =
|
|
248
|
+
`<h1>Lanes Link</h1>${switcher(view)}` +
|
|
249
|
+
`<h2 class="eyebrow">Connections</h2>${connectionsSection(view)}` +
|
|
250
|
+
(available ? `<h2 class="eyebrow">Available</h2><div class="rows">${available}</div>` : '') +
|
|
251
|
+
(another ? `<h2 class="eyebrow">Connect another account</h2><div class="rows">${another}</div>` : '') +
|
|
252
|
+
'<h2 class="eyebrow">Elsewhere</h2>' +
|
|
253
|
+
'<p class="empty">What is reachable, and what is wrong with it:</p>' +
|
|
254
|
+
commandLine(command(view, 'status')) +
|
|
255
|
+
commandLine(command(view, 'doctor'));
|
|
256
|
+
|
|
257
|
+
return shell(body, 'Lanes Link', 200);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* What an unauthenticated browser gets.
|
|
262
|
+
*
|
|
263
|
+
* The narrow card rather than the wide page, because it is one sentence — and
|
|
264
|
+
* it names the command instead of asking for the token, since the command is
|
|
265
|
+
* what puts the token in the URL in the first place. A password field here
|
|
266
|
+
* would be a second way in to guard for no benefit: whoever can run the command
|
|
267
|
+
* is already whoever the token would prove them to be.
|
|
268
|
+
*
|
|
269
|
+
* The one command on this page carries neither `--profile` nor `--target`,
|
|
270
|
+
* against the rule every other command here follows. Both are names, and this
|
|
271
|
+
* page answers before authentication — ADR-018 stopped `/health` naming
|
|
272
|
+
* profiles to an anonymous caller for exactly that reason, and a 401 that
|
|
273
|
+
* recites the workspace's profile list would put it back.
|
|
274
|
+
*/
|
|
275
|
+
export function dashboardSignInPage(status: number): Response {
|
|
276
|
+
return shell(
|
|
277
|
+
'<div class="narrow"><h1>Not signed in</h1>' +
|
|
278
|
+
'<p class="desc">This page opens from the terminal that is serving it.</p>' +
|
|
279
|
+
commandLine('lanes link dashboard') +
|
|
280
|
+
'</div>',
|
|
281
|
+
'Lanes Link',
|
|
282
|
+
status,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { escapeHtml, FONTS, FOOTER, PAGE_CSP, PAGE_HEADERS, TOKENS } from './brand.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The dashboard's chrome: its layout, its one listener, and the document they
|
|
5
|
+
* hang in.
|
|
6
|
+
*
|
|
7
|
+
* Split from `dashboard-page.ts` because the two answer different questions —
|
|
8
|
+
* that file decides what a row says, this one decides what a row looks like and
|
|
9
|
+
* what the response is allowed to load. Everything shared with the two
|
|
10
|
+
* authorization pages, which is every colour and every face, is in `brand.ts`;
|
|
11
|
+
* what is left here is the one thing this surface does not share with them, a
|
|
12
|
+
* wide list rather than a centred card.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const STYLE = `
|
|
16
|
+
body { padding: 40px 24px; }
|
|
17
|
+
.wrap { width: 100%; max-width: 620px; margin: 0 auto; }
|
|
18
|
+
.narrow { text-align: center; padding: 32px 0; }
|
|
19
|
+
h1 { margin: 0 0 20px; }
|
|
20
|
+
h2 { margin: 30px 0 8px; }
|
|
21
|
+
.desc { margin: 0 0 12px; font-size: 14px; line-height: 1.6; color: var(--muted-foreground); }
|
|
22
|
+
.empty { margin: 0 0 12px; font-size: 14px; line-height: 1.6; color: var(--muted-foreground);
|
|
23
|
+
opacity: 0.8; }
|
|
24
|
+
|
|
25
|
+
/* Profile and target selectors. The current one is not gold: gold says a thing
|
|
26
|
+
is good, and which profile you are looking at is not a verdict. */
|
|
27
|
+
.switch { display: flex; flex-wrap: wrap; gap: 20px; padding-bottom: 4px; }
|
|
28
|
+
.group { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
29
|
+
.chip { display: inline-block; padding: 3px 10px; font-size: 13px; border-radius: 999px;
|
|
30
|
+
border: 1px solid var(--border); color: var(--muted-foreground); text-decoration: none; }
|
|
31
|
+
.chip:hover { color: var(--foreground); background: var(--muted); }
|
|
32
|
+
.chip.on { color: var(--foreground); background: var(--muted); font-weight: 500; }
|
|
33
|
+
.chip.off { border-style: dashed; opacity: 0.5; }
|
|
34
|
+
|
|
35
|
+
/* One line per item: mark, name, whatever qualifies it, then the button hard
|
|
36
|
+
right. \`min-width: 0\` on the row lets the name ellipsize rather than push
|
|
37
|
+
the button off the end. */
|
|
38
|
+
.rows { display: flex; flex-direction: column; }
|
|
39
|
+
.row { display: flex; align-items: center; gap: 10px; min-width: 0;
|
|
40
|
+
padding: 7px 10px 7px 4px; border-radius: 6px; }
|
|
41
|
+
.row:hover { background: var(--muted); }
|
|
42
|
+
.glyph { flex: none; display: inline-flex; align-items: center; justify-content: center;
|
|
43
|
+
width: 17px; height: 17px; margin: 0 3px; opacity: 0.85; }
|
|
44
|
+
/* The stand-in wants the box the real marks do not: bare letters read as a mark
|
|
45
|
+
that failed to load, where a brand mark reads as itself. */
|
|
46
|
+
.glyph.letters { width: 23px; height: 23px; margin: 0; border-radius: 6px; font-size: 8.5px;
|
|
47
|
+
font-weight: 500; letter-spacing: 0.02em; color: var(--muted-foreground);
|
|
48
|
+
border: 1px solid var(--border); opacity: 1; }
|
|
49
|
+
.name, .key { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
50
|
+
.name { font-size: 14px; }
|
|
51
|
+
.key { font-family: var(--mono); font-size: 13px; }
|
|
52
|
+
.account { font-size: 12.5px; color: var(--muted-foreground); opacity: 0.8; min-width: 0;
|
|
53
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
54
|
+
|
|
55
|
+
/* Always visible, never revealed on hover: the button is the only way to get the
|
|
56
|
+
command now that the line itself is not on the page, and an affordance you
|
|
57
|
+
have to find by sweeping the mouse is one most readers never find. */
|
|
58
|
+
.copy { margin-left: auto; opacity: 0.6; }
|
|
59
|
+
.row:hover .copy { opacity: 1; }
|
|
60
|
+
.copy:hover, .copy:focus-visible { opacity: 1; }
|
|
61
|
+
|
|
62
|
+
/* The two places the command is still the label, so it stays visible. */
|
|
63
|
+
.cmd { display: flex; align-items: center; gap: 8px; margin-top: 8px; padding: 8px 10px;
|
|
64
|
+
background: var(--card); border: 1px dashed var(--border); border-radius: 10px; }
|
|
65
|
+
.cmd code { flex: 1; font-size: 12.5px; line-height: 1.5; word-break: break-all; }
|
|
66
|
+
.cmd .copy { opacity: 0.8; margin-left: 0; }
|
|
67
|
+
`.trim();
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The copy button, and nothing else.
|
|
71
|
+
*
|
|
72
|
+
* Inline because a separate file would be the first static asset this
|
|
73
|
+
* repository serves, and one listener is not worth that. It reads the command
|
|
74
|
+
* from a data attribute rather than from the DOM text so that what is copied is
|
|
75
|
+
* exactly what was rendered, whitespace included.
|
|
76
|
+
*/
|
|
77
|
+
const SCRIPT = `
|
|
78
|
+
document.addEventListener('click', function (event) {
|
|
79
|
+
var button = event.target.closest('.copy');
|
|
80
|
+
if (!button || !navigator.clipboard) return;
|
|
81
|
+
navigator.clipboard.writeText(button.dataset.copy).then(function () {
|
|
82
|
+
button.textContent = 'copied';
|
|
83
|
+
setTimeout(function () { button.textContent = 'copy'; }, 1200);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
`.trim();
|
|
87
|
+
|
|
88
|
+
export function shell(inner: string, title: string, status: number): Response {
|
|
89
|
+
return new Response(
|
|
90
|
+
`<!doctype html>
|
|
91
|
+
<html lang="en">
|
|
92
|
+
<head>
|
|
93
|
+
<meta charset="utf-8">
|
|
94
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
95
|
+
${FONTS}
|
|
96
|
+
<title>${escapeHtml(title)}</title>
|
|
97
|
+
<style>
|
|
98
|
+
${TOKENS}
|
|
99
|
+
${STYLE}
|
|
100
|
+
</style>
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
<div class="wrap">
|
|
104
|
+
${inner}
|
|
105
|
+
${FOOTER}
|
|
106
|
+
</div>
|
|
107
|
+
<script>
|
|
108
|
+
${SCRIPT}
|
|
109
|
+
</script>
|
|
110
|
+
</body>
|
|
111
|
+
</html>`,
|
|
112
|
+
{
|
|
113
|
+
status,
|
|
114
|
+
headers: {
|
|
115
|
+
...PAGE_HEADERS,
|
|
116
|
+
// The shared policy plus the one thing this page has that the others do
|
|
117
|
+
// not: an inline listener, for the copy button.
|
|
118
|
+
'content-security-policy': `${PAGE_CSP}; script-src 'unsafe-inline'`,
|
|
119
|
+
// Connection keys and account names are not something to leave in a
|
|
120
|
+
// shared browser's back-forward cache.
|
|
121
|
+
'cache-control': 'no-store',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
}
|
package/src/cli/identity.ts
CHANGED
|
@@ -58,7 +58,18 @@ export async function resolveAccount(
|
|
|
58
58
|
headers: token ? { authorization: `Bearer ${token}` } : {},
|
|
59
59
|
});
|
|
60
60
|
if (!response.ok) return null;
|
|
61
|
-
|
|
61
|
+
|
|
62
|
+
const body = await response.json();
|
|
63
|
+
const primary = pluck(body, identity.field);
|
|
64
|
+
if (!primary || !identity.qualifier) return primary;
|
|
65
|
+
|
|
66
|
+
// `alice (Acme)` rather than `alice`. The bracketed half is what makes
|
|
67
|
+
// two workspaces two accounts instead of one overwritten twice, and it
|
|
68
|
+
// survives into the connection id because `idFromAccount` slugifies the
|
|
69
|
+
// whole string when there is no `@` in it — `alice_acme`, which is a row
|
|
70
|
+
// somebody can read in `status`.
|
|
71
|
+
const qualifier = pluck(body, identity.qualifier);
|
|
72
|
+
return qualifier ? `${primary} (${qualifier})` : primary;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
if (!probe.callTool) return null;
|
package/src/cli/main.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
auditVerify,
|
|
6
6
|
check,
|
|
7
7
|
configShow,
|
|
8
|
+
dashboard,
|
|
8
9
|
doctor,
|
|
9
10
|
outputs,
|
|
10
11
|
plan,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
import { profileAdd, profileDefault, profileList } from './commands/profile.ts';
|
|
20
21
|
import { removeProfile as profileRemove } from './commands/profile/remove.ts';
|
|
21
22
|
import { targetList, targetShow, targetUse } from './commands/target.ts';
|
|
23
|
+
import { identityAdd, identityList, identityRemove } from './commands/identity.ts';
|
|
22
24
|
import { setupPlan } from './commands/setup.ts';
|
|
23
25
|
import { mcpAdd, mcpList, mcpStdio, skillDocument } from './commands/mcp.ts';
|
|
24
26
|
import { deploy } from '#deployments/deploy.ts';
|
|
@@ -38,8 +40,10 @@ import {
|
|
|
38
40
|
vaultRemove,
|
|
39
41
|
vaultSet,
|
|
40
42
|
} from './commands/owner.ts';
|
|
43
|
+
import { knowledgeShow, knowledgeUse } from './commands/knowledge.ts';
|
|
41
44
|
import { update } from './commands/update.ts';
|
|
42
|
-
import { globalFlags, ownerFlags, parseArgv, text } from './argv.ts';
|
|
45
|
+
import { all, globalFlags, knowledgeFlags, ownerFlags, parseArgv, text } from './argv.ts';
|
|
46
|
+
import { assertKnownFlags, requireSelection } from './selection.ts';
|
|
43
47
|
import { PROGRAM, USAGE } from './usage.ts';
|
|
44
48
|
import { version } from './version.ts';
|
|
45
49
|
import { print } from './output.ts';
|
|
@@ -74,6 +78,14 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
74
78
|
return;
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
// Before the switch, so no command can be reached having been handed a flag it
|
|
82
|
+
// does not read or missing one it needs. One call site rather than a check per
|
|
83
|
+
// case: the reported bug was a single command building its own options literal
|
|
84
|
+
// and dropping `--target` into it, which is exactly what a per-case check
|
|
85
|
+
// leaves room for.
|
|
86
|
+
assertKnownFlags(first, second, flags);
|
|
87
|
+
await requireSelection(first, second, flags);
|
|
88
|
+
|
|
77
89
|
switch (first) {
|
|
78
90
|
case 'connect':
|
|
79
91
|
if (!second) throw new Error(`Usage: ${PROGRAM} connect <provider>`);
|
|
@@ -85,6 +97,7 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
85
97
|
nonInteractive: flags['non-interactive'] === true,
|
|
86
98
|
acceptBroadScopes: flags['accept-broad-scopes'] === true,
|
|
87
99
|
ownClient: flags['own-client'] === true,
|
|
100
|
+
auth: text(flags, 'auth'),
|
|
88
101
|
json,
|
|
89
102
|
});
|
|
90
103
|
|
|
@@ -97,8 +110,17 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
97
110
|
case 'profile':
|
|
98
111
|
switch (second) {
|
|
99
112
|
case 'add':
|
|
100
|
-
if (!rest[0])
|
|
101
|
-
|
|
113
|
+
if (!rest[0]) {
|
|
114
|
+
throw new Error(`Usage: ${PROGRAM} profile add <name> --target <name> [--target <name>]`);
|
|
115
|
+
}
|
|
116
|
+
return profileAdd(rest[0], {
|
|
117
|
+
// Read from argv rather than from `flags`, because this is the one
|
|
118
|
+
// place a flag is a list: a profile declares every target it can run
|
|
119
|
+
// on, and the parser keeps only the last value of a repeated flag.
|
|
120
|
+
targets: all(argv, 'target'),
|
|
121
|
+
nonInteractive: flags['non-interactive'] === true,
|
|
122
|
+
json,
|
|
123
|
+
});
|
|
102
124
|
case 'list':
|
|
103
125
|
case undefined:
|
|
104
126
|
return profileList({ json });
|
|
@@ -128,13 +150,35 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
128
150
|
return targetList({ ...global, json, urls: flags['urls'] === true });
|
|
129
151
|
case 'use':
|
|
130
152
|
if (!rest[0]) throw new Error(`Usage: ${PROGRAM} target use <name>`);
|
|
131
|
-
return targetUse(rest[0]
|
|
153
|
+
return targetUse(rest[0]);
|
|
132
154
|
case 'show':
|
|
133
155
|
return targetShow(rest[0], { ...global, json });
|
|
134
156
|
default:
|
|
135
157
|
throw new Error(`Unknown: ${PROGRAM} target ${second}`);
|
|
136
158
|
}
|
|
137
159
|
|
|
160
|
+
case 'identity': {
|
|
161
|
+
// Both subcommands take the same two positionals, so the usage line is
|
|
162
|
+
// built once rather than written twice with one of them going stale.
|
|
163
|
+
const [kind, value] = rest;
|
|
164
|
+
const usage = (form: string): string =>
|
|
165
|
+
`Usage: ${PROGRAM} identity ${form}\n e.g. ${PROGRAM} identity add name "Your Name" --note "for open-source work"`;
|
|
166
|
+
|
|
167
|
+
switch (second) {
|
|
168
|
+
case 'add':
|
|
169
|
+
if (!kind || !value) throw new Error(usage('add <kind> <value> [--note text]'));
|
|
170
|
+
return identityAdd(kind, value, { ...global, note: text(flags, 'note'), json });
|
|
171
|
+
case 'list':
|
|
172
|
+
case undefined:
|
|
173
|
+
return identityList({ ...global, json });
|
|
174
|
+
case 'remove':
|
|
175
|
+
if (!kind || !value) throw new Error(usage('remove <kind> <value>'));
|
|
176
|
+
return identityRemove(kind, value, { ...global, json });
|
|
177
|
+
default:
|
|
178
|
+
throw new Error(`Unknown: ${PROGRAM} identity ${second}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
138
182
|
case 'policy':
|
|
139
183
|
switch (second) {
|
|
140
184
|
case 'list':
|
|
@@ -221,6 +265,19 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
221
265
|
throw new Error(`Unknown: ${PROGRAM} skills ${second}`);
|
|
222
266
|
}
|
|
223
267
|
|
|
268
|
+
// Beside `memory` and `skills` because it is the question they raise next:
|
|
269
|
+
// those two say what is stored, and this says where it is kept.
|
|
270
|
+
case 'knowledge':
|
|
271
|
+
switch (second) {
|
|
272
|
+
case 'show':
|
|
273
|
+
case undefined:
|
|
274
|
+
return knowledgeShow(knowledgeFlags(flags));
|
|
275
|
+
case 'use':
|
|
276
|
+
return knowledgeUse(rest[0], knowledgeFlags(flags));
|
|
277
|
+
default:
|
|
278
|
+
throw new Error(`Unknown: ${PROGRAM} knowledge ${second}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
224
281
|
case 'vault':
|
|
225
282
|
switch (second) {
|
|
226
283
|
case 'list':
|
|
@@ -252,6 +309,13 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
252
309
|
case 'outputs':
|
|
253
310
|
return outputs({ ...global, show, json });
|
|
254
311
|
|
|
312
|
+
// Beside `outputs` for the same reason `tools` is: it answers the next
|
|
313
|
+
// question a person has rather than the next one an agent has. `outputs`
|
|
314
|
+
// hands a harness a URL and a token; this opens the one page a person can
|
|
315
|
+
// read, and only a local endpoint serves it.
|
|
316
|
+
case 'dashboard':
|
|
317
|
+
return dashboard({ ...global, print: flags['print'] === true });
|
|
318
|
+
|
|
255
319
|
// Beside `outputs` because it answers the next question. `outputs` says
|
|
256
320
|
// where the endpoint is; this says what it would hand a client that asked
|
|
257
321
|
// right now — which is the only way to tell a stale client from a wrong
|