@flow-engineer/flow 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +167 -10
  2. package/coding-agents/auth-skill.md +137 -0
  3. package/coding-agents/context-skill.md +94 -0
  4. package/coding-agents/skill.md +219 -17
  5. package/coding-agents/test-skill.md +3 -1
  6. package/coding-agents/tools-skill.md +78 -1
  7. package/dist/cli/src/auth.d.ts +11 -0
  8. package/dist/cli/src/auth.js +147 -0
  9. package/dist/cli/src/commands.d.ts +119 -0
  10. package/dist/cli/src/commands.js +212 -0
  11. package/dist/cli/src/index.d.ts +2 -0
  12. package/dist/cli/src/index.js +5 -0
  13. package/dist/cli/src/run.d.ts +28 -0
  14. package/dist/cli/src/run.js +523 -0
  15. package/dist/cli/src/terminal.d.ts +3 -0
  16. package/dist/cli/src/terminal.js +135 -0
  17. package/dist/flow-messaging/src/client.d.ts +2 -2
  18. package/dist/flow-messaging/src/client.js +3 -3
  19. package/dist/flow-messaging/src/index.js +17 -9
  20. package/dist/mcp/src/client.d.ts +54 -0
  21. package/dist/mcp/src/client.js +175 -0
  22. package/dist/mcp/src/describe.d.ts +41 -0
  23. package/dist/mcp/src/describe.js +110 -0
  24. package/dist/mcp/src/index.d.ts +4 -0
  25. package/dist/mcp/src/index.js +7 -0
  26. package/dist/mcp/src/server.d.ts +70 -0
  27. package/dist/mcp/src/server.js +252 -0
  28. package/dist/mcp/src/terminal.d.ts +24 -0
  29. package/dist/mcp/src/terminal.js +54 -0
  30. package/dist/simulators/src/index.d.ts +1 -1
  31. package/dist/simulators/src/page.html +267 -49
  32. package/dist/simulators/src/server.d.ts +27 -0
  33. package/dist/simulators/src/server.js +264 -40
  34. package/dist/src/agent.d.ts +69 -3
  35. package/dist/src/agent.js +162 -8
  36. package/dist/src/channels.d.ts +6 -4
  37. package/dist/src/channels.js +4 -3
  38. package/dist/src/conversation.d.ts +17 -1
  39. package/dist/src/conversation.js +170 -16
  40. package/dist/src/events.d.ts +1 -1
  41. package/dist/src/index.d.ts +2 -2
  42. package/dist/src/index.js +1 -1
  43. package/dist/src/model.d.ts +27 -0
  44. package/dist/src/model.js +85 -13
  45. package/dist/src/skills.d.ts +4 -0
  46. package/dist/src/skills.js +23 -0
  47. package/dist/src/store.d.ts +2 -0
  48. package/dist/src/store.js +18 -0
  49. package/dist/src/tools.d.ts +69 -0
  50. package/dist/src/tools.js +17 -1
  51. package/dist/src/traces.js +32 -4
  52. package/dist/src/types.d.ts +5 -0
  53. package/dist/testing/src/account.d.ts +1 -1
  54. package/dist/testing/src/account.js +368 -57
  55. package/dist/testing/src/chat.js +24 -16
  56. package/dist/testing/src/check.d.ts +4 -0
  57. package/dist/testing/src/check.js +90 -0
  58. package/dist/testing/src/cli.js +77 -19
  59. package/dist/testing/src/flow-login.d.ts +33 -0
  60. package/dist/testing/src/flow-login.js +30 -5
  61. package/dist/testing/src/new.d.ts +14 -5
  62. package/dist/testing/src/new.js +54 -20
  63. package/dist/testing/src/plugin.d.ts +4 -0
  64. package/dist/testing/src/plugin.js +116 -0
  65. package/docs/agents.md +87 -12
  66. package/examples/cafe/src/cli.ts +24 -0
  67. package/examples/cafe/src/mcp.ts +22 -0
  68. package/examples/cafe/src/tools.ts +13 -0
  69. package/flow-messaging/README.md +3 -1
  70. package/package.json +28 -1
