@flow-engineer/flow 0.3.0 → 0.3.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.
@@ -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 Pip's prompt, skills, tools and tests into yours. A provider key is optional:
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,9 @@ 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`. `login`, `setup` and `link` also run on their own.
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.
353
355
 
354
356
  In `src/main.ts`, put `fm.telegram()` from `@flow-engineer/flow/flow-messaging` in the agent's
355
357
  channels and call `fm.start(agent, { cursorFile: ".flow/flow-messaging-cursor" })`: with no webhook
@@ -511,7 +511,7 @@ async function setupEnv(options, json, out, say, forLink) {
511
511
  out({ ci: { secret: SETUP_ENV.cloud_key, steps } });
512
512
  for (const line of steps)
513
513
  say(line);
514
- return { answer: {} };
514
+ return { answer: {}, warnings: [] };
515
515
  }
516
516
  const dir = process.cwd();
517
517
  agentFolder(dir);
@@ -550,18 +550,29 @@ async function setupEnv(options, json, out, say, forLink) {
550
550
  };
551
551
  const chosenPage = (n) => forLink !== undefined && n === SETUP_ENV.try_url;
552
552
  let answer;
553
+ // For link: the server's refusal when Messaging's part came (a join code) and only observe's failed.
554
+ let observeFailed;
553
555
  try {
554
556
  // Every channel: each comes back with its shared sender only while it is open.
555
557
  answer = await setup(url, token, { name, hostname: hostname(), channels: [...CHANNELS], have, ...(rotate ? { rotate } : {}), ...(rotateNow ? { rotate_now: true } : {}) });
556
558
  }
557
559
  catch (error) {
558
- // The keys minted anyway are not shown again, so whatever the error carries goes into .env first.
559
- if (error instanceof LoginRefused) {
560
- const written = writeEnv(file, lines, before, envEntries(withTryUrl(error.fields)), (n) => options.replace || rotatedEnv.has(n) || chosenPage(n));
561
- if (written.some((w) => w.action === "added" || w.action === "replaced"))
562
- say(`Kept in .env before the error: ${written.map((w) => `${w.name} ${w.action}`).join(", ")}.`);
560
+ // Texting needs only Messaging's part, so link goes on without observe's (traces and the test
561
+ // cloud) and says what is missing; setup alone, whose job is both, still stops.
562
+ if (forLink && error instanceof LoginRefused && typeof error.fields.join_code === "string" && error.fields.join_code) {
563
+ const { error: _code, error_description: why, next: fix, ...rest } = error.fields;
564
+ answer = rest;
565
+ observeFailed = { why: typeof why === "string" && why ? why : error.message, fix: typeof fix === "string" && fix ? fix : error.next };
566
+ }
567
+ else {
568
+ // The keys minted anyway are not shown again, so whatever the error carries goes into .env first.
569
+ if (error instanceof LoginRefused) {
570
+ const written = writeEnv(file, lines, before, envEntries(withTryUrl(error.fields)), (n) => options.replace || rotatedEnv.has(n) || chosenPage(n));
571
+ if (written.some((w) => w.action === "added" || w.action === "replaced"))
572
+ say(`Kept in .env before the error: ${written.map((w) => `${w.name} ${w.action}`).join(", ")}.`);
573
+ }
574
+ throw error;
563
575
  }
564
- throw error;
565
576
  }
566
577
  const fields = answer;
567
578
  const warnings = warningsOf(fields);
@@ -574,6 +585,11 @@ async function setupEnv(options, json, out, say, forLink) {
574
585
  stop.warnings = warnings;
575
586
  throw stop;
576
587
  }
588
+ if (observeFailed) {
589
+ const missing = [SETUP_ENV.cloud_key, SETUP_ENV.traces_key].filter((n) => !before.get(n));
590
+ const why = observeFailed.why.replace(/\.$/, "");
591
+ warnings.push(`Traces and tests won't work${missing.length ? ` until .env has ${missing.join(" and ")}` : ""}: ${why}.${observeFailed.fix ? ` Then: ${observeFailed.fix}` : ""}`);
592
+ }
577
593
  printWarnings(warnings);
578
594
  const open = CHANNELS.filter((c) => typeof fields[`${c}_sender`] === "string");
579
595
  const displayName = agentDisplayName(dir);
@@ -612,7 +628,7 @@ async function setupEnv(options, json, out, say, forLink) {
612
628
  ...(forLink ? {} : { next }), warnings,
613
629
  });
614
630
  }
615
- say(`Agent ${answer.agent_id}. In .env:`);
631
+ say(answer.agent_id ? `Agent ${answer.agent_id}. In .env:` : "In .env:");
616
632
  for (const w of written)
617
633
  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
634
  if (previous)
@@ -634,7 +650,7 @@ async function setupEnv(options, json, out, say, forLink) {
634
650
  say(`If npm start is running, restart it to add ${added}.`);
635
651
  say(`Next: ${next.join(", then ")}`);
636
652
  }
637
- return { answer };
653
+ return { answer, warnings };
638
654
  }
639
655
  /** iMessage's count of the people it may reach on the trial, as a line; undefined when the answer had none. */
