@agentshouse/kit 0.0.1 → 0.1.0-alpha.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1696 -2
  3. package/bin/connect-linux.sh +127 -0
  4. package/bin/kit.js +1023 -0
  5. package/package.json +33 -3
  6. package/src/acp.js +99 -0
  7. package/src/adapter-mcp.js +44 -0
  8. package/src/adapter.js +252 -0
  9. package/src/agent-logins.js +65 -0
  10. package/src/agents.js +449 -0
  11. package/src/apply.js +29 -0
  12. package/src/attach.js +52 -0
  13. package/src/binding.js +90 -0
  14. package/src/blocks.js +156 -0
  15. package/src/bridge.js +449 -0
  16. package/src/bytes.js +89 -0
  17. package/src/channel.js +37 -0
  18. package/src/command.js +109 -0
  19. package/src/configuration.js +465 -0
  20. package/src/connector-login.js +91 -0
  21. package/src/connector.js +107 -0
  22. package/src/continuation.js +61 -0
  23. package/src/contract.js +363 -0
  24. package/src/conversation-originals.js +143 -0
  25. package/src/custom/authorization.js +63 -0
  26. package/src/declaration.js +35 -0
  27. package/src/delivery.js +191 -0
  28. package/src/directory.js +102 -0
  29. package/src/draft.js +89 -0
  30. package/src/google/calendar.js +177 -0
  31. package/src/google/declared.js +146 -0
  32. package/src/google/drive.js +435 -0
  33. package/src/google/gateway.js +166 -0
  34. package/src/google/gmail.js +249 -0
  35. package/src/google/id.js +1 -0
  36. package/src/google/oauth.js +273 -0
  37. package/src/google/source.js +25 -0
  38. package/src/hook.js +376 -0
  39. package/src/host.js +121 -0
  40. package/src/house-main.js +79 -0
  41. package/src/house.js +1085 -0
  42. package/src/inbound.js +175 -0
  43. package/src/instance/google.js +1 -0
  44. package/src/launcher-main.js +19 -0
  45. package/src/launcher.js +276 -0
  46. package/src/local-files.js +113 -0
  47. package/src/login.js +274 -0
  48. package/src/mcp.js +71 -0
  49. package/src/migrator.js +248 -0
  50. package/src/mount.js +582 -0
  51. package/src/operation.js +20 -0
  52. package/src/original.js +631 -0
  53. package/src/peer.js +124 -0
  54. package/src/plan.js +57 -0
  55. package/src/question.js +316 -0
  56. package/src/refusal.js +79 -0
  57. package/src/repository.js +521 -0
  58. package/src/session-bin/house +2 -0
  59. package/src/settlement.js +57 -0
  60. package/src/setup.js +185 -0
  61. package/src/source-originals.js +116 -0
  62. package/src/source.js +286 -0
  63. package/src/starter-set/grilling/LICENSE +21 -0
  64. package/src/starter-set/grilling/SKILL.md +30 -0
  65. package/src/starter-set/grilling/agents/openai.yaml +3 -0
  66. package/src/starter-set.js +17 -0
  67. package/src/state.js +157 -0
  68. package/src/sync-library.js +293 -0
  69. package/src/sync-state.js +116 -0
  70. package/src/sync.js +477 -0
  71. package/src/telegram.js +1045 -0
  72. package/src/terminal.js +57 -0
  73. package/src/tmux.js +119 -0
  74. package/src/worker.js +756 -0
  75. package/src/workspace.js +500 -0
package/package.json CHANGED
@@ -1,7 +1,37 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.0.1",
4
- "description": "House Kit. Placeholder that reserves the name; real releases come from agentshouse/kit's publish workflow.",
3
+ "version": "0.1.0-alpha.1",
5
4
  "license": "MIT",