@@ -0,0 +1,116 @@
1
+ // Plugin packages generated from an agent's skills. The two native formats deliver skills once:
2
+ // both point at the tools-only MCP endpoint. Teams can edit manifests and add files alongside them.
3
+ import { createHash } from "node:crypto";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { parseArgs } from "node:util";
7
+ import { CHAT_ONLY } from "../../mcp/src/describe.js";
8
+ import { loadSkills, readAgentFolder } from "../../src/skills.js";
9
+ export const PLUGIN_LINE = " flow plugin --server <url> --out <dir> Write Claude Code and Agent Plugins packages";
10
+ const STAMP = ".chinwag-skills.json";
11
+ function skillFiles(dir) {
12
+ const files = {};
13
+ const root = join(dir, "agent");
14
+ function visit(path) {
15
+ for (const entry of readdirSync(join(root, path), { withFileTypes: true })) {
16
+ const child = `${path}/${entry.name}`;
17
+ if (entry.isSymbolicLink())
18
+ throw new Error(`Plugin skills cannot copy a symlink: ${child}`);
19
+ if (entry.isDirectory())
20
+ visit(child);
21
+ else if (entry.isFile())
22
+ files[child] = readFileSync(join(root, child));
23
+ }
24
+ }
25
+ if (existsSync(join(root, "skills")))
26
+ visit("skills");
27
+ return files;
28
+ }
29
+ /** Hashes source bytes, rather than mtimes: a checkout cannot make an old plugin look current. */
30
+ function hashes(files) {
31
+ return Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([p, body]) => [p, createHash("sha256").update(body).digest("hex")]));
32
+ }
33
+ export function pluginProblems(dir, out) {
34
+ const expected = hashes(skillFiles(dir));
35
+ try {
36
+ const recorded = JSON.parse(readFileSync(join(out, STAMP), "utf8"));
37
+ if (JSON.stringify(recorded) !== JSON.stringify(expected))
38
+ return ["plugin skills are stale; run flow plugin again"];
39
+ for (const format of ["claude", "agent-plugins"]) {
40
+ if (JSON.stringify(hashes(Object.fromEntries(Object.keys(expected).map((p) => [p, readFileSync(join(out, format, p))])))) !== JSON.stringify(expected)) {
41
+ return [`${format} plugin skills differ from agent/skills; run flow plugin again`];
42
+ }
43
+ }
44
+ return [];
45
+ }
46
+ catch {
47
+ return ["plugin skills are missing or unreadable; run flow plugin again"];
48
+ }
49
+ }
50
+ export function writePlugins(dir, server, out, name) {
51
+ const pkg = existsSync(join(dir, "package.json")) ? JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) : {};
52
+ const pluginName = name ?? String(pkg.name ?? "agent").replace(/^@[^/]+\//, "");
53
+ if (!/^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(pluginName) || pluginName.length > 64)
54
+ throw new Error("Give --name a plugin name: lowercase letters, numbers, dots and hyphens, up to 64 characters.");
55
+ const url = new URL(server);
56
+ if (url.username || url.password || url.search || url.hash)
57
+ throw new Error("The server URL must not contain credentials, a query or a fragment.");
58
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
59
+ throw new Error("Use https for remote servers, or http on localhost.");
60
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}${url.pathname.replace(/\/+$/, "").endsWith("/plugin") ? "" : "/plugin"}`;
61
+ const files = skillFiles(dir);
62
+ for (const skill of loadSkills(readAgentFolder(join(dir, "agent")))) {
63
+ const named = CHAT_ONLY.exec(skill.body);
64
+ if (named)
65
+ throw new Error(`skill ${skill.name}: names ${named[1]}, which the plugin's MCP server does not have`);
66
+ }
67
+ const manifest = { name: pluginName, ...(pkg.version ? { version: pkg.version } : {}), ...(pkg.description ? { description: pkg.description } : {}) };
68
+ const json = (path, value) => write(path, `${JSON.stringify(value, null, 2)}\n`);
69
+ function write(path, body) {
70
+ mkdirSync(dirname(path), { recursive: true });
71
+ writeFileSync(path, body);
72
+ }
73
+ // Remove only files this generator previously owned, so deleted source skills do not linger.
74
+ let previous = {};
75
+ if (existsSync(join(out, STAMP)))
76
+ previous = JSON.parse(readFileSync(join(out, STAMP), "utf8"));
77
+ for (const path of Object.keys(previous)) {
78
+ if (!path.startsWith("skills/") || path.split("/").includes(".."))
79
+ throw new Error("Invalid plugin skills stamp");
80
+ if (!(path in files))
81
+ for (const format of ["claude", "agent-plugins"])
82
+ rmSync(join(out, format, path), { force: true });
83
+ }
84
+ for (const format of ["claude", "agent-plugins"]) {
85
+ const root = join(out, format);
86
+ for (const [path, body] of Object.entries(files))
87
+ write(join(root, path), body);
88
+ if (format === "claude") {
89
+ json(join(root, ".claude-plugin/plugin.json"), manifest);
90
+ json(join(root, ".mcp.json"), { mcpServers: { [pluginName]: { type: "http", url: url.href } } });
91
+ }
92
+ else {
93
+ json(join(root, "plugin.json"), { $schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", ...manifest });
94
+ json(join(root, "mcp.json"), { $schema: "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", mcpServers: { [pluginName]: { type: "streamable-http", url: url.href } } });
95
+ }
96
+ }
97
+ json(join(out, STAMP), hashes(files));
98
+ }
99
+ export function pluginCli(argv, dir = process.cwd()) {
100
+ try {
101
+ const { values } = parseArgs({ args: argv, options: { server: { type: "string" }, out: { type: "string" }, name: { type: "string" }, help: { type: "boolean", short: "h" } } });
102
+ if (values.help) {
103
+ console.log(PLUGIN_LINE.trim());
104
+ return 0;
105
+ }
106
+ if (!values.server || !values.out)
107
+ throw new Error(PLUGIN_LINE.trim());
108
+ writePlugins(dir, values.server, resolve(dir, values.out), values.name);
109
+ console.log(`Wrote ${values.out}/claude and ${values.out}/agent-plugins. Check: flow check --plugin ${values.out}`);
110
+ return 0;
111
+ }
112
+ catch (error) {
113
+ console.error(error.message);
114
+ return 1;
115
+ }
116
+ }
package/docs/agents.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Flow Framework for coding agents
2
2
 
3
+ This guide is at https://flow.engineer/flow-framework/agents.md: that is the link to give.
4
+
3
5
  You are building or changing a chat agent for a person (the owner) so they can text it from their
4
- phone while it runs on their laptop. No tunnel, no public URL, and no key the owner copies by hand
5
- except their model key. Every command here runs without a terminal and never reads stdin, except
6
- `chat`, which reads the messages you pipe to it.
6
+ phone while it runs on their laptop. No tunnel, no public URL, and no key the owner copies by hand.
7
+ Every command here runs without a terminal and never reads stdin, except `chat`, which reads the
8
+ messages you pipe to it.
7
9
 
8
10
  Always name the package: `npx @flow-engineer/flow ...`. `npx flow` on its own fetches another npm
9
11
  package.
@@ -11,14 +13,52 @@ package.
11
13
  ## The flow
12
14
 
13
15
  ```sh
14
- npx @flow-engineer/flow init # the starter agent in this (empty) folder; flow new <name> makes a folder
16
+ npx @flow-engineer/flow init --display-name "FlatX agent" # the starter in this (empty) folder; ask the owner what to call it first
15
17
  npm install
16
- # the owner puts one model key in .env (OPENROUTER_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY or GEMINI_API_KEY)
17
- printf 'hi\nA latte please\n' | npx @flow-engineer/flow chat # talk to it locally: no account, one message per line
18
- npx @flow-engineer/flow link --json # signs in and sets up if needed, then prints the phone link
19
- npm start # keep it running; the owner texts it from the phone
18
+ npx @flow-engineer/flow link --json # signs in and sets up if needed (the keys, Flow's trial model key among them), then prints the phone link
19
+ npm start # keep it running: the playground link it prints, and the phone link reaches it
20
+ npx @flow-engineer/flow status --json # any time: what the folder has and lacks
21
+ printf 'hi\nA latte please\n' | npx @flow-engineer/flow chat # your own check: talk to it locally, one message per line
20
22
  ```
21
23
 
24
+ Before `init` (or `flow new <folder>`), ask the owner what people should call their agent, such as
25
+ "FlatX agent", and pass it as `--display-name`. It is kept as `displayName` in `package.json`, and
26
+ the message their phone sends to reach the agent is `Hi FlatX agent (ref <code>)`; change it there
27
+ any time. Without the flag, `init` asks in a terminal and otherwise uses the folder's name in words.
28
+
29
+ ## How the owner tests it
30
+
31
+ When the owner asks how to test the agent, recommend these, in this order:
32
+
33
+ 1. **The playground in the browser**: `npm start`, then open the link it prints. The test people
34
+ (`testPeople`; Maria, Sam and Priya in the starter) and the channels are side by side, and
35
+ **Debug** shows each tool call and how fast each message was answered.
36
+ 2. **Their phone**: the link from `link` (`say_to_person` in `link --json`), while `npm start` runs.
37
+ It is the channel's own link (`url`): `https://t.me/<bot>?start=<code>` (press Start),
38
+ `https://wa.me/<number>?text=...` or, for iMessage, `sms:<number>?&body=...` (an iPhone or a Mac),
39
+ each with `Hi <displayName> (ref <code>)` filled in (`join_message`). `page` is the same on
40
+ flow.engineer, for a computer without that app.
41
+
42
+ `flow chat` and `npm test -- --synthetic` (the test cloud) are for you, to check your own work
43
+ before you hand it back; run them yourself, and do not offer them to the owner as ways to test.
44
+
45
+ - The model key: a provider key is optional. `setup` (which `link` runs; alone it needs `login`
46
+ first) writes `FLOW_KEY`, the agent's one Flow key, into `.env` (printed only as its prefix and
47
+ last 4, `FLOW_KEY=flow_test_…abcd added`; never read or print its value): with no provider key
48
+ the agent calls models through Flow with it, on the account's $2 of Flow trial credit (printed as
49
+ `Model: Flow trial credit ($1.87 left)`). The owner may add their own: one of `OPENROUTER_API_KEY`,
50
+ `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` or `GEMINI_API_KEY` in `.env` wins over `FLOW_KEY` for the model
51
+ (to switch back, empty that line), and a key they set is never written over. When the credit is
52
+ used up, a model call fails with code `trial_credit_used` (`trial_credit_unavailable` when the account gets none) and the next step: add your own key. With no key, `chat` and `npm start` stop with one sentence and
53
+ `Next: run npx @flow-engineer/flow setup to get Flow's free trial credit, or add your own key.`
54
+ (`chat --json`: `{"error":{"code":"no_model_key","message":...,"next":"npx @flow-engineer/flow setup"}}`),
55
+ and `init`, `setup`, `link` and `status` say so too, with the way to get one first in `next`.
56
+ Each folder's `setup` makes its own `FLOW_KEY` (the credit is the account's);
57
+ `setup --rotate key` replaces a leaked one (the old one works 24 h more; `--now` ends it at once).
58
+ - `status` reports `FLOW_KEY` and the model key (never their values; `flow_key.set` in `--json`; with
59
+ `FLOW_KEY` the model's key, the credit left from Flow, `model_key.flow_credit`; a key Flow does not
60
+ know gets `setup --rotate key`), the sign-in, each channel's sender in `.env`
61
+ and, signed in, which channels are open now with their trial counts. It writes nothing.
22
62
  - `chat` builds the agent from `src/agent.ts` and writes as its first test person. Each reply is