640
656
  function peopleLine(t) {
@@ -800,7 +816,8 @@ function dotenvValue() {
800
816
  * in .env, or no sender for a WhatsApp or iMessage link): it signs in first when it has to (the same
801
817
  * login, resuming a code already waiting) and runs setup, then asks flow.engineer for the channel's
802
818
  * 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.
819
+ * 0); with none open, it stops with the server's next step. A setup whose observe part failed but
820
+ * whose Messaging part came goes on the same way, with one warning (setupEnv).
804
821
  */
805
822
  async function link(options, json, out, say) {
806
823
  const again = `${NEXT.link}${options.channel === "telegram" ? "" : ` --channel ${options.channel}`}`;
@@ -810,6 +827,7 @@ async function link(options, json, out, say) {
810
827
  let credit;
811
828
  let trial;
812
829
  let had;
830
+ let warnings = [];
813
831
  if (!value(SETUP_ENV.join_code) || !value(SETUP_ENV.messaging_key) || (needsSender && !value(SENDER_ENV[options.channel]))) {
814
832
  agentFolder(process.cwd());
815
833
  if (!(await signedInAccount(loginUrl()))) {
@@ -818,7 +836,7 @@ async function link(options, json, out, say) {
818
836
  }
819
837
  say("Setting up this agent's keys in .env.");
820
838
  had = value;
821
- ({ answer: { model_credit: credit, trial } } = await setupEnv({ replace: false, rotate: [], ci: false }, json, out, say, { channel: options.channel }));
839
+ ({ answer: { model_credit: credit, trial }, warnings } = await setupEnv({ replace: false, rotate: [], ci: false }, json, out, say, { channel: options.channel }));
822
840
  value = dotenvValue();
823
841
  }
824
842
  const joinCode = value(SETUP_ENV.join_code);
@@ -880,6 +898,8 @@ async function link(options, json, out, say) {
880
898
  url: first.link, page: first.page, join_message: first.message, channel, join_code: joinCode, reply_only: Boolean(replyOnly), say_to_person: sayToPerson,
881
899
  ...(notOpen ? { not_open: options.channel, message: notOpen, links: Object.fromEntries(links.map(([c, d]) => [c, d.link])) } : {}),
882
900
  model_key: model.report, running: running ? { url: running.url } : false, next: next.join(", then "),
901
+ // Setup's warnings when it ran just now (also on its own line above), such as observe's part failing.
902
+ ...(warnings.length ? { warnings } : {}),
883
903
  });
884
904
  }
885
905
  if (notOpen) {
package/docs/agents.md CHANGED
@@ -80,6 +80,14 @@ before you hand it back; run them yourself, and do not offer them to the owner a
80
80
  and WhatsApp links"), with the open channels' links, and exits 0: `link --json` then has
81
81
  `not_open` (the channel asked for), `links` (each open channel's) and `url`/`channel` for the
82
82
  first of them. Only when no channel is open does it exit 3 (`channel_not_open`).
83
+ - Texting needs only Flow Messaging's part of setup (and a model key). When setup's answer has it
84
+ (the join code, the Messaging key and senders) but observe's part failed (traces and the test
85
+ cloud: `observe_failed`, or observe's own limit), `link` prints one warning line on stderr,
86
+ `Traces and tests won't work until .env has FLOW_FRAMEWORK_CLOUD_KEY and FLOW_FRAMEWORK_TRACES_KEY:
87
+ <the server's reason>. Then: <its next>`, then the link, the channel's trial line and its `Next:`
88
+ as usual, and exits 0; `link --json` has the line in `warnings`, on setup's line and on its own.
89
+ Give it to the owner; `setup` makes the missing keys once it is fixed. Run again, `link` finds
90
+ Messaging's part in `.env` and does not run setup. `setup` on its own still exits 3 for it.
83
91
  - On WhatsApp and iMessage the agent can only reply after the owner writes first (WhatsApp: within
84
92
  24 hours of their last message); it cannot message them first. `link --json` says so
85
93
  (`reply_only: true`, and in `say_to_person`): tell the owner.
@@ -105,7 +113,7 @@ before you hand it back; run them yourself, and do not offer them to the owner a
105
113
  | 0 | Done | Go on |
106
114
  | 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
115
  | 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 |
116
+ | 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
117
  | 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
118
 
111
119
  `login` and `link` wait at most `--timeout` seconds (120 by default) before exit 4, and no single
@@ -1,4 +1,4 @@
1
- // Builds Pip. The playground (main.ts) and the tests build it the same way; tests pass their own
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 Pip runs, for a host with no file system (Cloudflare): its files as text, its database, no schedule timer. */
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 Pip for now");
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 Pip is on: Telegram always, WhatsApp and iMessage when .env names
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 = "Pip" } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { displayName?: string };
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) : [];
@@ -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 = "Pip" } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { displayName?: string };
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
- // Pip's tools, over a tiny in-memory café. Placing an order is slow on purpose (a real point-of-sale
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-engineer/flow",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "An open-source runtime for chat agents that feel like a person texting back.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {