@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.
- package/README.md +167 -10
- package/coding-agents/auth-skill.md +137 -0
- package/coding-agents/context-skill.md +94 -0
- package/coding-agents/skill.md +219 -17
- package/coding-agents/test-skill.md +3 -1
- package/coding-agents/tools-skill.md +78 -1
- package/dist/cli/src/auth.d.ts +11 -0
- package/dist/cli/src/auth.js +147 -0
- package/dist/cli/src/commands.d.ts +119 -0
- package/dist/cli/src/commands.js +212 -0
- package/dist/cli/src/index.d.ts +2 -0
- package/dist/cli/src/index.js +5 -0
- package/dist/cli/src/run.d.ts +28 -0
- package/dist/cli/src/run.js +523 -0
- package/dist/cli/src/terminal.d.ts +3 -0
- package/dist/cli/src/terminal.js +135 -0
- package/dist/flow-messaging/src/client.d.ts +2 -2
- package/dist/flow-messaging/src/client.js +3 -3
- package/dist/flow-messaging/src/index.js +17 -9
- package/dist/mcp/src/client.d.ts +54 -0
- package/dist/mcp/src/client.js +175 -0
- package/dist/mcp/src/describe.d.ts +41 -0
- package/dist/mcp/src/describe.js +110 -0
- package/dist/mcp/src/index.d.ts +4 -0
- package/dist/mcp/src/index.js +7 -0
- package/dist/mcp/src/server.d.ts +70 -0
- package/dist/mcp/src/server.js +252 -0
- package/dist/mcp/src/terminal.d.ts +24 -0
- package/dist/mcp/src/terminal.js +54 -0
- package/dist/simulators/src/index.d.ts +1 -1
- package/dist/simulators/src/page.html +267 -49
- package/dist/simulators/src/server.d.ts +27 -0
- package/dist/simulators/src/server.js +264 -40
- package/dist/src/agent.d.ts +69 -3
- package/dist/src/agent.js +162 -8
- package/dist/src/channels.d.ts +6 -4
- package/dist/src/channels.js +4 -3
- package/dist/src/conversation.d.ts +17 -1
- package/dist/src/conversation.js +170 -16
- package/dist/src/events.d.ts +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/model.d.ts +27 -0
- package/dist/src/model.js +85 -13
- package/dist/src/skills.d.ts +4 -0
- package/dist/src/skills.js +23 -0
- package/dist/src/store.d.ts +2 -0
- package/dist/src/store.js +18 -0
- package/dist/src/tools.d.ts +69 -0
- package/dist/src/tools.js +17 -1
- package/dist/src/traces.js +32 -4
- package/dist/src/types.d.ts +5 -0
- package/dist/testing/src/account.d.ts +1 -1
- package/dist/testing/src/account.js +368 -57
- package/dist/testing/src/chat.js +24 -16
- package/dist/testing/src/check.d.ts +4 -0
- package/dist/testing/src/check.js +90 -0
- package/dist/testing/src/cli.js +77 -19
- package/dist/testing/src/flow-login.d.ts +33 -0
- package/dist/testing/src/flow-login.js +30 -5
- package/dist/testing/src/new.d.ts +14 -5
- package/dist/testing/src/new.js +54 -20
- package/dist/testing/src/plugin.d.ts +4 -0
- package/dist/testing/src/plugin.js +116 -0
- package/docs/agents.md +87 -12
- package/examples/cafe/src/cli.ts +24 -0
- package/examples/cafe/src/mcp.ts +22 -0
- package/examples/cafe/src/tools.ts +13 -0
- package/flow-messaging/README.md +3 -1
- 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
|
-
|
|
6
|
-
|
|
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
|
|
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
|
|
17
|
-
|
|
18
|
-
npx @flow-engineer/flow
|
|
19
|
-
|
|
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`)
|
|
35
|
-
`link`
|
|
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,
|
|
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
|
|
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);
|
package/flow-messaging/README.md
CHANGED
|
@@ -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.
|
|
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
|
}
|