23
63
  printed (`agent: ...`) once the turn is over; `--json` gives one JSON line per thing it sends.
24
64
  - `link` does the rest when the folder is not set up (no `FLOW_FRAMEWORK_JOIN_CODE` or
@@ -27,12 +67,17 @@ npm start # keep it running; the owner
27
67
  `setup` (writes the keys and each open channel's sender into `.env`), then prints the link.
28
68
  `--channel telegram|whatsapp|imessage` picks the channel (Telegram by default). Run it in the
29
69
  agent's folder: `setup` and `link` refuse a folder with no `package.json` (`not_an_agent_folder`).
70
+ - A channel that is not open yet is said once ("iMessage isn't open yet; here are your Telegram
71
+ and WhatsApp links"), with the open channels' links, and exits 0: `link --json` then has
72
+ `not_open` (the channel asked for), `links` (each open channel's) and `url`/`channel` for the
73
+ first of them. Only when no channel is open does it exit 3 (`channel_not_open`).
30
74
  - On WhatsApp and iMessage the agent can only reply after the owner writes first (WhatsApp: within
31
75
  24 hours of their last message); it cannot message them first. `link --json` says so
32
76
  (`reply_only: true`, and in `say_to_person`): tell the owner.
33
77
  - The starter's `npm start` is on Telegram, plus WhatsApp and iMessage when `.env` has their senders
34
- (`FLOW_MESSAGING_WHATSAPP_SENDER`, `FLOW_MESSAGING_IMESSAGE_SENDER`). If it was running when
35
- `link` added one, restart it.
78
+ (`FLOW_MESSAGING_WHATSAPP_SENDER`, `FLOW_MESSAGING_IMESSAGE_SENDER`), and reads `.env` once. When
79
+ `setup` or `link` adds a channel (or the model key) it says so: "If npm start is running, restart
80
+ it to add iMessage."
36
81
  - Give the owner `say_to_person` from `link --json`, and the sign-in link and code from the
37
82
  `pending` line while you wait.
38
83
  - `npm start` connects to Flow Messaging by an outbound event stream. Messages sent while it was
@@ -40,12 +85,14 @@ npm start # keep it running; the owner
40
85
 
41
86
  ## Exit codes (login, setup, link)
42
87
 
88
+ `status` exits 0 whatever it finds (1 for a wrong flag): read its fields.
89
+
43
90
  | Code | Meaning | What to do |
44
91
  |---|---|---|
45
92
  | 0 | Done | Go on |
46
93
  | 1 | Usage, not signed in, not an agent's folder, or a login URL that is not https | Fix the command, or run `next` |
47
94
  | 2 | flow.engineer could not be reached, or did not answer in time (`timeout`) | Try again |
48
- | 3 | Refused: denied, expired, a product failed, a channel not open yet, 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 |
95
+ | 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 |
49
96
  | 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` |
50
97
 
51
98
  `login` and `link` wait at most `--timeout` seconds (120 by default) before exit 4, and no single
@@ -59,6 +106,9 @@ sign-in, and so is a code from another login URL.
59
106
  Every error is `{"error":{"code","message","next"}}` on stdout with `--json`, and
60
107
  `<message> Next: <next>` on stderr. A waiting sign-in is `{"status":"pending",...,"next":...}`.
61
108
  `next` is the exact command or URL to run; when it came from flow.engineer it is the server's.
109
+ A run gives one `next`: `setup --json`'s is a list in order (a model key first when there is
110
+ none, then `npm start` for the playground, then `link` for the phone); inside `link` setup's line
111
+ has none, and an error's `next` replaces it.
62
112
  Run it, or give it to the owner when it needs them (a browser approval, a page to open). A
63
113
  `slow_down` error also has `retry_after` (seconds, or `null` when the server gave none). A
64
114
  trial limit's `next` is a sentence, not a command: it is printed on its own line as it came; pass
@@ -71,11 +121,17 @@ an error line has it when there were any. Pass them on to the owner.
71
121
  `setup` also shows the free trial: messages used per channel, and on iMessage the people it may
72
122
  reach (`iMessage: 1 of 2 people`; `trial.imessage.contacts` `{limit, used}` in `--json`).
73
123
 
124
+ `chat` exits 1 for anything that stops it before the first message: `no_model_key`, `no_agent`
125
+ (no `src/agent.ts`), `not_built`, `no_test_people`, `no_channel`; with `--json` as
126
+ `{"error":{"code","message","next"}}` on stdout.
127
+
74
128
  ## Keys
75
129
 
76
130
  - Never ask the owner to paste a key into the chat, and never print, read aloud or commit `.env`.
77
131
  `setup` writes the keys itself and prints only their last 4 characters.
78
- - The model key is the owner's: they type it into `.env` themselves.
132
+ - The model key: Flow's trial key from `setup`, or the owner's own, typed into `.env` themselves.
133
+ - `setup --rotate cloud,traces,messaging,model` (or `all`) makes those keys again and replaces
134
+ their lines in `.env`.
79
135
  - `setup` keeps every value already in `.env` (`--replace` takes the new ones), but a name there
80
136
  with no value (`cp .env.example .env`) counts as missing and is filled on its own line. Keys
81
137
  flow.engineer made are written even when the rest of setup fails, since they are shown only once.
@@ -83,3 +139,22 @@ reach (`iMessage: 1 of 2 people`; `trial.imessage.contacts` `{limit, used}` in `
83
139
  - The sign-in token goes only to the server that issued it, over https (http only to localhost):
84
140
  with `FLOW_FRAMEWORK_LOGIN_URL` naming another server, commands stop with `signed_in_elsewhere`.
85
141
  - CI never signs in: `npx @flow-engineer/flow setup --ci` says how to give it the test cloud key.
142
+
143
+ ## Shared test numbers and privacy
144
+
145
+ The free trial runs on Flow's shared Telegram bot, WhatsApp number and iMessage line. Sending an
146
+ agent's join message (`Hi <name> (ref <code>)`, what the link fills in) binds that phone to that
147
+ agent's app on the shared sender, so the person's messages go only to that app; sending another
148
+ app's join message moves them there. Trial chats are
149
+ test data, kept for the account's retention (30 days by default). Do not use the trial for real
150
+ customers' data: connect your own number for that.
151
+
152
+ ## Stability
153
+
154
+ Flow Framework is 0.x. The commands, `--json` fields and exit codes in this guide stay compatible
155
+ within 0.2.x; a breaking change comes with a minor version bump and a note in the release. Pin the
156
+ version in `package.json` for production.
157
+
158
+ ## Host it
159
+
160
+ Coming soon: `npx @flow-engineer/flow deploy`, to keep the agent answering while the laptop sleeps.
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // The café's command line, over its MCP server (npm run mcp in another terminal):
3
+ // npm run cli -- order -h
4
+ // npm run cli -- order place --name Sam --items '[{"item":"Latte","size":"large","milk":"oat"}]'
5
+ // npm run cli -- order status BL-1040 --json
6
+ // (npm reads flags before `--` as its own: keep the `--`.) Installed, it is the `cafe` command.
7
+ // Every tool is already a command under its own name; this file adds the words and arguments.
8
+
9
+ import { fileURLToPath } from "node:url";
10
+ import { defineCli, runCli } from "../../../cli/src/index.ts";
11
+
12
+ export const cli = defineCli({
13
+ name: "cafe",
14
+ server: process.env.CAFE_SERVER ?? "http://127.0.0.1:8790/mcp",
15
+ description: "Bean & Leaf's menu and pickup orders",
16
+ groups: { order: "Place and track pickup orders" },
17
+ commands: {
18
+ menu: { tool: "get_menu", columns: ["item", "milks", "allergens"] },
19
+ "order place": { tool: "place_order", examples: [`cafe order place --name Sam --items '[{"item":"Latte","size":"large","milk":"oat"}]'`] },
20
+ "order status": { tool: "order_status", args: ["order_id"], view: ["found", "status", "ready_by"], examples: ["cafe order status BL-1040"] },
21
+ },
22
+ });
23
+
24
+ if (process.argv[1] === fileURLToPath(import.meta.url)) process.exit(await runCli(cli, process.argv.slice(2)));
@@ -0,0 +1,22 @@
1
+ // Pip's tools and skills as an MCP server, for Claude, ChatGPT or any MCP host: `npm run mcp`.
2
+ // The same tools the chat agent uses; no model key needed, since the host brings its own model.
3
+
4
+ import { createServer } from "node:http";
5
+ import { fileURLToPath } from "node:url";
6
+ import { mcpServer } from "../../../mcp/src/index.ts";
7
+ import { tools } from "./tools.ts";
8
+
9
+ export const mcp = mcpServer({
10
+ name: "bean-and-leaf",
11
+ version: "1.0.0",
12
+ tools,
13
+ dir: fileURLToPath(new URL("../agent", import.meta.url)),
14
+ // The café has no accounts yet, so every tool answers without sign-in. With accounts, add
15
+ // auth: { issuer, resource, scopes, verify } (the chinwag-auth skill).
16
+ });
17
+
18
+ const port = Number(process.env.MCP_PORT ?? 8790);
19
+ createServer((req, res) => void mcp.node(req, res)).listen(port, "127.0.0.1", () => {
20
+ console.log(`Pip's MCP server: http://127.0.0.1:${port}/mcp`);
21
+ console.log(`Try it in Claude Code: claude mcp add --transport http bean-and-leaf http://127.0.0.1:${port}/mcp`);
22
+ });
@@ -19,8 +19,15 @@ const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
19
19
  export const tools = [
20
20
  tool({
21
21
  name: "get_menu",
22
+ title: "Menu",
22
23
  description: "The full menu: items, sizes with prices in USD, milk options and allergens.",
23
24
  input: z.object({}),
25
+ output: z.object({
26
+ menu: z.array(z.object({ item: z.string(), sizes: z.record(z.string(), z.number()), milks: z.array(z.string()), allergens: z.array(z.string()) })),
27
+ note: z.string(),
28
+ }),
29
+ signIn: "none",
30
+ hints: { readOnly: true, openWorld: false },
24
31
  run: async () => {
25
32
  await wait(150);
26
33
  return { menu: MENU, note: "This is the whole menu." };
@@ -28,6 +35,7 @@ export const tools = [
28
35
  }),
29
36
  tool({
30
37
  name: "place_order",
38
+ title: "Place an order",
31
39
  description: "Place a confirmed pickup order. Returns the order number and when it will be ready.",
32
40
  input: z.object({
33
41
  name: z.string().describe("The name for the order"),
@@ -40,6 +48,9 @@ export const tools = [
40
48
  }),
41
49
  ),
42
50
  }),
51
+ output: z.object({ order_id: z.string(), ready_by: z.string(), pay_at_pickup: z.boolean() }),
52
+ status: { running: "Placing your order…", done: "Order placed" },
53
+ hints: { destructive: false, openWorld: false },
43
54
  run: async ({ items }) => {
44
55
  await wait(5_000);
45
56
  const id = `BL-${nextOrder++}`;
@@ -52,6 +63,8 @@ export const tools = [
52
63
  name: "order_status",
53
64
  description: "Where an order is: received, being made, or ready.",
54
65
  input: z.object({ order_id: z.string() }),
66
+ output: z.object({ found: z.boolean(), status: z.enum(["being made", "ready"]).optional(), ready_by: z.string().optional() }),
67
+ hints: { readOnly: true, openWorld: false },
55
68
  run: async ({ order_id }) => {
56
69
  await wait(200);
57
70
  const order = orders.get(order_id);
@@ -148,7 +148,9 @@ Each of these can also be passed in code: `flowMessaging({ apiKey, webhookSecret
148
148
  ## What it does for you
149
149
 
150
150
  - Every send carries its own Idempotency-Key. A 429 or 5xx is tried again with the same key after
151
- a short Retry-After, so a message never goes out twice.
151
+ a short Retry-After, so a message never goes out twice. A nudge or a scheduled text keeps one key
152
+ from its id (`nudge:<id>`, `run:<id>`) in every process, so a restart or a second instance does not
153
+ send it again.
152
154
  - Flow's `409 outside_window` becomes the framework's `outside_window`, so the agent knows to send a
153
155
  template.
154
156
  - A permanent refusal (opted out, no consent, ...) reaches the agent once, with "don't resend it".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-engineer/flow",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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": {
@@ -29,6 +29,14 @@
29
29
  "types": "./dist/testing/src/index.d.ts",
30
30
  "default": "./dist/testing/src/index.js"
31
31
  },
32
+ "./mcp": {
33
+ "types": "./dist/mcp/src/index.d.ts",
34
+ "default": "./dist/mcp/src/index.js"
35
+ },
36
+ "./cli": {
37
+ "types": "./dist/cli/src/index.d.ts",
38
+ "default": "./dist/cli/src/index.js"
39
+ },
32
40
  "./package.json": "./package.json"
33
41
  },
34
42
  "bin": {
@@ -61,7 +69,26 @@
61
69
  "yaml": "^2.9.1",
62
70
  "zod": "^4.6.5"
63
71
  },
72
+ "peerDependencies": {
73
+ "@modelcontextprotocol/client": "2.3.1",
74
+ "@modelcontextprotocol/server": "2.3.1",
75
+ "@stricli/core": "1.3.0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "@modelcontextprotocol/client": {
79
+ "optional": true
80
+ },
81
+ "@modelcontextprotocol/server": {
82
+ "optional": true
83
+ },
84
+ "@stricli/core": {
85
+ "optional": true
86
+ }
87
+ },
64
88
  "devDependencies": {
89
+ "@modelcontextprotocol/client": "2.3.1",
90
+ "@modelcontextprotocol/server": "2.3.1",
91
+ "@stricli/core": "1.3.0",
65
92
  "@types/node": "^26.6.5",
66
93
  "typescript": "^7.0.2"
67
94
  }