@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.
- package/LICENSE +202 -21
- package/README.md +28 -12
- package/coding-agents/skill.md +32 -0
- package/coding-agents/test-skill.md +10 -10
- package/dist/flow-messaging/src/client.d.ts +7 -1
- package/dist/flow-messaging/src/client.js +13 -1
- package/dist/flow-messaging/src/index.d.ts +38 -3
- package/dist/flow-messaging/src/index.js +235 -39
- package/dist/src/names.d.ts +5 -0
- package/dist/src/names.js +5 -0
- package/dist/testing/src/account.d.ts +4 -0
- package/dist/testing/src/account.js +580 -0
- package/dist/testing/src/chat.d.ts +3 -0
- package/dist/testing/src/chat.js +117 -0
- package/dist/testing/src/chinwag.js +1 -1
- package/dist/testing/src/cli.js +42 -13
- package/dist/testing/src/client.d.ts +2 -0
- package/dist/testing/src/client.js +4 -3
- package/dist/testing/src/flow-login.d.ts +143 -0
- package/dist/testing/src/flow-login.js +249 -0
- package/dist/testing/src/new.d.ts +4 -0
- package/dist/testing/src/new.js +38 -13
- package/docs/agents.md +85 -0
- package/examples/cafe/src/agent.ts +6 -2
- package/examples/cafe/src/channels.ts +20 -0
- package/examples/cafe/src/main.ts +17 -3
- package/flow-messaging/README.md +66 -27
- package/package.json +3 -2
- package/spec/agent-endpoint.openapi.yaml +2 -2
- package/spec/nudges.openapi.yaml +2 -2
|
@@ -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 {};
|
package/dist/testing/src/cli.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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_...:
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 ?
|
|
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 =
|
|
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 (
|
|
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 -
|
|
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;
|