@flow-engineer/flow 0.2.0 → 0.2.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.
@@ -0,0 +1,117 @@
1
+ // `flow chat`: talk to the agent in this folder from the terminal, with no account and no channel.
2
+ // It builds the agent from src/agent.ts (buildAgent({ events }), as `flow test` does) with the
3
+ // model key in .env, and writes as the agent's first test person, the way the playground does
4
+ // (agent.simulate). Each line read from stdin is one message; the agent's replies are printed once
5
+ // its turn is over, so it works typed by a person or piped by a coding agent:
6
+ // printf 'hi\nA latte please\n' | npx @flow-engineer/flow chat
7
+ // --json prints each thing the agent sends as one JSON line. The conversation lives only as long as
8
+ // the command. Exit status: 0 done (stdin ended), 1 usage or the agent could not be built.
9
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
10
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
11
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
12
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
13
+ });
14
+ }
15
+ return path;
16
+ };
17
+ import { existsSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { createInterface } from "node:readline";
20
+ import { pathToFileURL } from "node:url";
21
+ import { parseArgs } from "node:util";
22
+ import { loadEnv } from "./report.js";
23
+ export const CHAT_LINE = ` flow chat [--channel <name>] [--person <phone>] [--json]
24
+ Talk to the agent in this folder from the terminal: one message per line
25
+ of stdin, no account needed (only the model key in .env)`;
26
+ const USAGE = `Usage:\n${CHAT_LINE}
27
+
28
+ Writes as the agent's first test person (createAgent({ testPeople })) on its first channel, unless
29
+ --person or --channel says otherwise. Exit status: 0 done, 1 usage or the agent could not be built.`;
30
+ /** Runs `flow chat`; resolves to the exit status. */
31
+ export async function chatCli(args, input = process.stdin) {
32
+ let values;
33
+ try {
34
+ ({ values } = parseArgs({ args, strict: true, options: { channel: { type: "string" }, person: { type: "string" }, json: { type: "boolean", default: false }, help: { type: "boolean", short: "h", default: false } } }));
35
+ }
36
+ catch (error) {
37
+ console.error(`flow chat: ${error.message.split(". ")[0]}\n\n${USAGE}`);
38
+ return 1;
39
+ }
40
+ if (values.help) {
41
+ console.log(USAGE);
42
+ return 0;
43
+ }
44
+ loadEnv(undefined);
45
+ const file = join(process.cwd(), "src", "agent.ts");
46
+ if (!existsSync(file)) {
47
+ console.error(`flow chat: no src/agent.ts here. Run it from your agent's folder (npx @flow-engineer/flow init makes one).`);
48
+ return 1;
49
+ }
50
+ const turns = [];
51
+ let agent;
52
+ try {
53
+ const module = await import(__rewriteRelativeImportExtension(pathToFileURL(file).href));
54
+ if (typeof module.buildAgent !== "function")
55
+ throw new Error("src/agent.ts must export buildAgent({ events }).");
56
+ agent = module.buildAgent({ events: [(event) => turns.forEach((listener) => listener(event))] });
57
+ }
58
+ catch (error) {
59
+ console.error(`flow chat: ${error.message}`);
60
+ return 1;
61
+ }
62
+ const person = values.person ? agent.testPeople().find((p) => p.phone.replace(/\D/g, "") === values.person.replace(/\D/g, "")) : agent.testPeople()[0];
63
+ const channel = values.channel ? agent.channels().find((c) => c.name === values.channel) : agent.channels()[0];
64
+ if (!person || !channel) {
65
+ console.error(!person
66
+ ? `flow chat: ${values.person ? `${values.person} is not one of the agent's testPeople` : "the agent has no testPeople"}; declare them in createAgent({ testPeople: [{ name, phone }] }).`
67
+ : `flow chat: ${values.channel ? `no channel named ${values.channel}` : "the agent has no channels"}; it has ${agent.channels().map((c) => c.name).join(", ") || "none"}.`);
68
+ return 1;
69
+ }
70
+ agent.attachPlayground(({ outbound }) => print(outbound, channel.name, values.json === true));
71
+ turns.push((event) => {
72
+ if (event.type === "model.error")
73
+ console.error(`flow chat: the model failed: ${String(event.error ?? event.message ?? "error")}`);
74
+ });
75
+ const prompt = process.stdin.isTTY && input === process.stdin;
76
+ if (!values.json)
77
+ console.error(`Talking to the agent on ${channel.name} as ${person.name} (${person.phone}). Ctrl-D ends.`);
78
+ const lines = createInterface({ input, terminal: false });
79
+ if (prompt)
80
+ process.stderr.write("you: ");
81
+ let conversationId;
82
+ for await (const line of lines) {
83
+ const text = line.trim();
84
+ if (text) {
85
+ const taken = await agent.simulate({ person: person.phone, channel: channel.name, message: text });
86
+ if (taken)
87
+ await agent.conversation((conversationId = taken.conversationId))?.whenIdle();
88
+ }
89
+ if (prompt)
90
+ process.stderr.write("you: ");
91
+ }
92
+ // A tool still running in the background answers in a turn of its own: wait for it before leaving.
93
+ while (conversationId && agent.status().backgroundTasks > 0) {
94
+ await new Promise((done) => turns.push((event) => event.type === "task.done" && done()));
95
+ await agent.conversation(conversationId)?.whenIdle();
96
+ }
97
+ await agent.flushTraces();
98
+ return 0;
99
+ }
100
+ /** One thing the agent sent, as a person would read it (or one JSON line). */
101
+ function print(outbound, channel, json) {
102
+ if (outbound.type === "typing" || outbound.type === "status" || outbound.type === "read")
103
+ return;
104
+ if (json)
105
+ return void process.stdout.write(`${JSON.stringify({ channel, ...outbound })}\n`);
106
+ if (outbound.type === "reaction")
107
+ return void process.stdout.write(`agent reacted ${outbound.emoji}\n`);
108
+ if (outbound.type === "template")
109
+ return void process.stdout.write(`agent: [template ${outbound.name}]\n`);
110
+ const extra = [
111
+ outbound.image && `[photo ${outbound.image.url}]`,
112
+ outbound.link && `[${outbound.link.label}: ${outbound.link.url}]`,
113
+ outbound.buttons?.map((b) => `[${b}]`).join(" "),
114
+ outbound.list && `[${outbound.list.button}: ${outbound.list.rows.map((r) => r.title).join(" | ")}]`,
115
+ ].filter(Boolean);
116
+ process.stdout.write(`agent: ${[outbound.text, ...extra].filter(Boolean).join(" ")}\n`);
117
+ }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // The `chinwag` command from before the rename, kept for one release: it says so on stderr (stdout
3
3
  // stays the report's, for --json), then runs the flow command (testing/src/cli.ts) as it is.
4
- console.error("chinwag is now Flow Framework (@flow-engineer/flow): run npx flow, and npm i @flow-engineer/flow.");
4
+ console.error("chinwag is now Flow Framework (@flow-engineer/flow): run npx @flow-engineer/flow, and npm i @flow-engineer/flow.");
5
5
  await import("./cli.js");
6
6
  export {};
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // The flow command. `flow new <name>` makes ./<name>/, an agent started from the café example
3
- // (testing/src/new.ts). `flow test`, or `flow` with flags, runs an agent's tests in the cloud,
2
+ // The flow command. `flow new <name>` makes ./<name>/, an agent started from the café example, and
3
+ // `flow init` the same agent in the current folder (testing/src/new.ts). `flow chat` talks to the
4
+ // agent in this folder from the terminal (testing/src/chat.ts). `flow test`, or `flow` with flags, runs an agent's tests in the cloud,
4
5
  // from the agent's folder:
5
6
  // flow test [--cloud https://flow.engineer/observe] [--runs 3] [--only late-order,refund | --failed] [--json]
6
7
  // A hosted cloud (any --cloud off this machine, the default included) needs the
@@ -15,6 +16,8 @@
15
16
  // (what each run ended as and the judge's reasons, never the messages), and --failed runs again the
16
17
  // scenarios that did not pass. --json prints one report (testing/src/report.ts) on stdout at every exit,
17
18
  // and `flow show <run>` prints one run's record from the cloud (testing/src/show.ts).
19
+ // `flow login`, `logout`, `whoami`, `setup` and `link` sign in to flow.engineer and get the agent's
20
+ // keys and its phone link (testing/src/account.ts).
18
21
  var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
19
22
  if (typeof path === "string" && /^\.\.?\//.test(path)) {
20
23
  return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
@@ -33,12 +36,15 @@ import { OBSERVE_URL, setting } from "../../src/traces.js";
33
36
  import { cloudHeaders, runInCloud, SessionStopped } from "./client.js";
34
37
  import { runPrinter } from "./print-run.js";
35
38
  import { checkEndpointCli } from "./check-endpoint.js";
39
+ import { ACCOUNT_COMMANDS, ACCOUNT_LINES, accountCli } from "./account.js";
40
+ import { CHAT_LINE, chatCli } from "./chat.js";
36
41
  import { httpAgent } from "./http-agent.js";
37
- import { newStandalone, packageRoot } from "./new.js";
42
+ import { initHere, newStandalone, packageRoot } from "./new.js";
38
43
  import { EXIT, finishedOutcome, loadEnv, networkCode, reportError, runVerdict, stoppedOutcome } from "./report.js";
39
44
  import { LAST_SESSION, lastSessionIn, SHOW_LINE, showCli } from "./show.js";
40
45
  import { loadSuite, parseScenario, parseToolFile, suiteHash } from "./suite.js";
41
- const NEW_LINE = " flow new <name> Make ./<name>/, a new agent started from the café example";
46
+ const NEW_LINE = ` flow new <name> Make ./<name>/, a new agent started from the café example
47
+ flow init Make that agent in the current folder, named after it`;
42
48
  const TEST_LINE = ` flow test [options] Run the agent's tests in a test cloud, from the agent's folder
43
49
  (src/agent.ts exports buildAgent({ model, events, tools }) and tools;
44
50
  the tests are in tests/scenarios and tests/tools)`;
@@ -91,14 +97,19 @@ with the judge's reasons. Never the messages. The folder is git-ignored and the
91
97
  Environment (also read from the agent folder's .env, or --dotenv; variables already set win;
92
98
  the CHINWAG_ names from before the rename still work):
93
99
  FLOW_FRAMEWORK_CLOUD The test cloud, when --cloud is not given
94
- FLOW_FRAMEWORK_CLOUD_KEY The agent's CI key, for a hosted cloud (oci_...: make one at
95
- https://flow.engineer/observe, your agent, Developers)
100
+ FLOW_FRAMEWORK_CLOUD_KEY The agent's CI key, for a hosted cloud (oci_...: flow setup
101
+ writes it, or make one at https://flow.engineer/observe, your agent, Developers)
96
102
  FLOW_FRAMEWORK_AGENT_TOKEN
97
103
  With --agent-url: sent to it as authorization: Bearer <token>
98
104
  OPENROUTER_API_KEY The agent's model key, or OPENAI_API_KEY, ANTHROPIC_API_KEY or
99
105
  GEMINI_API_KEY: the first with a value is used`;
100
106
  const TEST_USAGE = `Usage:\n${TEST_LINE}\n\n${TEST_OPTIONS}`;
101
- const USAGE = `Usage:\n${NEW_LINE}\n${TEST_LINE}\n${SHOW_LINE}\n${CHECK_LINE}\n flow --version Show the version\n\n${TEST_OPTIONS}`;
107
+ const USAGE = `Usage:\n${NEW_LINE}\n${CHAT_LINE}\n${TEST_LINE}\n${SHOW_LINE}\n${CHECK_LINE}\n${ACCOUNT_LINES}\n flow --version Show the version\n\n${TEST_OPTIONS}`;
108
+ const NEXT_STEPS = ` 2. Add one model key: cp .env.example .env; the file says where to make one
109
+ 3. npx @flow-engineer/flow chat, and type to it (or npm start for the playground)
110
+ 4. npx @flow-engineer/flow link: signs in, writes the other keys, prints the link for your phone;
111
+ keep npm start running and text it
112
+ 5. Tell your coding agent what to build: its skills are in .claude/skills/`;
102
113
  checkNode(process.versions.node);
103
114
  if (process.argv[2] === "check-endpoint") {
104
115
  // Its token may be in the agent folder's .env; no value is ever printed.
@@ -107,6 +118,10 @@ if (process.argv[2] === "check-endpoint") {
107
118
  }
108
119
  if (process.argv[2] === "show")
109
120
  process.exit(await showCli(process.argv.slice(3)));
121
+ if (process.argv[2] === "chat")
122
+ process.exit(await chatCli(process.argv.slice(3)));
123
+ if (ACCOUNT_COMMANDS.has(process.argv[2] ?? ""))
124
+ process.exit(await accountCli(process.argv[2], process.argv.slice(3)));
110
125
  const TEST_FLAGS = {
111
126
  cloud: { type: "string" },
112
127
  runs: { type: "string", default: "1" },
@@ -134,7 +149,7 @@ if (values.version) {
134
149
  process.exit(0);
135
150
  }
136
151
  if (values.help || process.argv.length <= 2) {
137
- console.log(command === "new" ? NEW_USAGE : command === "test" ? TEST_USAGE : USAGE);
152
+ console.log(command === "new" || command === "init" ? NEW_USAGE : command === "test" ? TEST_USAGE : USAGE);
138
153
  process.exit(0);
139
154
  }
140
155
  if (command === "new") {
@@ -148,9 +163,23 @@ if (command === "new") {
148
163
  }
149
164
  console.log(`Made ./${name}/ from the café example (Pip). Next:
150
165
  1. cd ${name} && npm install
151
- 2. Add one model key: cp .env.example .env; the file says where to make one
152
- 3. npm start, then open the link it prints
153
- 4. Tell your coding agent what to build: its skills are in .claude/skills/`);
166
+ ${NEXT_STEPS}`);
167
+ process.exit(0);
168
+ }
169
+ if (command === "init") {
170
+ let name;
171
+ try {
172
+ if (rest.length)
173
+ throw new Error("Usage: flow init (it makes the agent in the current folder; flow new <name> makes a folder for it)");
174
+ name = initHere();
175
+ }
176
+ catch (error) {
177
+ console.error(error.message);
178
+ process.exit(1);
179
+ }
180
+ console.log(`Made ${name}, started from the café example (Pip), in this folder. Next:
181
+ 1. npm install
182
+ ${NEXT_STEPS}`);
154
183
  process.exit(0);
155
184
  }
