@flow-engineer/flow 0.3.0 → 0.3.2
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/coding-agents/skill.md +6 -2
- package/dist/testing/src/account.d.ts +1 -1
- package/dist/testing/src/account.js +90 -23
- package/dist/testing/src/flow-login.d.ts +4 -0
- package/dist/testing/src/flow-login.js +5 -2
- package/docs/agents.md +13 -2
- package/examples/cafe/src/agent.ts +3 -3
- package/examples/cafe/src/channels.ts +1 -1
- package/examples/cafe/src/main.ts +1 -1
- package/examples/cafe/src/mcp.ts +1 -1
- package/examples/cafe/src/tools.ts +1 -1
- package/package.json +1 -1
package/coding-agents/skill.md
CHANGED
|
@@ -20,7 +20,7 @@ and `flow-framework-auth` (`auth-skill.md`) when tools need to know who the pers
|
|
|
20
20
|
`npx @flow-engineer/flow new <name>` (lowercase letters, digits and dashes) makes `./<name>/` from the café
|
|
21
21
|
example: a complete agent with tools and tests that depends on the `@flow-engineer/flow` package and imports
|
|
22
22
|
`@flow-engineer/flow` and `@flow-engineer/flow/simulators`, with these skills in `.claude/skills/`. Run `npm install` there.
|
|
23
|
-
Build in it, changing
|
|
23
|
+
Build in it, changing the café agent's prompt, skills, tools and tests into yours. A provider key is optional:
|
|
24
24
|
`npx @flow-engineer/flow link` (or `setup`) writes `FLOW_KEY`, the agent's one Flow key, into `.env` (never
|
|
25
25
|
read or print it), and the agent calls models through Flow with it on $2 of Flow trial credit. A key of the owner's own wins over it, made
|
|
26
26
|
at the provider: `OPENROUTER_API_KEY` at https://openrouter.ai/keys,
|
|
@@ -349,7 +349,11 @@ kept: give the owner the link and code, and run `link` again (it polls the same
|
|
|
349
349
|
`setup`, which writes the test cloud, traces and Flow Messaging keys into `.env`, prints names and
|
|
350
350
|
last 4 characters only, keeps values already there (`--replace` takes the new ones), and is safe to
|
|
351
351
|
run again. `--channel telegram|whatsapp|imessage` picks the channel; a channel flow.engineer cannot
|
|
352
|
-
open yet exits 3 with its `next`.
|
|
352
|
+
open yet exits 3 with its `next`. When only observe's part of setup fails (traces and the test cloud),
|
|
353
|
+
`link` still prints the link and exits 0, with one warning saying what is missing and the server's
|
|
354
|
+
fix (`warnings` in `--json`): pass it on to the owner. `login`, `setup` and `link` also run on their own.
|
|
355
|
+
When the owner names a team's observe workspace, pass `--workspace <name|acc_id>` to `setup` or `link`
|
|
356
|
+
once: its `acc_` id is kept in `.env` (`FLOW_FRAMEWORK_WORKSPACE`) for later runs, and `status` shows it.
|
|
353
357
|
|
|
354
358
|
In `src/main.ts`, put `fm.telegram()` from `@flow-engineer/flow/flow-messaging` in the agent's
|
|
355
359
|
channels and call `fm.start(agent, { cursorFile: ".flow/flow-messaging-cursor" })`: with no webhook
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare const ACCOUNT_COMMANDS: Set<string>;
|
|
2
|
-
export declare const ACCOUNT_LINES = " flow login [--wait | --no-wait] [--timeout <s>] [--force] [--no-browser] [--json]\n Sign in to flow.engineer: opens the browser, prints the link and code,\n and waits up to --timeout (default 120) for approval; --wait goes on\n waiting for the same code, --no-wait returns at once\n flow logout [--json] End this machine's sign-in and delete its token\n flow whoami [--json] Show the account you are signed in as\n flow setup [--replace] [--rotate <keys> [--now]] [--display-name <name>] [--ci] [--json]\n Write this agent's keys (its Flow key FLOW_KEY, which also pays for the\n model on the account's trial credit when there is no provider key, test\n cloud, traces, Flow Messaging) and each open channel's sender into\n ./.env (in an agent's folder: package.json); --rotate key,cloud,traces,\n messaging or all makes those keys again (the old FLOW_KEY works 24 h\n more; --now ends it at once)\n flow link [--channel telegram|whatsapp|imessage] [--display-name <name>] [--timeout <s>] [--no-browser] [--json]\n Print the link that opens a chat with this agent from a phone; signs in\n and runs setup first when this folder is not set up yet. --display-name\n (here and on setup) renames the agent: package.json's displayName and\n its prompt, agent/AGENTS.md; the phone link then says \"Hi <name>\"\n flow status [--json] What this folder has and lacks: FLOW_KEY and the model key (never their\n values) with the trial credit left, the\n sign-in, each channel's sender in .env, and, signed in, which channels\n are open now and their free-trial counts";
|
|
2
|
+
export declare const ACCOUNT_LINES = " flow login [--wait | --no-wait] [--timeout <s>] [--force] [--no-browser] [--json]\n Sign in to flow.engineer: opens the browser, prints the link and code,\n and waits up to --timeout (default 120) for approval; --wait goes on\n waiting for the same code, --no-wait returns at once\n flow logout [--json] End this machine's sign-in and delete its token\n flow whoami [--json] Show the account you are signed in as\n flow setup [--replace] [--rotate <keys> [--now]] [--workspace <name|acc_id>] [--display-name <name>] [--ci] [--json]\n Write this agent's keys (its Flow key FLOW_KEY, which also pays for the\n model on the account's trial credit when there is no provider key, test\n cloud, traces, Flow Messaging) and each open channel's sender into\n ./.env (in an agent's folder: package.json); --rotate key,cloud,traces,\n messaging or all makes those keys again (the old FLOW_KEY works 24 h\n more; --now ends it at once). --workspace (here and on link) puts the\n agent in that observe workspace of yours, kept in .env as\n FLOW_FRAMEWORK_WORKSPACE; a new one makes the cloud and traces keys again\n flow link [--channel telegram|whatsapp|imessage] [--workspace <name|acc_id>] [--display-name <name>] [--timeout <s>] [--no-browser] [--json]\n Print the link that opens a chat with this agent from a phone; signs in\n and runs setup first when this folder is not set up yet. --display-name\n (here and on setup) renames the agent: package.json's displayName and\n its prompt, agent/AGENTS.md; the phone link then says \"Hi <name>\"\n flow status [--json] What this folder has and lacks: FLOW_KEY and the model key (never their\n values) with the trial credit left, the workspace, the\n sign-in, each channel's sender in .env, and, signed in, which channels\n are open now and their free-trial counts";
|
|
3
3
|
/** Runs one account command; resolves to the exit status. */
|
|
4
4
|
export declare function accountCli(command: string, args: string[]): Promise<number>;
|
|
@@ -30,20 +30,22 @@ export const ACCOUNT_LINES = ` flow login [--wait | --no-wait] [--timeout <s>]
|
|
|
30
30
|
waiting for the same code, --no-wait returns at once
|
|
31
31
|
flow logout [--json] End this machine's sign-in and delete its token
|
|
32
32
|
flow whoami [--json] Show the account you are signed in as
|
|
33
|
-
flow setup [--replace] [--rotate <keys> [--now]] [--display-name <name>] [--ci] [--json]
|
|
33
|
+
flow setup [--replace] [--rotate <keys> [--now]] [--workspace <name|acc_id>] [--display-name <name>] [--ci] [--json]
|
|
34
34
|
Write this agent's keys (its Flow key FLOW_KEY, which also pays for the
|
|
35
35
|
model on the account's trial credit when there is no provider key, test
|
|
36
36
|
cloud, traces, Flow Messaging) and each open channel's sender into
|
|
37
37
|
./.env (in an agent's folder: package.json); --rotate key,cloud,traces,
|
|
38
38
|
messaging or all makes those keys again (the old FLOW_KEY works 24 h
|
|
39
|
-
more; --now ends it at once)
|
|
40
|
-
|
|
39
|
+
more; --now ends it at once). --workspace (here and on link) puts the
|
|
40
|
+
agent in that observe workspace of yours, kept in .env as
|
|
41
|
+
FLOW_FRAMEWORK_WORKSPACE; a new one makes the cloud and traces keys again
|
|
42
|
+
flow link [--channel telegram|whatsapp|imessage] [--workspace <name|acc_id>] [--display-name <name>] [--timeout <s>] [--no-browser] [--json]
|
|
41
43
|
Print the link that opens a chat with this agent from a phone; signs in
|
|
42
44
|
and runs setup first when this folder is not set up yet. --display-name
|
|
43
45
|
(here and on setup) renames the agent: package.json's displayName and
|
|
44
46
|
its prompt, agent/AGENTS.md; the phone link then says "Hi <name>"
|
|
45
47
|
flow status [--json] What this folder has and lacks: FLOW_KEY and the model key (never their
|
|
46
|
-
values) with the trial credit left, the
|
|
48
|
+
values) with the trial credit left, the workspace, the
|
|
47
49
|
sign-in, each channel's sender in .env, and, signed in, which channels
|
|
48
50
|
are open now and their free-trial counts`;
|
|
49
51
|
const USAGE = `Usage:\n${ACCOUNT_LINES}
|
|
@@ -100,7 +102,7 @@ export async function accountCli(command, args) {
|
|
|
100
102
|
: {}),
|
|
101
103
|
...(command === "setup" ? { replace: { type: "boolean", default: false }, rotate: { type: "string" }, now: { type: "boolean", default: false }, ci: { type: "boolean", default: false } } : {}),
|
|
102
104
|
...(command === "link" ? { channel: { type: "string", default: "telegram" } } : {}),
|
|
103
|
-
...(command === "link" || command === "setup" ? { "display-name": { type: "string" } } : {}),
|
|
105
|
+
...(command === "link" || command === "setup" ? { "display-name": { type: "string" }, workspace: { type: "string" } } : {}),
|
|
104
106
|
},
|
|
105
107
|
}));
|
|
106
108
|
}
|
|
@@ -131,6 +133,12 @@ export async function accountCli(command, args) {
|
|
|
131
133
|
console.error(`flow setup: --now goes with --rotate key (or all): it ends the old FLOW_KEY at once.\n\n${USAGE}`);
|
|
132
134
|
return EXIT.usage;
|
|
133
135
|
}
|
|
136
|
+
const workspace = typeof values.workspace === "string" ? values.workspace.trim() : undefined;
|
|
137
|
+
// flow.engineer takes a workspace's acc_ id or its name: 1 to 100 characters, no control characters.
|
|
138
|
+
if (workspace !== undefined && (!workspace || workspace.length > 100 || /[\u0000-\u001f\u007f]/.test(workspace))) {
|
|
139
|
+
console.error(`flow ${command}: --workspace takes a workspace's name or its acc_ id, 1 to 100 characters.\n\n${USAGE}`);
|
|
140
|
+
return EXIT.usage;
|
|
141
|
+
}
|
|
134
142
|
const json = values.json === true;
|
|
135
143
|
// Straight to the file descriptors, never buffered: a coding agent's cut-off command keeps every line.
|
|
136
144
|
const out = (report) => writeAll(1, `${JSON.stringify(report)}\n`);
|
|
@@ -162,9 +170,9 @@ export async function accountCli(command, args) {
|
|
|
162
170
|
else if (command === "status")
|
|
163
171
|
await status(json, out, say);
|
|
164
172
|
else if (command === "setup")
|
|
165
|
-
await setupEnv({ replace: values.replace === true, rotate, now: values.now === true, ci: values.ci === true }, json, out, say);
|
|
173
|
+
await setupEnv({ replace: values.replace === true, rotate, now: values.now === true, ci: values.ci === true, workspace }, json, out, say);
|
|
166
174
|
else
|
|
167
|
-
await link({ channel, noBrowser: values["no-browser"] === true, timeoutSeconds, renamed }, json, out, say);
|
|
175
|
+
await link({ channel, noBrowser: values["no-browser"] === true, timeoutSeconds, renamed, workspace }, json, out, say);
|
|
168
176
|
return EXIT.ok;
|
|
169
177
|
}
|
|
170
178
|
catch (error) {
|
|
@@ -499,6 +507,19 @@ const OPEN_HINT = {
|
|
|
499
507
|
whatsapp: "Open it and send the message it fills in.",
|
|
500
508
|
imessage: "Open it on an iPhone or a Mac and send the message it fills in.",
|
|
501
509
|
};
|
|
510
|
+
/** The observe workspace this folder's agent was put in by --workspace (its acc_ id); unset: the personal one. */
|
|
511
|
+
const WORKSPACE_ENV = "FLOW_FRAMEWORK_WORKSPACE";
|
|
512
|
+
/** The answer's workspace_id, under the name setupEnv gives it only when a workspace was asked for. */
|
|
513
|
+
const WORKSPACE_FIELD = "workspace_kept";
|
|
514
|
+
/** --workspace names another workspace than the one .env's cloud and traces keys are in (none named yet: the personal one). */
|
|
515
|
+
function movesWorkspace(workspace, value) {
|
|
516
|
+
if (workspace === undefined)
|
|
517
|
+
return false;
|
|
518
|
+
const stored = value(WORKSPACE_ENV);
|
|
519
|
+
if (stored)
|
|
520
|
+
return workspace.toLowerCase() !== stored.toLowerCase();
|
|
521
|
+
return Boolean(value(SETUP_ENV.cloud_key) || value(SETUP_ENV.traces_key));
|
|
522
|
+
}
|
|
502
523
|
async function setupEnv(options, json, out, say, forLink) {
|
|
503
524
|
if (options.ci) {
|
|
504
525
|
const steps = [
|
|
@@ -511,7 +532,7 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
511
532
|
out({ ci: { secret: SETUP_ENV.cloud_key, steps } });
|
|
512
533
|
for (const line of steps)
|
|
513
534
|
say(line);
|
|
514
|
-
return { answer: {} };
|
|
535
|
+
return { answer: {}, warnings: [] };
|
|
515
536
|
}
|
|
516
537
|
const dir = process.cwd();
|
|
517
538
|
agentFolder(dir);
|
|
@@ -527,6 +548,14 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
527
548
|
const name = named.replace(/^@[^/]*\//, "").replace(/[^A-Za-z0-9 ._-]/g, "-").slice(0, 64) || "my-agent";
|
|
528
549
|
const all = options.rotate.includes("all");
|
|
529
550
|
const rotated = new Set(all ? Object.values(ROTATE).filter(Boolean) : options.rotate.map((k) => ROTATE[k]));
|
|
551
|
+
// The workspace asked for: --workspace, else the one an earlier --workspace kept in .env.
|
|
552
|
+
const workspace = options.workspace ?? (before.get(WORKSPACE_ENV) || undefined);
|
|
553
|
+
// Another workspace: the cloud and traces keys in .env are the old one's, so they are made again
|
|
554
|
+
// there (Messaging's key and FLOW_KEY are not bound to a workspace).
|
|
555
|
+
const moving = movesWorkspace(options.workspace, (n) => before.get(n));
|
|
556
|
+
if (moving)
|
|
557
|
+
for (const field of ["cloud_key", "traces_key"])
|
|
558
|
+
rotated.add(field);
|
|
530
559
|
const rotatedEnv = new Set([...rotated].map((field) => (field === "flow_key" ? FLOW_KEY : SETUP_ENV[field])));
|
|
531
560
|
// An empty value (`cp .env.example .env`) is missing: the server mints it. A model key counts in
|
|
532
561
|
// the shell too (npm start reads both): FLOW_KEY is made only when it is not named as had, and
|
|
@@ -542,38 +571,64 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
542
571
|
// FLOW_FRAMEWORK_TRY_URL is one channel's page: link's channel when it is open, else the server's (Telegram's).
|
|
543
572
|
const withTryUrl = (fields) => {
|
|
544
573
|
const code = fields.join_code;
|
|
574
|
+
// The workspace the answer put the agent in, kept only when one was asked for.
|
|
575
|
+
if (workspace !== undefined && typeof fields.workspace_id === "string" && fields.workspace_id)
|
|
576
|
+
fields = { ...fields, [WORKSPACE_FIELD]: fields.workspace_id };
|
|
545
577
|
if (!forLink || typeof fields[`${forLink.channel}_sender`] !== "string" || typeof code !== "string" || !code)
|
|
546
578
|
return fields;
|
|
547
579
|
const pages = fields.try_urls;
|
|
548
580
|
const page = typeof pages?.[forLink.channel] === "string" ? pages[forLink.channel] : tryLink(url, code, forLink.channel);
|
|
549
581
|
return { ...fields, try_url: page };
|
|
550
582
|
};
|
|
551
|
-
const chosenPage = (n) => forLink !== undefined && n === SETUP_ENV.try_url;
|
|
583
|
+
const chosenPage = (n) => (forLink !== undefined && n === SETUP_ENV.try_url) || n === WORKSPACE_ENV;
|
|
552
584
|
let answer;
|
|
585
|
+
// For link: the server's refusal when Messaging's part came (a join code) and only observe's failed.
|
|
586
|
+
let observeFailed;
|
|
553
587
|
try {
|
|
554
588
|
// Every channel: each comes back with its shared sender only while it is open.
|
|
555
|
-
answer = await setup(url, token, { name, hostname: hostname(), channels: [...CHANNELS], have, ...(rotate ? { rotate } : {}), ...(rotateNow ? { rotate_now: true } : {}) });
|
|
589
|
+
answer = await setup(url, token, { name, hostname: hostname(), channels: [...CHANNELS], have, ...(workspace !== undefined ? { workspace } : {}), ...(rotate ? { rotate } : {}), ...(rotateNow ? { rotate_now: true } : {}) });
|
|
556
590
|
}
|
|
557
591
|
catch (error) {
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
592
|
+
// Texting needs only Messaging's part, so link goes on without observe's (traces and the test
|
|
593
|
+
// cloud) and says what is missing; setup alone, whose job is both, still stops.
|
|
594
|
+
if (forLink && error instanceof LoginRefused && typeof error.fields.join_code === "string" && error.fields.join_code) {
|
|
595
|
+
const { error: _code, error_description: why, next: fix, ...rest } = error.fields;
|
|
596
|
+
answer = rest;
|
|
597
|
+
observeFailed = { why: typeof why === "string" && why ? why : error.message, fix: typeof fix === "string" && fix ? fix : error.next };
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
// The keys minted anyway are not shown again, so whatever the error carries goes into .env first.
|
|
601
|
+
if (error instanceof LoginRefused) {
|
|
602
|
+
const written = writeEnv(file, lines, before, envEntries(withTryUrl(error.fields)), (n) => options.replace || rotatedEnv.has(n) || chosenPage(n));
|
|
603
|
+
if (written.some((w) => w.action === "added" || w.action === "replaced"))
|
|
604
|
+
say(`Kept in .env before the error: ${written.map((w) => `${w.name} ${w.action}`).join(", ")}.`);
|
|
605
|
+
}
|
|
606
|
+
throw error;
|
|
563
607
|
}
|
|
564
|
-
throw error;
|
|
565
608
|
}
|
|
566
609
|
const fields = answer;
|
|
567
610
|
const warnings = warningsOf(fields);
|
|
568
611
|
const had = new Map(before);
|
|
569
612
|
// --replace takes every new value but a model key: only --rotate key replaces FLOW_KEY.
|
|
570
613
|
const written = writeEnv(file, lines, before, envEntries(withTryUrl(fields)), (n) => rotatedEnv.has(n) || (options.replace && !MODEL_KEY_NAMES.includes(n)) || chosenPage(n));
|
|
614
|
+
// An older flow.engineer ignores the field and sets the agent up in the personal workspace.
|
|
615
|
+
if (workspace !== undefined && !observeFailed && (typeof answer.workspace_id !== "string" || !answer.workspace_id)) {
|
|
616
|
+
const stop = new Stop("workspace_unsupported", "flow.engineer does not support --workspace yet; the agent was set up in your personal workspace.", undefined, EXIT.refused);
|
|
617
|
+
stop.warnings = warnings;
|
|
618
|
+
throw stop;
|
|
619
|
+
}
|
|
571
620
|
if (typeof answer.join_code !== "string" || !answer.join_code) {
|
|
572
621
|
const kept = written.filter((w) => w.action === "added" || w.action === "replaced").map((w) => w.name);
|
|
573
622
|
const stop = new Stop("no_join_code", `flow.engineer gave no join code${kept.length ? ` (the keys it made are in .env: ${kept.join(", ")})` : ""}, so there is no phone link yet.`, answer.next?.[0] ?? NEXT.setup, EXIT.refused);
|
|
574
623
|
stop.warnings = warnings;
|
|
575
624
|
throw stop;
|
|
576
625
|
}
|
|
626
|
+
if (observeFailed) {
|
|
627
|
+
// Moving workspaces, the old workspace's keys still in .env do not count.
|
|
628
|
+
const missing = [SETUP_ENV.cloud_key, SETUP_ENV.traces_key].filter((n) => !before.get(n) || (moving && before.get(n) === had.get(n)));
|
|
629
|
+
const why = observeFailed.why.replace(/\.$/, "");
|
|
630
|
+
warnings.push(`Traces and tests won't work${missing.length ? ` until .env has ${missing.join(" and ")}` : ""}: ${why}.${observeFailed.fix ? ` Then: ${observeFailed.fix}` : ""}`);
|
|
631
|
+
}
|
|
577
632
|
printWarnings(warnings);
|
|
578
633
|
const open = CHANNELS.filter((c) => typeof fields[`${c}_sender`] === "string");
|
|
579
634
|
const displayName = agentDisplayName(dir);
|
|
@@ -605,6 +660,7 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
605
660
|
whatsapp_link: answer.whatsapp_link ?? null, imessage_link: answer.imessage_link ?? null, try_urls: links,
|
|
606
661
|
direct_links: Object.fromEntries(Object.entries(direct).map(([c, d]) => [c, d.link])), join_message: Object.values(direct)[0]?.message ?? null,
|
|
607
662
|
trial, model_key: model.report,
|
|
663
|
+
workspace: typeof answer.workspace_id === "string" && answer.workspace_id ? { id: answer.workspace_id, name: answer.workspace_name ?? null } : null,
|
|
608
664
|
// FLOW_KEY's state, never its value: made in this call, with its id, mode and scopes.
|
|
609
665
|
flow_key: { set: Boolean(before.get(FLOW_KEY) || process.env[FLOW_KEY]?.trim()), minted: flowKey?.minted === true,
|
|
610
666
|
...(flowKey?.minted ? { kid: flowKey.kid ?? null, mode: flowKey.mode ?? null, scopes: flowKey.scopes ?? [], previous_expires_at: flowKey.previous_expires_at ?? null } : {}) },
|
|
@@ -612,11 +668,13 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
612
668
|
...(forLink ? {} : { next }), warnings,
|
|
613
669
|
});
|
|
614
670
|
}
|
|
615
|
-
say(`Agent ${answer.agent_id}. In .env:`);
|
|
671
|
+
say(answer.agent_id ? `Agent ${answer.agent_id}. In .env:` : "In .env:");
|
|
616
672
|
for (const w of written)
|
|
617
673
|
say(` ${w.name}=${w.value} ${w.action}${w.action === "kept" ? (w.name === FLOW_KEY ? " (--rotate key makes a new one)" : " (yours differs; --replace takes the new one)") : ""}`);
|
|
618
674
|
if (previous)
|
|
619
675
|
say(previous);
|
|
676
|
+
if (answer.workspace_id)
|
|
677
|
+
say(`Workspace: ${answer.workspace_name ? `${answer.workspace_name} (${answer.workspace_id})` : answer.workspace_id}`);
|
|
620
678
|
if (answer.dashboard_url)
|
|
621
679
|
say(`Its dashboard: ${answer.dashboard_url}`);
|
|
622
680
|
// Said by link instead, once, with its link.
|
|
@@ -634,7 +692,7 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
634
692
|
say(`If npm start is running, restart it to add ${added}.`);
|
|
635
693
|
say(`Next: ${next.join(", then ")}`);
|
|
636
694
|
}
|
|
637
|
-
return { answer };
|
|
695
|
+
return { answer, warnings };
|
|
638
696
|
}
|
|
639
697
|
/** iMessage's count of the people it may reach on the trial, as a line; undefined when the answer had none. */
|
|
640
698
|
function peopleLine(t) {
|
|
@@ -653,6 +711,7 @@ function objectField(fields, name) {
|
|
|
653
711
|
*/
|
|
654
712
|
function envEntries(fields) {
|
|
655
713
|
const entries = Object.entries(SETUP_ENV).map(([field, envName]) => [envName, fields[field]]);
|
|
714
|
+
entries.push([WORKSPACE_ENV, fields[WORKSPACE_FIELD]]);
|
|
656
715
|
const key = objectField(fields, "flow_key");
|
|
657
716
|
if ((key?.env ?? FLOW_KEY) === FLOW_KEY && typeof key?.value === "string")
|
|
658
717
|
entries.push([FLOW_KEY, key.value]);
|
|
@@ -729,7 +788,8 @@ async function status(json, out, say) {
|
|
|
729
788
|
const named = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf8")).name ?? basename(process.cwd());
|
|
730
789
|
const name = named.replace(/^@[^/]*\//, "").replace(/[^A-Za-z0-9 ._-]/g, "-").slice(0, 64) || "my-agent";
|
|
731
790
|
const have = [...Object.values(SETUP_ENV).filter((n) => n !== SETUP_ENV.try_url), ...MODEL_KEY_NAMES];
|
|
732
|
-
|
|
791
|
+
const workspace = value(WORKSPACE_ENV);
|
|
792
|
+
server = await setup(url, current.token, { name, hostname: hostname(), channels: [...CHANNELS], have, ...(workspace ? { workspace } : {}) });
|
|
733
793
|
}
|
|
734
794
|
}
|
|
735
795
|
catch {
|
|
@@ -748,11 +808,13 @@ async function status(json, out, say) {
|
|
|
748
808
|
...(model.report.set && !model.next ? ["npm start"] : []),
|
|
749
809
|
...(!joinCode || !value(SETUP_ENV.messaging_key) ? [NEXT.link] : CHANNELS.filter((c) => channels[c].open && !channels[c].sender).map((c) => `${NEXT.link} --channel ${c}`)),
|
|
750
810
|
];
|
|
811
|
+
const workspace = value(WORKSPACE_ENV) || "personal";
|
|
751
812
|
if (json)
|
|
752
|
-
out({ model_key: model.report, flow_key: { set: Boolean(value(FLOW_KEY)) }, signed_in: account ? { email: account.email, account: account.account } : false, join_code: joinCode, messaging_key: Boolean(value(SETUP_ENV.messaging_key)), channels, next });
|
|
813
|
+
out({ model_key: model.report, flow_key: { set: Boolean(value(FLOW_KEY)) }, workspace, signed_in: account ? { email: account.email, account: account.account } : false, join_code: joinCode, messaging_key: Boolean(value(SETUP_ENV.messaging_key)), channels, next });
|
|
753
814
|
if (model.report.env && model.report.env !== FLOW_KEY)
|
|
754
815
|
say(value(FLOW_KEY) ? `${FLOW_KEY} is set.` : `${FLOW_KEY}: not set yet (${RUN} setup writes it).`);
|
|
755
816
|
say(model.line);
|
|
817
|
+
say(`Workspace: ${workspace}`);
|
|
756
818
|
say(account ? `Signed in as ${account.email}.` : current ? "Not signed in here (or flow.engineer could not be reached)." : "Not signed in.");
|
|
757
819
|
for (const c of CHANNELS) {
|
|
758
820
|
const { sender, open, trial } = channels[c];
|
|
@@ -800,7 +862,8 @@ function dotenvValue() {
|
|
|
800
862
|
* in .env, or no sender for a WhatsApp or iMessage link): it signs in first when it has to (the same
|
|
801
863
|
* login, resuming a code already waiting) and runs setup, then asks flow.engineer for the channel's
|
|
802
864
|
* page. A channel that is not open yet is said once, with the links of the channels that are (exit
|
|
803
|
-
* 0); with none open, it stops with the server's next step.
|
|
865
|
+
* 0); with none open, it stops with the server's next step. A setup whose observe part failed but
|
|
866
|
+
* whose Messaging part came goes on the same way, with one warning (setupEnv).
|
|
804
867
|
*/
|
|
805
868
|
async function link(options, json, out, say) {
|
|
806
869
|
const again = `${NEXT.link}${options.channel === "telegram" ? "" : ` --channel ${options.channel}`}`;
|
|
@@ -810,7 +873,9 @@ async function link(options, json, out, say) {
|
|
|
810
873
|
let credit;
|
|
811
874
|
let trial;
|
|
812
875
|
let had;
|
|
813
|
-
|
|
876
|
+
let warnings = [];
|
|
877
|
+
const newWorkspace = options.workspace !== undefined && (movesWorkspace(options.workspace, value) || !value(WORKSPACE_ENV));
|
|
878
|
+
if (newWorkspace || !value(SETUP_ENV.join_code) || !value(SETUP_ENV.messaging_key) || (needsSender && !value(SENDER_ENV[options.channel]))) {
|
|
814
879
|
agentFolder(process.cwd());
|
|
815
880
|
if (!(await signedInAccount(loginUrl()))) {
|
|
816
881
|
say("Not signed in to flow.engineer yet: signing in first.");
|
|
@@ -818,7 +883,7 @@ async function link(options, json, out, say) {
|
|
|
818
883
|
}
|
|
819
884
|
say("Setting up this agent's keys in .env.");
|
|
820
885
|
had = value;
|
|
821
|
-
({ answer: { model_credit: credit, trial } } = await setupEnv({ replace: false, rotate: [], ci: false }, json, out, say, { channel: options.channel }));
|
|
886
|
+
({ answer: { model_credit: credit, trial }, warnings } = await setupEnv({ replace: false, rotate: [], ci: false, workspace: options.workspace }, json, out, say, { channel: options.channel }));
|
|
822
887
|
value = dotenvValue();
|
|
823
888
|
}
|
|
824
889
|
const joinCode = value(SETUP_ENV.join_code);
|
|
@@ -880,6 +945,8 @@ async function link(options, json, out, say) {
|
|
|
880
945
|
url: first.link, page: first.page, join_message: first.message, channel, join_code: joinCode, reply_only: Boolean(replyOnly), say_to_person: sayToPerson,
|
|
881
946
|
...(notOpen ? { not_open: options.channel, message: notOpen, links: Object.fromEntries(links.map(([c, d]) => [c, d.link])) } : {}),
|
|
882
947
|
model_key: model.report, running: running ? { url: running.url } : false, next: next.join(", then "),
|
|
948
|
+
// Setup's warnings when it ran just now (also on its own line above), such as observe's part failing.
|
|
949
|
+
...(warnings.length ? { warnings } : {}),
|
|
883
950
|
});
|
|
884
951
|
}
|
|
885
952
|
if (notOpen) {
|
|
@@ -71,6 +71,9 @@ export interface FlowKey {
|
|
|
71
71
|
}
|
|
72
72
|
export interface SetupAnswer {
|
|
73
73
|
agent_id: string;
|
|
74
|
+
/** The observe workspace (acc_...) the agent and its cloud and traces keys are in; absent from an older server. */
|
|
75
|
+
workspace_id?: string;
|
|
76
|
+
workspace_name?: string;
|
|
74
77
|
/** Only the keys minted in this call come back (those the request did not say it has). */
|
|
75
78
|
cloud_key?: string;
|
|
76
79
|
traces_key?: string;
|
|
@@ -148,6 +151,7 @@ export declare function setup(url: string, token: string, request: {
|
|
|
148
151
|
hostname: string;
|
|
149
152
|
channels: string[];
|
|
150
153
|
have: string[];
|
|
154
|
+
workspace?: string;
|
|
151
155
|
rotate?: true | string[];
|
|
152
156
|
rotate_now?: true;
|
|
153
157
|
}): Promise<SetupAnswer>;
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// authorization_pending, slow_down, expired_token, access_denied, invalid_grant
|
|
7
7
|
// POST /cli/revoke (bearer) {} ends this token
|
|
8
8
|
// GET /cli/whoami (bearer) -> {account, email, token}
|
|
9
|
-
// POST /cli/setup (bearer) {name, hostname, channels, have, rotate?, rotate_now?}
|
|
10
|
-
// -> {agent_id, cloud_key?, traces_key?, messaging_key?, flow_key?, model_credit?, join_code, account, app_id,
|
|
9
|
+
// POST /cli/setup (bearer) {name, hostname, channels, have, workspace?, rotate?, rotate_now?}
|
|
10
|
+
// -> {agent_id, workspace_id?, workspace_name?, cloud_key?, traces_key?, messaging_key?, flow_key?, model_credit?, join_code, account, app_id,
|
|
11
11
|
// dashboard_url, try_url, <channel>_sender?, <channel>_link? (telegram, whatsapp, imessage: each only while
|
|
12
12
|
// that shared sender is open), try_urls?, trial?, minted, next}; a 502 carries every field that worked too;
|
|
13
13
|
// flow_key is the agent's one Flow key, {env: "FLOW_KEY", value: "flow_test_...", kid, mode, scopes, agent_id,
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
// the reason in warnings; every folder's setup makes its own key, the credit is the account's;
|
|
19
19
|
// rotate is true (every key) or the answer fields to make again, such as ["flow_key"]; rotate_now: true
|
|
20
20
|
// ends the old FLOW_KEY at once instead of 24 h later (previous_expires_at);
|
|
21
|
+
// workspace is an observe workspace's acc_ id or name (case-insensitive), 1 to 100 characters; absent: the
|
|
22
|
+
// personal one; workspace_id and workspace_name are the workspace the agent and its cloud and traces keys are in
|
|
23
|
+
// (whenever observe answered; an older server sends neither); 403 not_a_member, 409 workspace_ambiguous (pass the id);
|
|
21
24
|
// 429 slow_down with Retry-After; 403 for a trial limit (sandbox_allowance_used, ...), its next prose
|
|
22
25
|
// Any answer may carry warnings: up to 20 strings to show the owner as they are.
|
|
23
26
|
// GET <model URL>/credit (bearer FLOW_KEY) -> {limit_usd, spent_usd, left_usd, default_model, models};
|
package/docs/agents.md
CHANGED
|
@@ -63,9 +63,12 @@ before you hand it back; run them yourself, and do not offer them to the owner a
|
|
|
63
63
|
and `init`, `setup`, `link` and `status` say so too, with the way to get one first in `next`.
|
|
64
64
|
Each folder's `setup` makes its own `FLOW_KEY` (the credit is the account's);
|
|
65
65
|
`setup --rotate key` replaces a leaked one (the old one works 24 h more; `--now` ends it at once).
|
|
66
|
+
- `setup --workspace <name|acc_id>` (and `link --workspace`) puts the agent, its cloud and traces keys in
|
|
67
|
+
that observe workspace of yours and keeps its `acc_` id in `.env` as `FLOW_FRAMEWORK_WORKSPACE`, which later
|
|
68
|
+
runs send; another workspace makes those two keys again (403 `not_a_member`, 409 `workspace_ambiguous`: pass the id).
|
|
66
69
|
- `status` reports `FLOW_KEY` and the model key (never their values; `flow_key.set` in `--json`; with
|
|
67
70
|
`FLOW_KEY` the model's key, the credit left from Flow, `model_key.flow_credit`; a key Flow does not
|
|
68
|
-
know gets `setup --rotate key`), the sign-in, each channel's sender in `.env`
|
|
71
|
+
know gets `setup --rotate key`), the workspace (`FLOW_FRAMEWORK_WORKSPACE`, else `personal`), the sign-in, each channel's sender in `.env`
|
|
69
72
|
and, signed in, which channels are open now with their trial counts. It writes nothing.
|
|
70
73
|
- `chat` builds the agent from `src/agent.ts` and writes as its first test person. Each reply is
|
|
71
74
|
printed (`agent: ...`) once the turn is over; `--json` gives one JSON line per thing it sends.
|
|
@@ -80,6 +83,14 @@ before you hand it back; run them yourself, and do not offer them to the owner a
|
|
|
80
83
|
and WhatsApp links"), with the open channels' links, and exits 0: `link --json` then has
|
|
81
84
|
`not_open` (the channel asked for), `links` (each open channel's) and `url`/`channel` for the
|
|
82
85
|
first of them. Only when no channel is open does it exit 3 (`channel_not_open`).
|
|
86
|
+
- Texting needs only Flow Messaging's part of setup (and a model key). When setup's answer has it
|
|
87
|
+
(the join code, the Messaging key and senders) but observe's part failed (traces and the test
|
|
88
|
+
cloud: `observe_failed`, or observe's own limit), `link` prints one warning line on stderr,
|
|
89
|
+
`Traces and tests won't work until .env has FLOW_FRAMEWORK_CLOUD_KEY and FLOW_FRAMEWORK_TRACES_KEY:
|
|
90
|
+
<the server's reason>. Then: <its next>`, then the link, the channel's trial line and its `Next:`
|
|
91
|
+
as usual, and exits 0; `link --json` has the line in `warnings`, on setup's line and on its own.
|
|
92
|
+
Give it to the owner; `setup` makes the missing keys once it is fixed. Run again, `link` finds
|
|
93
|
+
Messaging's part in `.env` and does not run setup. `setup` on its own still exits 3 for it.
|
|
83
94
|
- On WhatsApp and iMessage the agent can only reply after the owner writes first (WhatsApp: within
|
|
84
95
|
24 hours of their last message); it cannot message them first. `link --json` says so
|
|
85
96
|
(`reply_only: true`, and in `say_to_person`): tell the owner.
|
|
@@ -105,7 +116,7 @@ before you hand it back; run them yourself, and do not offer them to the owner a
|
|
|
105
116
|
| 0 | Done | Go on |
|
|
106
117
|
| 1 | Usage, not signed in, not an agent's folder, or a login URL that is not https | Fix the command, or run `next` |
|
|
107
118
|
| 2 | flow.engineer could not be reached, or did not answer in time (`timeout`) | Try again |
|
|
108
|
-
| 3 | Refused: denied, expired, a product failed, no channel open yet (one closed while others are open is exit 0 with their links), a server with no CLI sign-in (`server_unsupported`), or a free-trial limit (`sandbox_allowance_used`, the iMessage people cap; HTTP 403) | Run `next`; for a trial limit `next` is a sentence: give it to the owner as it is |
|
|
119
|
+
| 3 | Refused: denied, expired, a product failed (for `link`, only when Messaging's part did: observe's alone is exit 0 with a warning), no channel open yet (one closed while others are open is exit 0 with their links), a server with no CLI sign-in (`server_unsupported`), or a free-trial limit (`sandbox_allowance_used`, the iMessage people cap; HTTP 403) | Run `next`; for a trial limit `next` is a sentence: give it to the owner as it is |
|
|
109
120
|
| 4 | Sign-in still waiting for the owner's approval, or flow.engineer asked to wait (`slow_down`, HTTP 429) | Approval: ask the owner to approve, then run `next` (the same code is kept). `slow_down`: wait `retry_after` seconds (printed as `Wait <n> s`), then run `next` |
|
|
110
121
|
|
|
111
122
|
`login` and `link` wait at most `--timeout` seconds (120 by default) before exit 4, and no single
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Builds
|
|
1
|
+
// Builds the agent. The playground (main.ts) and the tests build it the same way; tests pass their own
|
|
2
2
|
// model, events and (simulated) tools.
|
|
3
3
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -11,7 +11,7 @@ export interface BuildOptions {
|
|
|
11
11
|
tools?: Tool[];
|
|
12
12
|
/** Channels that are connected, such as Telegram through Flow Messaging (main.ts adds them when .env has its key); one named whatsapp replaces the unconnected one. */
|
|
13
13
|
channels?: Channel[];
|
|
14
|
-
/** Where
|
|
14
|
+
/** Where the agent runs, for a host with no file system (Cloudflare): its files as text, its database, no schedule timer. */
|
|
15
15
|
host?: Pick<AgentOptions, "files" | "database" | "scheduleCheckMs">;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -20,7 +20,7 @@ export const tools = cafeTools;
|
|
|
20
20
|
// The café's numbers and web chat are not connected yet, so every provider refuses to send. Test
|
|
21
21
|
// people write from the playground, and replies to them go back there, never through these.
|
|
22
22
|
const notConnected = async () => {
|
|
23
|
-
throw new Error("not connected yet: only test people in the playground reach
|
|
23
|
+
throw new Error("not connected yet: only test people in the playground reach the agent for now");
|
|
24
24
|
};
|
|
25
25
|
const provider = { sendMessage: notConnected, react: notConnected, typing: notConnected, markRead: notConnected, status: notConnected };
|
|
26
26
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Which Flow Messaging channels
|
|
1
|
+
// Which Flow Messaging channels the agent is on: Telegram always, WhatsApp and iMessage when .env names
|
|
2
2
|
// their sender (FLOW_MESSAGING_WHATSAPP_SENDER, FLOW_MESSAGING_IMESSAGE_SENDER).
|
|
3
3
|
|
|
4
4
|
import type { flowMessaging } from "../../../flow-messaging/src/index.ts";
|
|
@@ -12,7 +12,7 @@ import { buildAgent } from "./agent.ts";
|
|
|
12
12
|
import { channelNames, flowChannels } from "./channels.ts";
|
|
13
13
|
|
|
14
14
|
// What people call it: package.json's displayName (`npx @flow-engineer/flow link --display-name <name>` changes it).
|
|
15
|
-
const { displayName: name = "
|
|
15
|
+
const { displayName: name = "your agent" } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { displayName?: string };
|
|
16
16
|
const play = simulators({ title: `${name} · Bean & Leaf` });
|
|
17
17
|
const fm = process.env.FLOW_MESSAGING_KEY ? flowMessaging() : undefined;
|
|
18
18
|
const channels = fm ? flowChannels(fm) : [];
|
package/examples/cafe/src/mcp.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const mcp = mcpServer({
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
// What people call it: package.json's displayName.
|
|
21
|
-
const { displayName: name = "
|
|
21
|
+
const { displayName: name = "your agent" } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { displayName?: string };
|
|
22
22
|
// MCP_PORT, else 8790 or the next free port above it.
|
|
23
23
|
let port: number;
|
|
24
24
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The agent's tools, over a tiny in-memory café. Placing an order is slow on purpose (a real point-of-sale
|
|
2
2
|
// takes seconds), so it shows a tool moving to the background.
|
|
3
3
|
|
|
4
4
|
import { tool, z } from "../../../src/index.ts";
|