6
- "repository": { "type": "git", "url": "git+https://github.com/agentshouse/kit.git" }
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/agentshouse/kit.git"
8
+ },
9
+ "private": false,
10
+ "type": "module",
11
+ "bin": {
12
+ "kit": "bin/kit.js"
13
+ },
14
+ "dependencies": {
15
+ "@agentclientprotocol/sdk": "1.5.0",
16
+ "@agentshouse/mdmodel": "0.1.0-alpha.12",
17
+ "yaml": "2.9.1",
18
+ "zod": "4.6.5"
19
+ },
20
+ "packageManager": "npm@11.19.0",
21
+ "engines": {
22
+ "node": "24.21.0",
23
+ "npm": "11.19.0"
24
+ },
25
+ "devEngines": {
26
+ "runtime": {
27
+ "name": "node",
28
+ "version": "24.21.0",
29
+ "onFail": "error"
30
+ },
31
+ "packageManager": {
32
+ "name": "npm",
33
+ "version": "11.19.0",
34
+ "onFail": "error"
35
+ }
36
+ }
7
37
  }
package/src/acp.js ADDED
@@ -0,0 +1,99 @@
1
+ import { Readable, Writable } from "node:stream";
2
+ import { client, ndJsonStream, RequestError } from "@agentclientprotocol/sdk";
3
+ import {
4
+ ACP_CLIENT_INFO,
5
+ ADAPTER_EXIT_GRACE_MS,
6
+ ELICITATION_CREATE,
7
+ REQUEST_PERMISSION,
8
+ SESSION_UPDATE,
9
+ } from "./contract.js";
10
+ import { KitRefusal } from "./refusal.js";
11
+
12
+ function observedStream(adapter, observed) {
13
+ const framed = ndJsonStream(Writable.toWeb(adapter.stdin), Readable.toWeb(adapter.stdout));
14
+ const writer = framed.writable.getWriter();
15
+ return {
16
+ readable: framed.readable,
17
+ writable: new WritableStream({
18
+ write: (message) => {
19
+ if (message?.method === "initialize") observed.initialize = message.params;
20
+ return writer.write(message);
21
+ },
22
+ close: () => writer.close(),
23
+ abort: (reason) => writer.abort(reason),
24
+ }),
25
+ };
26
+ }
27
+
28
+ export function acpClient(adapter, handlers) {
29
+ const app = client({ name: ACP_CLIENT_INFO.name });
30
+ if (handlers.onPermission !== undefined) {
31
+ app.onRequest(REQUEST_PERMISSION, ({ params }) => handlers.onPermission(params));
32
+ }
33
+ if (handlers.onElicitation !== undefined) {
34
+ app.onRequest(ELICITATION_CREATE, ({ params, requestId }) =>
35
+ handlers.onElicitation({ id: requestId, params }),
36
+ );
37
+ }
38
+ if (handlers.onNativeQuestion !== undefined) {
39
+ app.onRequest(
40
+ "x.ai/ask_user_question",
41
+ (value) => value,
42
+ ({ params, requestId }) => handlers.onNativeQuestion({ id: requestId, params }),
43
+ );
44
+ }
45
+ app.onNotification(SESSION_UPDATE, ({ params }) => handlers.onUpdate(params));
46
+
47
+ const observed = { initialize: null };
48
+ let failure = null;
49
+ let announce;
50
+ const stopped = new Promise((resolve) => {
51
+ announce = resolve;
52
+ });
53
+ const fail = (refusal) => {
54
+ failure ??= refusal;
55
+ announce(failure);
56
+ };
57
+ adapter.on("error", () => fail(new KitRefusal("kit_adapter_unavailable")));
58
+ adapter.on("close", (status) => fail(new KitRefusal("kit_adapter_exited", `${status}`)));
59
+ adapter.stdin.on("error", () => undefined);
60
+ const connection = app.connect(observedStream(adapter, observed));
61
+
62
+ async function refusalFor(error) {
63
+ if (error instanceof RequestError) {
64
+ const refusal = new KitRefusal("kit_adapter_failed", error.message);
65
+ refusal.protocolCode = error.code;
66
+ return refusal;
67
+ }
68
+ if (failure !== null) return failure;
69
+ let grace;
70
+ try {
71
+ return await Promise.race([
72
+ stopped,
73
+ new Promise((resolve) => {
74
+ grace = setTimeout(
75
+ () => resolve(new KitRefusal("kit_adapter_failed", "adapter channel closed")),
76
+ ADAPTER_EXIT_GRACE_MS,
77
+ ).unref();
78
+ }),
79
+ ]);
80
+ } finally {
81
+ clearTimeout(grace);
82
+ }
83
+ }
84
+
85
+ return {
86
+ declaration: () => observed.initialize,
87
+ cancel(params) {
88
+ return connection.agent.notify("session/cancel", params);
89
+ },
90
+ async request(method, params) {
91
+ if (failure !== null) throw failure;
92
+ try {
93
+ return await connection.agent.request(method, params);
94
+ } catch (error) {
95
+ throw await refusalFor(error);
96
+ }
97
+ },
98
+ };
99
+ }
@@ -0,0 +1,44 @@
1
+ import { COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS, MCP_PROTOCOL_VERSION } from "./contract.js";
2
+ import { bridgeCall } from "./mcp.js";
3
+ import { KitRefusal } from "./refusal.js";
4
+
5
+ function readableDiscovery(discovery) {
6
+ const identity = discovery?._meta?.["io.modelcontextprotocol/serverInfo"];
7
+ return Array.isArray(discovery?.supportedVersions) &&
8
+ discovery.supportedVersions.includes(MCP_PROTOCOL_VERSION) &&
9
+ typeof discovery.capabilities === "object" && discovery.capabilities !== null &&
10
+ !Array.isArray(discovery.capabilities) &&
11
+ typeof identity?.name === "string" && typeof identity?.version === "string" &&
12
+ (discovery.instructions === undefined || typeof discovery.instructions === "string");
13
+ }
14
+
15
+ async function commandHouseReply(bridge, request) {
16
+ const deadline = AbortSignal.timeout(COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS);
17
+ try {
18
+ return await bridgeCall(bridge, request, deadline);
19
+ } catch (refusal) {
20
+ if (deadline.aborted) throw new KitRefusal("kit_mcp_discovery_timeout");
21
+ throw refusal;
22
+ }
23
+ }
24
+
25
+ export async function commandHouseInstructions(bridge) {
26
+ const frame = await commandHouseReply(bridge, { method: "server/discover", params: {} });
27
+ if (frame.error !== undefined || !readableDiscovery(frame.result) || !frame.result.instructions?.trim()) {
28
+ throw new KitRefusal("kit_mcp_discovery_unreadable");
29
+ }
30
+ return frame.result.instructions;
31
+ }
32
+
33
+ export async function commandHouseRequest(bridge) {
34
+ const frame = await commandHouseReply(bridge, { method: "tools/call", params: { name: "session_get_request", arguments: {} } });
35
+ if (frame.error !== undefined || frame.result?.isError === true) throw new KitRefusal("kit_run_input_unreadable");
36
+ try {
37
+ const content = frame.result.content;
38
+ const input = JSON.parse(content.find((item) => item.type === "text").text);
39
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error();
40
+ return input;
41
+ } catch {
42
+ throw new KitRefusal("kit_run_input_unreadable");
43
+ }
44
+ }
package/src/adapter.js ADDED
@@ -0,0 +1,252 @@
1
+ import { spawn } from "node:child_process";
2
+ import { delimiter } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { acpClient } from "./acp.js";
5
+ import { inheritedEnvironment, withoutHouseCredentials } from "./command.js";
6
+ import {
7
+ ACP_AUTH_REQUIRED,
8
+ ACP_CLIENT_CAPABILITIES,
9
+ ACP_CLIENT_INFO,
10
+ ADAPTER_CONTRACTS,
11
+ ADAPTER_KINDS,
12
+ BRIDGE_NONCE_ENVIRONMENT,
13
+ BRIDGE_SOCKET_ENVIRONMENT,
14
+ COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS,
15
+ COMMAND_ADAPTER_SESSION_TIMEOUT_MS,
16
+ WORKER_INHERITED_ENVIRONMENT,
17
+ } from "./contract.js";
18
+ import { KitRefusal } from "./refusal.js";
19
+
20
+ const HOUSE_CLI_HOME = fileURLToPath(new URL("session-bin", import.meta.url));
21
+
22
+ function requiredAdapter(contract) {
23
+ return (
24
+ `requires ${contract.release.package} ${contract.release.minimum} or a later ` +
25
+ `${contract.release.minimum.split(".")[0]}.x release ` +
26
+ `for ${contract.kind} contract ${contract.version}`
27
+ );
28
+ }
29
+
30
+ function contractMismatch(contract, location = null) {
31
+ const required =
32
+ contract === undefined
33
+ ? `this Kit compiles ${ADAPTER_KINDS.join(" and ")}`
34
+ : requiredAdapter(contract);
35
+ return new KitRefusal(
36
+ "kit_adapter_contract_mismatch",
37
+ location === null ? required : `${required}; ${location}`,
38
+ );
39
+ }
40
+
41
+ export function releaseOrder(left, right) {
42
+ const parts = (release) => release.split(".").map((part) => Number(part));
43
+ const [leftParts, rightParts] = [parts(left), parts(right)];
44
+ for (let position = 0; position < 3; position += 1) {
45
+ const difference = (leftParts[position] ?? 0) - (rightParts[position] ?? 0);
46
+ if (difference !== 0) return difference < 0 ? -1 : 1;
47
+ }
48
+ return 0;
49
+ }
50
+
51
+ export function servedRelease(release, minimum) {
52
+ if (typeof release !== "string" || !/^\d+\.\d+\.\d+$/.test(release)) return false;
53
+ return (
54
+ releaseOrder(release, minimum) >= 0 &&
55
+ release.split(".")[0] === minimum.split(".")[0]
56
+ );
57
+ }
58
+
59
+ export function adapterContract(kind) {
60
+ return Object.hasOwn(ADAPTER_CONTRACTS, kind) ? ADAPTER_CONTRACTS[kind] : undefined;
61
+ }
62
+
63
+ export function compiledContract(kind, location = null) {
64
+ const contract = adapterContract(kind);
65
+ if (contract === undefined) throw contractMismatch(undefined, location);
66
+ return contract;
67
+ }
68
+
69
+ function sameDescriptor(declared, contract) {
70
+ if (typeof declared !== "object" || declared === null || Array.isArray(declared)) return false;
71
+ const named = Object.keys(declared);
72
+ if (named.length !== 3) return false;
73
+ const kinds = declared.field_kinds;
74
+ if (!Array.isArray(kinds) || kinds.length !== contract.capabilities.field_kinds.length) {
75
+ return false;
76
+ }
77
+ if (kinds.some((kind, position) => kind !== contract.capabilities.field_kinds[position])) {
78
+ return false;
79
+ }
80
+ if (declared.decline !== contract.capabilities.decline) return false;
81
+ const reasons = declared.stop_reasons;
82
+ if (typeof reasons !== "object" || reasons === null || Array.isArray(reasons)) return false;
83
+ const namedReasons = Object.keys(contract.capabilities.stop_reasons);
84
+ if (Object.keys(reasons).length !== namedReasons.length) return false;
85
+ return namedReasons.every((reason) => reasons[reason] === contract.capabilities.stop_reasons[reason]);
86
+ }
87
+
88
+ export function assertAcquiredCommandAdapter(adapter) {
89
+ const contract = adapterContract(adapter?.kind);
90
+ if (contract === undefined) throw contractMismatch(undefined);
91
+ if (adapter.contract_version !== contract.version) throw contractMismatch(contract);
92
+ if (!sameDescriptor(adapter.capabilities, contract)) throw contractMismatch(contract);
93
+ return { kind: contract.kind, contractVersion: contract.version };
94
+ }
95
+
96
+ function houseCli(bridge, path) {
97
+ return {
98
+ PATH: [HOUSE_CLI_HOME, path].filter(Boolean).join(delimiter),
99
+ [BRIDGE_SOCKET_ENVIRONMENT]: bridge.socket,
100
+ [BRIDGE_NONCE_ENVIRONMENT]: bridge.nonce,
101
+ };
102
+ }
103
+
104
+ export function startCommandAdapter(start, bridge = null) {
105
+ const environment = {
106
+ ...inheritedEnvironment(WORKER_INHERITED_ENVIRONMENT),
107
+ ...withoutHouseCredentials(start.environment),
108
+ };
109
+ return spawn(start.command[0], [...start.command.slice(1), ...start.arguments], {
110
+ cwd: start.workingDirectory,
111
+ env: bridge === null ? environment : { ...environment, ...houseCli(bridge, environment.PATH) },
112
+ stdio: ["pipe", "pipe", "inherit"],
113
+ });
114
+ }
115
+
116
+ function declaredElicitation(declaration) {
117
+ const capabilities = declaration?.clientCapabilities;
118
+ return (
119
+ capabilities?.fs?.readTextFile === false &&
120
+ capabilities?.fs?.writeTextFile === false &&
121
+ capabilities?.terminal === false &&
122
+ typeof capabilities?.elicitation?.form === "object" &&
123
+ capabilities.elicitation.form !== null &&
124
+ capabilities.elicitation.url === undefined
125
+ );
126
+ }
127
+
128
+ function assertInitialization(contract, initialized, declaration) {
129
+ if (!declaredElicitation(declaration)) throw contractMismatch(contract, "initialize declaration");
130
+ if (initialized?.protocolVersion !== contract.protocol) throw contractMismatch(contract);
131
+ const identity = contract.identity;
132
+ const release =
133
+ identity.agentInfo !== undefined
134
+ ? initialized?.agentInfo?.name === identity.agentInfo && initialized.agentInfo.version
135
+ : initialized?._meta?.[identity.initializeMeta] === true &&
136
+ initialized._meta[identity.versionMeta];
137
+ if (!servedRelease(release, contract.release.minimum)) {
138
+ throw contractMismatch(contract);
139
+ }
140
+ if (contract.sessionLoad && initialized?.agentCapabilities?.loadSession !== true) {
141
+ throw contractMismatch(contract);
142
+ }
143
+ }
144
+
145
+ export async function initializeCommandAdapter(adapter, contract, handlers) {
146
+ const acp = acpClient(adapter, handlers);
147
+ const initialized = await acp.request("initialize", {
148
+ protocolVersion: contract.protocol,
149
+ clientCapabilities: ACP_CLIENT_CAPABILITIES,
150
+ clientInfo: ACP_CLIENT_INFO,
151
+ });
152
+ assertInitialization(contract, initialized, acp.declaration());
153
+ return { acp, initialized };
154
+ }
155
+
156
+ export function stopCommandAdapter(adapter) {
157
+ adapter.stdin.end();
158
+ adapter.kill("SIGTERM");
159
+ }
160
+
161
+ async function within(promise, timeoutMs, expired) {
162
+ let deadline;
163
+ try {
164
+ return await Promise.race([
165
+ promise,
166
+ new Promise((_, reject) => {
167
+ deadline = setTimeout(() => reject(expired), timeoutMs);
168
+ }),
169
+ ]);
170
+ } finally {
171
+ clearTimeout(deadline);
172
+ }
173
+ }
174
+
175
+ async function initializedWithin(adapter, contract, initializeTimeoutMs, location) {
176
+ try {
177
+ return await within(
178
+ initializeCommandAdapter(adapter, contract, { onUpdate: () => undefined }),
179
+ initializeTimeoutMs,
180
+ contractMismatch(contract, location),
181
+ );
182
+ } catch {
183
+ throw contractMismatch(contract, location);
184
+ }
185
+ }
186
+
187
+ export async function verifyCommandAdapter(
188
+ start,
189
+ contract,
190
+ initializeTimeoutMs = COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS,
191
+ location = null,
192
+ ) {
193
+ const adapter = startCommandAdapter(start);
194
+ try {
195
+ return (await initializedWithin(adapter, contract, initializeTimeoutMs, location)).initialized;
196
+ } finally {
197
+ stopCommandAdapter(adapter);
198
+ }
199
+ }
200
+
201
+ export function openedSession(opened) {
202
+ const sessionId = opened?.sessionId;
203
+ return typeof sessionId === "string" && sessionId.length > 0 && sessionId.length <= 512
204
+ ? sessionId
205
+ : null;
206
+ }
207
+
208
+ function offeredValues(configOptions, id, contract, location) {
209
+ if (id === null) return [];
210
+ const option = configOptions.find((candidate) => candidate?.id === id);
211
+ if (option?.type !== "select") return [];
212
+ if (!Array.isArray(option.options)) throw contractMismatch(contract, location);
213
+ const values = option.options.flatMap((entry) =>
214
+ Array.isArray(entry?.options) ? entry.options : [entry],
215
+ );
216
+ if (values.some((entry) => typeof entry?.value !== "string" || typeof entry?.name !== "string")) {
217
+ throw contractMismatch(contract, location);
218
+ }
219
+ return values.map((entry) => ({ value: entry.value, name: entry.name }));
220
+ }
221
+
222
+ export async function offeredSessionOptions(start, contract, deadlines, location = null) {
223
+ const adapter = startCommandAdapter(start);
224
+ try {
225
+ const { acp } = await initializedWithin(
226
+ adapter,
227
+ contract,
228
+ deadlines.initialize ?? COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS,
229
+ location,
230
+ );
231
+ let opened;
232
+ try {
233
+ opened = await within(
234
+ acp.request("session/new", { cwd: start.workingDirectory, mcpServers: [] }),
235
+ deadlines.session ?? COMMAND_ADAPTER_SESSION_TIMEOUT_MS,
236
+ contractMismatch(contract, location),
237
+ );
238
+ } catch (refusal) {
239
+ if (refusal instanceof KitRefusal && refusal.protocolCode === ACP_AUTH_REQUIRED) return null;
240
+ throw contractMismatch(contract, location);
241
+ }
242
+ if (openedSession(opened) === null) throw contractMismatch(contract, location);
243
+ const configOptions = opened.configOptions ?? [];
244
+ if (!Array.isArray(configOptions)) throw contractMismatch(contract, location);
245
+ return {
246
+ model: offeredValues(configOptions, contract.sessionConfig.model, contract, location),
247
+ effort: offeredValues(configOptions, contract.sessionConfig.effort, contract, location),
248
+ };
249
+ } finally {
250
+ stopCommandAdapter(adapter);
251
+ }
252
+ }
@@ -0,0 +1,65 @@
1
+ const TERMINAL_ESCAPE = /\u001b\[[0-9;?]*[A-Za-z]/gu;
2
+ const HTTPS_URL = /https:\/\/[^\s\u001b]+/u;
3
+
4
+ function plain(output) {
5
+ return output.replace(TERMINAL_ESCAPE, "");
6
+ }
7
+
8
+ export const CODEX_DEVICE_CODE = Object.freeze({
9
+ ceremony(output) {
10
+ const shown = plain(output);
11
+ const code = /one-time code[^\n]*\n[^\S\n]*([A-Z0-9]+(?:-[A-Z0-9]+)+)[^\S\n]*\n/u.exec(shown);
12
+ const url = HTTPS_URL.exec(shown);
13
+ if (code === null || url === null) return null;
14
+ return {
15
+ steps: [
16
+ { kind: "visit", label: "Open the Codex device page and sign in with ChatGPT", url: url[0] },
17
+ { kind: "show", label: "Enter this one-time code on that page", text: code[1] },
18
+ ],
19
+ };
20
+ },
21
+ input: () => null,
22
+ rejected: () => false,
23
+ });
24
+
25
+ export const CLAUDE_PASTED_CODE = Object.freeze({
26
+ ceremony(output) {
27
+ const shown = plain(output);
28
+ if (!shown.includes("Paste code here")) return null;
29
+ const url = HTTPS_URL.exec(shown);
30
+ if (url === null) return null;
31
+ return {
32
+ steps: [
33
+ { kind: "visit", label: "Sign in to Claude and authorize Claude Code", url: url[0] },
34
+ {
35
+ kind: "collect",
36
+ label: "Authentication code",
37
+ name: "code",
38
+ description: "After you authorize, the Claude page shows an authentication code. Copy the whole code and paste it here.",
39
+ },
40
+ ],
41
+ };
42
+ },
43
+ input: (collected) => `${collected.code}\n`,
44
+ rejected: (answered) => plain(answered).includes("Invalid code."),
45
+ });
46
+
47
+ export const GROK_DEVICE_CODE = Object.freeze({
48
+ ceremony(output) {
49
+ const shown = plain(output);
50
+ if (!shown.includes("Waiting for authorization")) return null;
51
+ const url = HTTPS_URL.exec(shown);
52
+ const code = /(?:Confirm this code in your browser|Then enter this code):\s*([A-Z0-9-]+)/u.exec(
53
+ shown,
54
+ );
55
+ if (url === null || code === null) return null;
56
+ return {
57
+ steps: [
58
+ { kind: "visit", label: "Open Grok and sign in", url: url[0] },
59
+ { kind: "show", label: "Confirm this one-time code on that page", text: code[1] },
60
+ ],
61
+ };
62
+ },
63
+ input: () => null,
64
+ rejected: () => false,
65
+ });