156
185
  /** Every human line of `flow test`: stdout, or stderr with --json, where stdout is the report. */
@@ -183,7 +212,7 @@ try {
183
212
  });
184
213
  }
185
214
  catch (error) {
186
- const where = !ciKey && cloud.replace(/\/+$/, "") === OBSERVE_URL ? ` Make a key at ${OBSERVE_URL}: your agent, Developers.` : "";
215
+ const where = !ciKey && cloud.replace(/\/+$/, "") === OBSERVE_URL ? " Run npx @flow-engineer/flow login, then npx @flow-engineer/flow setup, to get one into .env." : "";
187
216
  invalid(`${error.message}${where}`);
188
217
  }
189
218
  const dir = process.cwd();
@@ -499,7 +528,7 @@ function stoppedReason(error, cloud) {
499
528
  }
500
529
  /** The command's flags, or exits 1 with what is wrong and that command's usage. */
501
530
  function parseCommandLine() {
502
- const isNew = process.argv[2] === "new";
531
+ const isNew = process.argv[2] === "new" || process.argv[2] === "init";
503
532
  const options = isNew ? { help: TEST_FLAGS.help, version: TEST_FLAGS.version } : TEST_FLAGS;
504
533
  try {
505
534
  return parseArgs({ allowPositionals: true, strict: true, options: options });
@@ -34,6 +34,8 @@ export interface ConnectOptions {
34
34
  retryBudgetMs?: number;
35
35
  /** How long a lost stream keeps trying to resume, on a cloud that resumes (Opened.resume). Default 2 minutes, as long as the cloud holds its commands. */
36
36
  resumeWindowMs?: number;
37
+ /** The clock the resume window is measured on. Default Date.now; a test passes one it moves itself. */
38
+ now?: () => number;
37
39
  /** How often a resumable stream is replaced, before the tunnel's one-hour request cap. Default 50 minutes. */
38
40
  streamRefreshMs?: number;
39
41
  /** The longest Retry-After a busy cloud's refused open is sent again after, once. Default 60s. */
@@ -204,6 +204,7 @@ async function playSession(options, only) {
204
204
  const retryBudgetMs = options.retryBudgetMs ?? RETRY_BUDGET_MS;
205
205
  const openRetryCapMs = options.openRetryCapMs ?? OPEN_RETRY_CAP_MS;
206
206
  const resumeWindowMs = options.resumeWindowMs ?? RESUME_WINDOW_MS;
207
+ const now = options.now ?? Date.now;
207
208
  const streamRefreshMs = options.streamRefreshMs ?? STREAM_REFRESH_MS;
208
209
  let idempotent = false;
209
210
  const base = options.cloud.replace(/\/$/, "");
@@ -536,13 +537,13 @@ async function playSession(options, only) {
536
537
  /** The stream opened again after the last command handled, with backoff, until the resume window closes. */
537
538
  const reconnect = async (why) => {
538
539
  log(`stream ${why}; resuming after command ${lastId}`);
539
- const deadline = Date.now() + resumeWindowMs;
540
+ const deadline = now() + resumeWindowMs;
540
541
  let last;
541
542
  for (let attempt = 0;; attempt++) {
542
543
  const backoff = attempt === 0 ? 0 : Math.min(10_000, 250 * 2 ** attempt) * (0.8 + Math.random() * 0.4);
543
544
  const refused = last instanceof Error && last.cause instanceof PostError ? last.cause : undefined;
544
545
  const wait = Math.max(backoff, refused?.retryAfterMs ?? 0);
545
- if (Date.now() + wait >= deadline) {
546
+ if (now() + wait >= deadline) {
546
547
  const detail = last instanceof Error ? `; last try: ${last.message}` : "";
547
548
  throw new StreamLost(`the stream did not resume within ${resumeWindowMs / 1000}s (${why})${detail}`);
548
549
  }
@@ -550,7 +551,7 @@ async function playSession(options, only) {
550
551
  if (failure)
551
552
  throw failure;
552
553
  try {
553
- return await openStream(streamUrl, { ...auth, "last-event-id": String(lastId) }, controller.signal, Math.max(1, Math.min(postTimeoutMs, deadline - Date.now())));
554
+ return await openStream(streamUrl, { ...auth, "last-event-id": String(lastId) }, controller.signal, Math.max(1, Math.min(postTimeoutMs, deadline - now())));
554
555
  }
555
556
  catch (error) {
556
557
  if (failure)
@@ -0,0 +1,143 @@
1
+ export declare const LOGIN_URL = "https://flow.engineer";
2
+ /** The longest one request to flow.engineer may take; login's polls are cut shorter by what is left of --timeout. */
3
+ export declare const REQUEST_TIMEOUT_MS = 30000;
4
+ /** A login URL a token may go to: https, or http to this machine only (a local flow.engineer). */
5
+ export declare function safeLoginUrl(url: string): boolean;
6
+ /** flow.engineer, or FLOW_FRAMEWORK_LOGIN_URL, without trailing slashes (trimmed from the end, in linear time). */
7
+ export declare function loginUrl(env?: NodeJS.ProcessEnv): string;
8
+ /** ~/.config/flow-framework/credentials.json (XDG_CONFIG_HOME when set), written at mode 0600. */
9
+ export declare function credentialsPath(env?: NodeJS.ProcessEnv): string;
10
+ export interface Credentials {
11
+ token: string;
12
+ expires_at?: string;
13
+ /** The server that issued it, so logout revokes it there. */
14
+ url: string;
15
+ }
16
+ /**
17
+ * The token to use: FLOW_FRAMEWORK_TOKEN, else the credentials file's (with `url`, the server that
18
+ * issued it, the only one it is sent to); undefined when signed out.
19
+ */
20
+ export declare function currentToken(env?: NodeJS.ProcessEnv): {
21
+ token: string;
22
+ from: "env" | "file";
23
+ url?: string;
24
+ } | undefined;
25
+ export declare function readCredentials(env?: NodeJS.ProcessEnv): Credentials | undefined;
26
+ export declare function saveCredentials(credentials: Credentials, env?: NodeJS.ProcessEnv): string;
27
+ export declare function deleteCredentials(env?: NodeJS.ProcessEnv): boolean;
28
+ /** A device code started by `flow login` and not approved yet, kept for `flow login --wait`. */
29
+ export interface PendingLogin extends DeviceStart {
30
+ /** When the code expires, in ms since the epoch. */
31
+ expires_at: number;
32
+ /** The server that issued it. */
33
+ url: string;
34
+ }
35
+ /** ~/.config/flow-framework/pending.json, written at mode 0600 and deleted once the code is used or expired. */
36
+ export declare function pendingPath(env?: NodeJS.ProcessEnv): string;
37
+ export declare function readPending(env?: NodeJS.ProcessEnv): PendingLogin | undefined;
38
+ export declare function savePending(pending: PendingLogin, env?: NodeJS.ProcessEnv): void;
39
+ export declare function deletePending(env?: NodeJS.ProcessEnv): void;
40
+ export interface DeviceStart {
41
+ device_code: string;
42
+ user_code: string;
43
+ verification_uri: string;
44
+ verification_uri_complete?: string;
45
+ interval: number;
46
+ expires_in: number;
47
+ }
48
+ export type TokenAnswer = {
49
+ status: "signed_in";
50
+ token: string;
51
+ expires_at?: string;
52
+ account?: Account;
53
+ } | {
54
+ status: "authorization_pending" | "slow_down" | "expired_token" | "access_denied";
55
+ };
56
+ export interface Account {
57
+ email: string;
58
+ account: string;
59
+ }
60
+ export interface SetupAnswer {
61
+ agent_id: string;
62
+ /** Only the keys minted in this call come back (those the request did not say it has). */
63
+ cloud_key?: string;
64
+ traces_key?: string;
65
+ messaging_key?: string;
66
+ /** Each channel's shared sender and phone link, only while Flow has that channel open. */
67
+ telegram_sender?: string;
68
+ whatsapp_sender?: string;
69
+ imessage_sender?: string;
70
+ whatsapp_link?: string;
71
+ imessage_link?: string;
72
+ /** Missing only when the server could not make one; the keys it minted are written anyway. */
73
+ join_code?: string;
74
+ account?: Account;
75
+ app_id?: string;
76
+ dashboard_url?: string;
77
+ /** The flow.engineer page that opens Telegram with the join message filled in. */
78
+ try_url?: string;
79
+ telegram_link?: string;
80
+ /** The try page per channel with a sender: {telegram: ".../try/<code>?c=telegram", ...}. */
81
+ try_urls?: Record<string, string>;
82
+ /** The account's free-trial counts per channel; limit null is unlimited. iMessage also counts the people it may reach. */
83
+ trial?: Record<string, {
84
+ limit: number | null;
85
+ used: number;
86
+ contacts?: {
87
+ limit: number;
88
+ used: number;
89
+ };
90
+ }>;
91
+ /** The answer fields of the keys made in this call. */
92
+ minted?: string[];
93
+ next?: string[];
94
+ /** Up to 20 lines for the owner, shown as they are. */
95
+ warnings?: string[];
96
+ }
97
+ /** The answer's warnings: its strings only, at most 20. */
98
+ export declare function warningsOf(body: Record<string, unknown>): string[];
99
+ /** Retry-After as seconds from now (delta-seconds or an HTTP date); undefined when absent or unreadable. */
100
+ export declare function retryAfterSeconds(header: string | null, now?: number): number | undefined;
101
+ /**
102
+ * The server answered with an error status; `code` is its `error` field when it gave one, `next` its
103
+ * next step, and `fields` the whole answer (a failed setup carries the keys it minted anyway).
104
+ */
105
+ export declare class LoginRefused extends Error {
106
+ readonly status: number;
107
+ readonly code: string;
108
+ readonly next: string | undefined;
109
+ readonly fields: Record<string, unknown>;
110
+ /** Seconds the server asked to wait (Retry-After), for a 429. */
111
+ readonly retryAfter: number | undefined;
112
+ constructor(status: number, code: string, message: string, next?: string, fields?: Record<string, unknown>, retryAfter?: number);
113
+ }
114
+ /** The server answered nothing in time: `ms` is how long this request was given. */
115
+ export declare class TimedOut extends Error {
116
+ readonly ms: number;
117
+ constructor(url: string, ms: number);
118
+ }
119
+ /** `clientName` and `hostname` are shown on the approval page. */
120
+ export declare function startDevice(url: string, clientName: string, hostname: string): Promise<DeviceStart>;
121
+ /** `timeoutMs` cuts this one poll short (login gives it what is left of --timeout). */
122
+ export declare function pollToken(url: string, deviceCode: string, timeoutMs?: number): Promise<TokenAnswer>;
123
+ export declare function revoke(url: string, token: string): Promise<void>;
124
+ export declare function whoami(url: string, token: string): Promise<Account>;
125
+ export declare function setup(url: string, token: string, request: {
126
+ name: string;
127
+ hostname: string;
128
+ channels: string[];
129
+ have: string[];
130
+ }): Promise<SetupAnswer>;
131
+ /** The agent's .env names for what setup hands back; only the keys' and the join's names are sent as `have`. */
132
+ export declare const SETUP_ENV: {
133
+ readonly cloud_key: "FLOW_FRAMEWORK_CLOUD_KEY";
134
+ readonly traces_key: "FLOW_FRAMEWORK_TRACES_KEY";
135
+ readonly messaging_key: "FLOW_MESSAGING_KEY";
136
+ readonly telegram_sender: "FLOW_MESSAGING_TELEGRAM_SENDER";
137
+ readonly whatsapp_sender: "FLOW_MESSAGING_WHATSAPP_SENDER";
138
+ readonly imessage_sender: "FLOW_MESSAGING_IMESSAGE_SENDER";
139
+ readonly join_code: "FLOW_FRAMEWORK_JOIN_CODE";
140
+ readonly try_url: "FLOW_FRAMEWORK_TRY_URL";
141
+ };
142
+ /** The phone link for a join code, built as the server builds its try_url, for a .env from before setup kept it. */
143
+ export declare function tryLink(url: string, joinCode: string, channel: string): string;