@flow-engineer/flow 0.2.1 → 0.2.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.
Files changed (66) 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/index.js +7 -1
  18. package/dist/mcp/src/client.d.ts +54 -0
  19. package/dist/mcp/src/client.js +175 -0
  20. package/dist/mcp/src/describe.d.ts +41 -0
  21. package/dist/mcp/src/describe.js +110 -0
  22. package/dist/mcp/src/index.d.ts +4 -0
  23. package/dist/mcp/src/index.js +7 -0
  24. package/dist/mcp/src/server.d.ts +70 -0
  25. package/dist/mcp/src/server.js +252 -0
  26. package/dist/mcp/src/terminal.d.ts +24 -0
  27. package/dist/mcp/src/terminal.js +54 -0
  28. package/dist/simulators/src/index.d.ts +1 -1
  29. package/dist/simulators/src/page.html +267 -49
  30. package/dist/simulators/src/server.d.ts +27 -0
  31. package/dist/simulators/src/server.js +264 -40
  32. package/dist/src/agent.d.ts +69 -3
  33. package/dist/src/agent.js +162 -8
  34. package/dist/src/channels.d.ts +2 -0
  35. package/dist/src/conversation.d.ts +16 -1
  36. package/dist/src/conversation.js +140 -12
  37. package/dist/src/events.d.ts +1 -1
  38. package/dist/src/index.d.ts +2 -2
  39. package/dist/src/index.js +1 -1
  40. package/dist/src/model.d.ts +27 -0
  41. package/dist/src/model.js +85 -13
  42. package/dist/src/skills.d.ts +4 -0
  43. package/dist/src/skills.js +23 -0
  44. package/dist/src/store.d.ts +2 -0
  45. package/dist/src/store.js +18 -0
  46. package/dist/src/tools.d.ts +69 -0
  47. package/dist/src/tools.js +17 -1
  48. package/dist/src/traces.js +32 -4
  49. package/dist/src/types.d.ts +2 -0
  50. package/dist/testing/src/account.d.ts +1 -1
  51. package/dist/testing/src/account.js +368 -57
  52. package/dist/testing/src/chat.js +24 -16
  53. package/dist/testing/src/check.d.ts +4 -0
  54. package/dist/testing/src/check.js +90 -0
  55. package/dist/testing/src/cli.js +77 -19
  56. package/dist/testing/src/flow-login.d.ts +33 -0
  57. package/dist/testing/src/flow-login.js +30 -5
  58. package/dist/testing/src/new.d.ts +14 -5
  59. package/dist/testing/src/new.js +54 -20
  60. package/dist/testing/src/plugin.d.ts +4 -0
  61. package/dist/testing/src/plugin.js +116 -0
  62. package/docs/agents.md +87 -12
  63. package/examples/cafe/src/cli.ts +24 -0
  64. package/examples/cafe/src/mcp.ts +22 -0
  65. package/examples/cafe/src/tools.ts +13 -0
  66. package/package.json +28 -1
@@ -4,8 +4,10 @@
4
4
  // (agent.simulate). Each line read from stdin is one message; the agent's replies are printed once
5
5
  // its turn is over, so it works typed by a person or piped by a coding agent:
6
6
  // printf 'hi\nA latte please\n' | npx @flow-engineer/flow chat
7
- // --json prints each thing the agent sends as one JSON line. The conversation lives only as long as
8
- // the command. Exit status: 0 done (stdin ended), 1 usage or the agent could not be built.
7
+ // --json prints each thing the agent sends as one JSON line, and an error as
8
+ // {"error":{"code","message","next"}} (no_model_key, no_agent, not_built, no_test_people,
9
+ // no_channel). The conversation lives only as long as the command. Exit status: 0 done (stdin
10
+ // ended), 1 usage or the agent could not be built.
9
11
  var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
10
12
  if (typeof path === "string" && /^\.\.?\//.test(path)) {
11
13
  return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
@@ -19,6 +21,7 @@ import { join } from "node:path";
19
21
  import { createInterface } from "node:readline";
20
22
  import { pathToFileURL } from "node:url";
21
23
  import { parseArgs } from "node:util";
24
+ import { NO_MODEL_KEY } from "../../src/model.js";
22
25
  import { loadEnv } from "./report.js";
23
26
  export const CHAT_LINE = ` flow chat [--channel <name>] [--person <phone>] [--json]
24
27
  Talk to the agent in this folder from the terminal: one message per line
@@ -42,11 +45,17 @@ export async function chatCli(args, input = process.stdin) {
42
45
  return 0;
43
46
  }
44
47
  loadEnv(undefined);
45
- const file = join(process.cwd(), "src", "agent.ts");
46
- if (!existsSync(file)) {
47
- console.error(`flow chat: no src/agent.ts here. Run it from your agent's folder (npx @flow-engineer/flow init makes one).`);
48
+ const json = values.json === true;
49
+ /** Stops with exit 1: the sentence on stderr, and with --json the error as one JSON line on stdout. */
50
+ const fail = (code, message, next, nextLine = next ? `Next: ${next}` : undefined) => {
51
+ console.error(`flow chat: ${message}${nextLine ? `\n${nextLine}` : ""}`);
52
+ if (json)
53
+ process.stdout.write(`${JSON.stringify({ error: { code, message, next: next ?? null } })}\n`);
48
54
  return 1;
49
- }
55
+ };
56
+ const file = join(process.cwd(), "src", "agent.ts");
57
+ if (!existsSync(file))
58
+ return fail("no_agent", "There is no src/agent.ts here: run it from your agent's folder.", "npx @flow-engineer/flow init", "Next: npx @flow-engineer/flow init makes one in an empty folder.");
50
59
  const turns = [];
51
60
  let agent;
52
61
  try {
@@ -56,24 +65,23 @@ export async function chatCli(args, input = process.stdin) {
56
65
  agent = module.buildAgent({ events: [(event) => turns.forEach((listener) => listener(event))] });
57
66
  }
58
67
  catch (error) {
59
- console.error(`flow chat: ${error.message}`);
60
- return 1;
68
+ if (error.code === NO_MODEL_KEY.code)
69
+ return fail(NO_MODEL_KEY.code, NO_MODEL_KEY.message, NO_MODEL_KEY.next, NO_MODEL_KEY.nextLine);
70
+ return fail("not_built", error.message);
61
71
  }
62
72
  const person = values.person ? agent.testPeople().find((p) => p.phone.replace(/\D/g, "") === values.person.replace(/\D/g, "")) : agent.testPeople()[0];
63
73
  const channel = values.channel ? agent.channels().find((c) => c.name === values.channel) : agent.channels()[0];
64
- if (!person || !channel) {
65
- console.error(!person
66
- ? `flow chat: ${values.person ? `${values.person} is not one of the agent's testPeople` : "the agent has no testPeople"}; declare them in createAgent({ testPeople: [{ name, phone }] }).`
67
- : `flow chat: ${values.channel ? `no channel named ${values.channel}` : "the agent has no channels"}; it has ${agent.channels().map((c) => c.name).join(", ") || "none"}.`);
68
- return 1;
69
- }
70
- agent.attachPlayground(({ outbound }) => print(outbound, channel.name, values.json === true));
74
+ if (!person)
75
+ return fail("no_test_people", `${values.person ? `${values.person} is not one of the agent's testPeople` : "the agent has no testPeople"}; declare them in createAgent({ testPeople: [{ name, phone }] }).`);
76
+ if (!channel)
77
+ return fail("no_channel", `${values.channel ? `no channel named ${values.channel}` : "the agent has no channels"}; it has ${agent.channels().map((c) => c.name).join(", ") || "none"}.`);
78
+ agent.attachPlayground(({ outbound }) => print(outbound, channel.name, json));
71
79
  turns.push((event) => {
72
80
  if (event.type === "model.error")
73
81
  console.error(`flow chat: the model failed: ${String(event.error ?? event.message ?? "error")}`);
74
82
  });
75
83
  const prompt = process.stdin.isTTY && input === process.stdin;
76
- if (!values.json)
84
+ if (!json)
77
85
  console.error(`Talking to the agent on ${channel.name} as ${person.name} (${person.phone}). Ctrl-D ends.`);
78
86
  const lines = createInterface({ input, terminal: false });
79
87
  if (prompt)
@@ -0,0 +1,4 @@
1
+ export declare const CHECK_LINE = " flow check [--json] [--plugin <dir>] Check the agent's tools, CLI file and skills for MCP and the CLI";
2
+ export declare function checkCli(argv: string[], dir?: string): Promise<number>;
3
+ /** `flow mcp update`: loads the agent with every mcpTools call writing its server's current tools to mcp.lock.json. */
4
+ export declare function mcpUpdateCli(dir?: string): Promise<number>;
@@ -0,0 +1,90 @@
1
+ // `flow check`, from an agent's folder: whether its tools can be served over MCP exactly as
2
+ // declared, whether its CLI file (src/cli.ts) still matches them, and whether the skills it would
3
+ // send to MCP hosts name tools only chats have. Rules only, no model: what it reports is
4
+ // objectively wrong (errors, exit 1) or worth a look (warnings).
5
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
6
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
7
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
8
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
9
+ });
10
+ }
11
+ return path;
12
+ };
13
+ import { existsSync } from "node:fs";
14
+ import { pluginProblems } from "./plugin.js";
15
+ import { join, resolve } from "node:path";
16
+ import { pathToFileURL } from "node:url";
17
+ import { cliProblems } from "../../cli/src/commands.js";
18
+ import { CHAT_ONLY, describe, toolsProblems } from "../../mcp/src/describe.js";
19
+ import { loadSkills, readAgentFolder } from "../../src/skills.js";
20
+ export const CHECK_LINE = ` flow check [--json] [--plugin <dir>] Check the agent's tools, CLI file and skills for MCP and the CLI`;
21
+ export async function checkCli(argv, dir = process.cwd()) {
22
+ const json = argv.includes("--json");
23
+ const errors = [];
24
+ const warnings = [];
25
+ const pluginAt = argv.indexOf("--plugin");
26
+ const pluginDir = pluginAt >= 0 ? argv[pluginAt + 1] : undefined;
27
+ if (pluginAt >= 0 && (!pluginDir || pluginDir.startsWith("--")))
28
+ errors.push("--plugin needs its generated folder");
29
+ if (pluginDir)
30
+ errors.push(...pluginProblems(dir, resolve(dir, pluginDir)));
31
+ const agentFile = join(dir, "src/agent.ts");
32
+ if (!existsSync(agentFile)) {
33
+ console.error(`No src/agent.ts in ${dir}. Run flow check from the agent's folder.`);
34
+ return 1;
35
+ }
36
+ const agent = await import(__rewriteRelativeImportExtension(pathToFileURL(agentFile).href));
37
+ const tools = Array.isArray(agent.tools) ? agent.tools : [];
38
+ if (!Array.isArray(agent.tools))
39
+ errors.push("src/agent.ts exports no tools array");
40
+ errors.push(...toolsProblems(tools));
41
+ if (existsSync(join(dir, "agent"))) {
42
+ for (const skill of loadSkills(readAgentFolder(join(dir, "agent")))) {
43
+ const named = CHAT_ONLY.exec(skill.body);
44
+ if (named)
45
+ (pluginAt >= 0 ? errors : warnings).push(`skill ${skill.name}: names ${named[1]}, which MCP hosts and the CLI do not have`);
46
+ }
47
+ }
48
+ const cliFile = join(dir, "src/cli.ts");
49
+ if (existsSync(cliFile)) {
50
+ const cli = (await import(__rewriteRelativeImportExtension(pathToFileURL(cliFile).href))).cli;
51
+ if (!cli)
52
+ errors.push("src/cli.ts exports no cli (export const cli = defineCli({ ... }))");
53
+ else {
54
+ const described = tools.flatMap((t) => {
55
+ try {
56
+ return [describe(t)];
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ });
62
+ const found = cliProblems(cli, described);
63
+ errors.push(...found.errors.map((e) => `src/cli.ts: ${e}`));
64
+ warnings.push(...found.warnings.map((w) => `src/cli.ts: ${w}`));
65
+ }
66
+ }
67
+ if (json)
68
+ console.log(JSON.stringify({ ok: errors.length === 0, errors, warnings }));
69
+ else {
70
+ for (const e of errors)
71
+ console.log(`error ${e}`);
72
+ for (const w of warnings)
73
+ console.log(`warn ${w}`);
74
+ console.log(errors.length ? `${errors.length} error${errors.length === 1 ? "" : "s"}.` : `No errors in ${tools.length} tool${tools.length === 1 ? "" : "s"}.`);
75
+ }
76
+ return errors.length ? 1 : 0;
77
+ }
78
+ /** `flow mcp update`: loads the agent with every mcpTools call writing its server's current tools to mcp.lock.json. */
79
+ export async function mcpUpdateCli(dir = process.cwd()) {
80
+ const agentFile = join(dir, "src/agent.ts");
81
+ if (!existsSync(agentFile)) {
82
+ console.error(`No src/agent.ts in ${dir}. Run flow mcp update from the agent's folder.`);
83
+ return 1;
84
+ }
85
+ const { UPDATE_ENV } = await import("../../mcp/src/client.js");
86
+ process.env[UPDATE_ENV] = "1";
87
+ await import(__rewriteRelativeImportExtension(pathToFileURL(agentFile).href));
88
+ console.error(`Updated ${join(dir, "mcp.lock.json")}. Run flow check and the tests, then commit it.`);
89
+ return 0;
90
+ }
@@ -32,25 +32,38 @@ import { request as httpsRequest } from "node:https";
32
32
  import { basename, dirname, join } from "node:path";
33
33
  import { pathToFileURL } from "node:url";
34
34
  import { parseArgs } from "node:util";
35
+ import { hasModelKey, NO_MODEL_KEY } from "../../src/model.js";
36
+ import { PLUGIN_LINE, pluginCli } from "./plugin.js";
35
37
  import { OBSERVE_URL, setting } from "../../src/traces.js";
36
38
  import { cloudHeaders, runInCloud, SessionStopped } from "./client.js";
37
39
  import { runPrinter } from "./print-run.js";
38
40
  import { checkEndpointCli } from "./check-endpoint.js";
41
+ import { CHECK_LINE as TOOLS_CHECK_LINE, checkCli, mcpUpdateCli } from "./check.js";
39
42
  import { ACCOUNT_COMMANDS, ACCOUNT_LINES, accountCli } from "./account.js";
40
43
  import { CHAT_LINE, chatCli } from "./chat.js";
41
44
  import { httpAgent } from "./http-agent.js";
42
- import { initHere, newStandalone, packageRoot } from "./new.js";
45
+ import { createInterface } from "node:readline/promises";
46
+ import { displayNameOf, folderName, initHere, newStandalone, packageRoot } from "./new.js";
43
47
  import { EXIT, finishedOutcome, loadEnv, networkCode, reportError, runVerdict, stoppedOutcome } from "./report.js";
44
48
  import { LAST_SESSION, lastSessionIn, SHOW_LINE, showCli } from "./show.js";
45
49
  import { loadSuite, parseScenario, parseToolFile, suiteHash } from "./suite.js";
46
- const NEW_LINE = ` flow new <name> Make ./<name>/, a new agent started from the café example
47
- flow init Make that agent in the current folder, named after it`;
50
+ const NEW_LINE = ` flow new <name> [--display-name <name>]
51
+ Make ./<name>/, a new agent started from the café example
52
+ flow init [--display-name <name>]
53
+ Make that agent in the current folder, named after it.
54
+ --display-name is what people call it, such as "FlatX agent": a
55
+ phone reaches it with "Hi FlatX agent (ref <code>)" (asked for when
56
+ run in a terminal; else the folder's name in words)`;
48
57
  const TEST_LINE = ` flow test [options] Run the agent's tests in a test cloud, from the agent's folder
49
58
  (src/agent.ts exports buildAgent({ model, events, tools }) and tools;
50
59
  the tests are in tests/scenarios and tests/tools)`;
51
60
  const CHECK_LINE = ` flow check-endpoint <url> [--token-env NAME] [--json]
52
61
  Check an agent's HTTP endpoint against the contract, for --agent-url
53
62
  (flow check-endpoint --help)`;
63
+ const MCP_LINE = ` flow mcp update Write the tools of the MCP servers the agent uses (mcpTools) to
64
+ mcp.lock.json, from the agent's folder; commit it after the tests`;
65
+ const CLI_LINE = ` flow cli --server <url> <command>
66
+ Any MCP server's tools as commands (flow cli --server <url> --help)`;
54
67
  const NEW_USAGE = `Usage:\n${NEW_LINE}\n\n -h, --help Show this help`;
55
68
  const TEST_OPTIONS = `Options for test:
56
69
  --cloud <url> The test cloud (default: FLOW_FRAMEWORK_CLOUD, else https://flow.engineer/observe)
@@ -102,15 +115,24 @@ the CHINWAG_ names from before the rename still work):
102
115
  FLOW_FRAMEWORK_AGENT_TOKEN
103
116
  With --agent-url: sent to it as authorization: Bearer <token>
104
117
  OPENROUTER_API_KEY The agent's model key, or OPENAI_API_KEY, ANTHROPIC_API_KEY or
105
- GEMINI_API_KEY: the first with a value is used`;
118
+ GEMINI_API_KEY: the first with a value is used, else FLOW_KEY
119
+ (the agent's Flow key, on its trial credit; flow setup writes it)`;
106
120
  const TEST_USAGE = `Usage:\n${TEST_LINE}\n\n${TEST_OPTIONS}`;
107
- const USAGE = `Usage:\n${NEW_LINE}\n${CHAT_LINE}\n${TEST_LINE}\n${SHOW_LINE}\n${CHECK_LINE}\n${ACCOUNT_LINES}\n flow --version Show the version\n\n${TEST_OPTIONS}`;
108
- const NEXT_STEPS = ` 2. Add one model key: cp .env.example .env; the file says where to make one
109
- 3. npx @flow-engineer/flow chat, and type to it (or npm start for the playground)
110
- 4. npx @flow-engineer/flow link: signs in, writes the other keys, prints the link for your phone;
111
- keep npm start running and text it
112
- 5. Tell your coding agent what to build: its skills are in .claude/skills/`;
121
+ const USAGE = `Usage:\n${NEW_LINE}\n${CHAT_LINE}\n${TEST_LINE}\n${SHOW_LINE}\n${CHECK_LINE}\n${TOOLS_CHECK_LINE}\n${MCP_LINE}\n${CLI_LINE}
122
+ ${PLUGIN_LINE}\n${ACCOUNT_LINES}\n flow --version Show the version\n\n${TEST_OPTIONS}`;
123
+ /** What to do after new or init, from step 2; a model key comes first when none is set here. */
124
+ function nextSteps() {
125
+ const steps = [
126
+ ...(hasModelKey(process.env) ? [] : [`Model key: none yet. ${NO_MODEL_KEY.nextLine.replace(/^Next: r/, "R")} (in .env: cp .env.example .env; the file says where to make one)`]),
127
+ `npm start, and open the link it prints: try it in the playground as its test people, channels side by side`,
128
+ `npx @flow-engineer/flow link: signs in, writes the other keys, prints the link for your phone;\n keep npm start running and text it`,
129
+ `Tell your coding agent what to build: its skills are in .claude/skills/`,
130
+ ];
131
+ return steps.map((step, i) => ` ${i + 2}. ${step}`).join("\n");
132
+ }
113
133
  checkNode(process.versions.node);
134
+ if (process.argv[2] === "plugin")
135
+ process.exit(pluginCli(process.argv.slice(3)));
114
136
  if (process.argv[2] === "check-endpoint") {
115
137
  // Its token may be in the agent folder's .env; no value is ever printed.
116
138
  loadEnv(undefined);
@@ -118,6 +140,20 @@ if (process.argv[2] === "check-endpoint") {
118
140
  }
119
141
  if (process.argv[2] === "show")
120
142
  process.exit(await showCli(process.argv.slice(3)));
143
+ if (process.argv[2] === "check")
144
+ process.exit(await checkCli(process.argv.slice(3)));
145
+ if (process.argv[2] === "mcp") {
146
+ if (process.argv[3] !== "update") {
147
+ console.error(MCP_LINE.trim());
148
+ process.exit(1);
149
+ }
150
+ process.exit(await mcpUpdateCli());
151
+ }
152
+ if (process.argv[2] === "cli") {
153
+ // Loaded only here: @flow-engineer/flow/cli needs @modelcontextprotocol/client, an optional peer.
154
+ const { defineCli, runCli } = await import("../../cli/src/index.js");
155
+ process.exit(await runCli(defineCli({ name: "flow" }), process.argv.slice(3)));
156
+ }
121
157
  if (process.argv[2] === "chat")
122
158
  process.exit(await chatCli(process.argv.slice(3)));
123
159
  if (ACCOUNT_COMMANDS.has(process.argv[2] ?? ""))
@@ -152,34 +188,56 @@ if (values.help || process.argv.length <= 2) {
152
188
  console.log(command === "new" || command === "init" ? NEW_USAGE : command === "test" ? TEST_USAGE : USAGE);
153
189
  process.exit(0);
154
190
  }
191
+ /** --display-name, else asked for in a terminal (Enter takes the folder's name in words), else that default. */
192
+ async function askDisplayName(given, packageName) {
193
+ if (given !== undefined || !process.stdin.isTTY || !process.stdout.isTTY)
194
+ return given;
195
+ const fallback = displayNameOf(undefined, packageName);
196
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
197
+ try {
198
+ return (await rl.question(`What should people call your agent? Their phone says "Hi <it>" (Enter for "${fallback}"): `)).trim() || undefined;
199
+ }
200
+ finally {
201
+ rl.close();
202
+ }
203
+ }
204
+ const namedLine = (dir) => {
205
+ const displayName = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).displayName;
206
+ return `People call it "${displayName}" (package.json displayName): a phone reaches it with "Hi ${displayName} (ref <code>)".`;
207
+ };
208
+ const displayNameFlag = values["display-name"];
155
209
  if (command === "new") {
156
210
  const name = rest.length === 1 ? rest[0] : "";
211
+ let dir;
157
212
  try {
158
- newStandalone(name);
213
+ if (!name)
214
+ throw new Error("Usage: flow new <name> [--display-name <name>]");
215
+ dir = newStandalone(name, { displayName: await askDisplayName(displayNameFlag, name) });
159
216
  }
160
217
  catch (error) {
161
- console.error(name ? error.message : "Usage: flow new <name>");
218
+ console.error(error.message);
162
219
  process.exit(1);
163
220
  }
164
- console.log(`Made ./${name}/ from the café example (Pip). Next:
221
+ console.log(`Made ./${name}/ from the café example (Pip). ${namedLine(dir)} Next:
165
222
  1. cd ${name} && npm install
166
- ${NEXT_STEPS}`);
223
+ ${nextSteps()}`);
167
224
  process.exit(0);
168
225
  }
169
226
  if (command === "init") {
170
227
  let name;
171
228
  try {
172
229
  if (rest.length)
173
- throw new Error("Usage: flow init (it makes the agent in the current folder; flow new <name> makes a folder for it)");
174
- name = initHere();
230
+ throw new Error("Usage: flow init [--display-name <name>] (it makes the agent in the current folder; flow new <name> makes a folder for it)");
231
+ name = initHere(process.cwd(), { displayName: await askDisplayName(displayNameFlag, folderName(process.cwd())) });
175
232
  }
176
233
  catch (error) {
177
234
  console.error(error.message);
178
235
  process.exit(1);
179
236
  }
180
- console.log(`Made ${name}, started from the café example (Pip), in this folder. Next:
237
+ loadEnv(undefined); // a .env already here may hold a model key
238
+ console.log(`Made ${name}, started from the café example (Pip), in this folder. ${namedLine(process.cwd())} Next:
181
239
  1. npm install
182
- ${NEXT_STEPS}`);
240
+ ${nextSteps()}`);
183
241
  process.exit(0);
184
242
  }
185
243
  /** Every human line of `flow test`: stdout, or stderr with --json, where stdout is the report. */
@@ -529,7 +587,7 @@ function stoppedReason(error, cloud) {
529
587
  /** The command's flags, or exits 1 with what is wrong and that command's usage. */
530
588
  function parseCommandLine() {
531
589
  const isNew = process.argv[2] === "new" || process.argv[2] === "init";
532
- const options = isNew ? { help: TEST_FLAGS.help, version: TEST_FLAGS.version } : TEST_FLAGS;
590
+ const options = isNew ? { help: TEST_FLAGS.help, version: TEST_FLAGS.version, "display-name": { type: "string" } } : TEST_FLAGS;
533
591
  try {
534
592
  return parseArgs({ allowPositionals: true, strict: true, options: options });
535
593
  }
@@ -57,6 +57,18 @@ export interface Account {
57
57
  email: string;
58
58
  account: string;
59
59
  }
60
+ /** setup's flow_key: the agent's one Flow key, for this mode (test or live). */
61
+ export interface FlowKey {
62
+ env?: string;
63
+ value?: string;
64
+ kid?: string;
65
+ mode?: string;
66
+ scopes?: string[];
67
+ agent_id?: string;
68
+ minted?: boolean;
69
+ /** When the key it replaced stops working (null: at once, or there was none). */
70
+ previous_expires_at?: string | null;
71
+ }
60
72
  export interface SetupAnswer {
61
73
  agent_id: string;
62
74
  /** Only the keys minted in this call come back (those the request did not say it has). */
@@ -88,6 +100,15 @@ export interface SetupAnswer {
88
100
  used: number;
89
101
  };
90
102
  }>;
103
+ /** The agent's one Flow key (FLOW_KEY), only when made in this call: its value is shown once. */
104
+ flow_key?: FlowKey;
105
+ /** The account's Flow trial model credit, when the agent has no provider key; absent: none (warnings say why). */
106
+ model_credit?: {
107
+ limit_usd?: number;
108
+ left_usd?: number;
109
+ base_url?: string;
110
+ default_model?: string;
111
+ };
91
112
  /** The answer fields of the keys made in this call. */
92
113
  minted?: string[];
93
114
  next?: string[];
@@ -127,7 +148,19 @@ export declare function setup(url: string, token: string, request: {
127
148
  hostname: string;
128
149
  channels: string[];
129
150
  have: string[];
151
+ rotate?: true | string[];
152
+ rotate_now?: true;
130
153
  }): Promise<SetupAnswer>;
154
+ /** GET /credit's answer: the account's Flow trial credit in US dollars. */
155
+ export interface ModelCredit {
156
+ limit_usd?: number;
157
+ spent_usd?: number;
158
+ left_usd?: number;
159
+ default_model?: string;
160
+ models?: string[];
161
+ }
162
+ /** The credit left on the account of a FLOW_KEY; `baseURL` is Flow's model API (flowModelURL). */
163
+ export declare function modelCredit(baseURL: string, key: string): Promise<ModelCredit>;
131
164
  /** The agent's .env names for what setup hands back; only the keys' and the join's names are sent as `have`. */
132
165
  export declare const SETUP_ENV: {
133
166
  readonly cloud_key: "FLOW_FRAMEWORK_CLOUD_KEY";
@@ -6,13 +6,25 @@
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}
10
- // -> {agent_id, cloud_key?, traces_key?, messaging_key?, join_code, account, app_id, dashboard_url, try_url,
11
- // <channel>_sender?, <channel>_link? (telegram, whatsapp, imessage: each only while that shared sender
12
- // is open), try_urls?, trial?, minted, next}; a 502 carries every field that worked too;
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,
11
+ // dashboard_url, try_url, <channel>_sender?, <channel>_link? (telegram, whatsapp, imessage: each only while
12
+ // that shared sender is open), try_urls?, trial?, minted, next}; a 502 carries every field that worked too;
13
+ // flow_key is the agent's one Flow key, {env: "FLOW_KEY", value: "flow_test_...", kid, mode, scopes, agent_id,
14
+ // minted: true, previous_expires_at}, only when made in this call (in every answer of that call, errors too;
15
+ // absent when have names FLOW_KEY and rotate does not); for now only the model uses it;
16
+ // model_credit is the account's Flow trial model credit, {limit_usd, left_usd, base_url, default_model}, when
17
+ // have names no provider key and the account has the credit (left_usd 0 when used up), else absent with
18
+ // the reason in warnings; every folder's setup makes its own key, the credit is the account's;
19
+ // rotate is true (every key) or the answer fields to make again, such as ["flow_key"]; rotate_now: true
20
+ // ends the old FLOW_KEY at once instead of 24 h later (previous_expires_at);
13
21
  // 429 slow_down with Retry-After; 403 for a trial limit (sandbox_allowance_used, ...), its next prose
14
22
  // Any answer may carry warnings: up to 20 strings to show the owner as they are.
15
- // Every error is flat: {"error": "<code>", "error_description": "<sentence>", "next": "<command, URL or sentence>"}.
23
+ // GET <model URL>/credit (bearer FLOW_KEY) -> {limit_usd, spent_usd, left_usd, default_model, models};
24
+ // the model URL is src/model.ts's flowModelURL (flow.engineer/llm/framework/v1); 401 invalid_api_key,
25
+ // 403 insufficient_scope (the key lacks model), 402 trial_credit_used or trial_credit_unavailable
26
+ // Every error is flat: {"error": "<code>", "error_description": "<sentence>", "next": "<command, URL or sentence>"}
27
+ // (the model API's are OpenAI's shape, {"error": {message, type, code, next}}).
16
28
  // The token is opaque (fcli_...), good only for /cli/*, and sent only to the server that issued it,
17
29
  // over https (http only to this machine). Every request gives up after REQUEST_TIMEOUT_MS at most.
18
30
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -232,6 +244,19 @@ export async function setup(url, token, request) {
232
244
  throw refused("/cli/setup", answer, url);
233
245
  return answer.body;
234
246
  }
247
+ /** The credit left on the account of a FLOW_KEY; `baseURL` is Flow's model API (flowModelURL). */
248
+ export async function modelCredit(baseURL, key) {
249
+ let end = baseURL.length;
250
+ while (end > 0 && baseURL[end - 1] === "/")
251
+ end--;
252
+ const answer = await call(baseURL.slice(0, end), "/credit", { token: key, method: "GET" });
253
+ if (answer.status !== 200) {
254
+ // OpenAI's error shape: {"error": {message, code, next}}.
255
+ const inner = answer.body.error && typeof answer.body.error === "object" ? answer.body.error : undefined;
256
+ throw refused("/credit", inner ? { ...answer, body: { error: inner.code, error_description: inner.message, next: inner.next } } : answer, baseURL);
257
+ }
258
+ return answer.body;
259
+ }
235
260
  /** The agent's .env names for what setup hands back; only the keys' and the join's names are sent as `have`. */
236
261
  export const SETUP_ENV = {
237
262
  cloud_key: "FLOW_FRAMEWORK_CLOUD_KEY",
@@ -2,12 +2,21 @@
2
2
  export declare function checkName(name: string): void;
3
3
  /** The @flow-engineer/flow package's folder: the root of a clone, or the installed package. */
4
4
  export declare function packageRoot(from?: string): string;
5
+ /** The longest display name: the join message carries it (flow.engineer's /try cuts it there too). */
6
+ export declare const MAX_DISPLAY_NAME = 60;
7
+ /** The name people call the agent, from the package name when none was given: "my-shop" is "my shop". */
8
+ export declare function displayNameOf(given: string | undefined, packageName: string): string;
9
+ interface MakeOptions {
10
+ root?: string;
11
+ /** What people call the agent; the package name in words when not given. */
12
+ displayName?: string;
13
+ }
5
14
  /** Makes `<into>/<name>` (the current folder by default) and returns it. */
6
- export declare function newStandalone(name: string, options?: {
15
+ export declare function newStandalone(name: string, options?: MakeOptions & {
7
16
  into?: string;
8
- root?: string;
9
17
  }): string;
18
+ /** The package name `flow init` gives the agent in `dir`: the folder's name in lowercase and dashes. */
19
+ export declare function folderName(dir: string): string;
10
20
  /** Makes the agent in `dir` (the current folder by default), named after the folder; returns its name. */
11
- export declare function initHere(dir?: string, options?: {
12
- root?: string;
13
- }): string;
21
+ export declare function initHere(dir?: string, options?: MakeOptions): string;
22
+ export {};
@@ -3,7 +3,8 @@
3
3
  // "@flow-engineer/flow/simulators" and "@flow-engineer/flow/flow-messaging", and the coding-agent
4
4
  // skills go in .claude/skills/, with an AGENTS.md (and a CLAUDE.md importing it) pointing at them.
5
5
  // `flow init` makes the same agent in the current folder. Inside a clone, `npm run new -- <name>`
6
- // makes agents/<name>/ instead.
6
+ // makes agents/<name>/ instead. `--display-name` is what people call the agent ("FlatX agent"), kept
7
+ // as package.json's displayName: the message a phone sends to reach it is "Hi <it> (ref <code>)".
7
8
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
8
9
  import { basename, dirname, join } from "node:path";
9
10
  import { fileURLToPath } from "node:url";
@@ -38,8 +39,10 @@ const TSCONFIG = {
38
39
  },
39
40
  include: ["src"],
40
41
  };
41
- const ENV_EXAMPLE = `# One model key. The first line with a value is the one used.
42
- # Make it at the provider, then paste it here. Never commit this file.
42
+ const ENV_EXAMPLE = `# The model key. The first line with a value is the one used. npx @flow-engineer/flow setup writes
43
+ # FLOW_KEY, the agent's one Flow key, which calls models on the account's free trial credit, so no
44
+ # provider key is needed; your own, made at the provider and pasted on its line, wins over it.
45
+ # Never commit this file.
43
46
  # OPENROUTER_API_KEY https://openrouter.ai/keys
44
47
  # OPENAI_API_KEY https://platform.openai.com/api-keys
45
48
  # ANTHROPIC_API_KEY https://console.anthropic.com/settings/keys
@@ -48,6 +51,7 @@ OPENROUTER_API_KEY=
48
51
  OPENAI_API_KEY=
49
52
  ANTHROPIC_API_KEY=
50
53
  GEMINI_API_KEY=
54
+ FLOW_KEY=
51
55
 
52
56
  # Optional. The model id for that provider; empty is that provider's default.
53
57
  # CAFE_MODEL=
@@ -64,39 +68,61 @@ const agentsMd = (name) => `# ${name}
64
68
 
65
69
  A chat agent on Flow Framework (https://github.com/flow-engineer/flow), started from its café example (Pip).
66
70
 
67
- - Load the skills in \`.claude/skills/\` before changing it: \`flow-framework\` to build the agent, \`flow-framework-tools\`
68
- for its tools and their test files, \`flow-framework-tests\` for its test scenarios.
71
+ - Load the skills in \`.claude/skills/\` before changing it: \`flow-framework\` to build the agent (and its MCP
72
+ server and CLI), \`flow-framework-tools\` for its tools and their test files, \`flow-framework-tests\` for its test
73
+ scenarios, \`flow-framework-context\` to gather what the tools are written from out of an existing
74
+ codebase, and \`flow-framework-auth\` for sign-in.
69
75
  - \`agent/AGENTS.md\` is the agent's own prompt, not instructions for coding agents.
70
- - The owner puts one model key in \`.env\` (never read or print it): \`OPENROUTER_API_KEY\`,
71
- \`OPENAI_API_KEY\`, \`ANTHROPIC_API_KEY\` or \`GEMINI_API_KEY\`. Then \`npx @flow-engineer/flow chat\`
72
- talks to it in the terminal, \`npm start\` runs it in the playground, and \`npx @flow-engineer/flow link\`
73
- signs in, puts its other keys in \`.env\` and prints the link that reaches it from a phone while
74
- \`npm start\` runs. \`npm run typecheck\` checks the types, \`npm test -- --synthetic\` runs the
75
- tests in the test cloud at https://flow.engineer/observe with the owner's \`FLOW_FRAMEWORK_CLOUD_KEY\`
76
+ - The model key goes in \`.env\` (never read or print it): \`npx @flow-engineer/flow link\` (or \`setup\`)
77
+ writes \`FLOW_KEY\`, the agent's one Flow key, which calls models on Flow's free trial credit, so no provider key is needed; a key the owner types
78
+ (\`OPENROUTER_API_KEY\`, \`OPENAI_API_KEY\`, \`ANTHROPIC_API_KEY\` or \`GEMINI_API_KEY\`) wins over it. \`npm start\` runs it in the playground,
79
+ and \`npx @flow-engineer/flow link\` signs in, puts its other keys in \`.env\` and prints the link that
80
+ reaches it from a phone while \`npm start\` runs.
81
+ - When the owner asks how to test it, recommend first the playground (\`npm start\`, then open the link
82
+ it prints: the test people and the channels side by side), then their phone (the \`link\`).
83
+ - Your own checks, not the owner's way to test: \`npx @flow-engineer/flow chat\` talks to it in the
84
+ terminal, \`npm run typecheck\` checks the types, \`npm test -- --synthetic\` runs the tests in the
85
+ test cloud at https://flow.engineer/observe with the owner's \`FLOW_FRAMEWORK_CLOUD_KEY\`
76
86
  (\`npx @flow-engineer/flow --help\`).
77
87
  - The guide for coding agents, with exit codes and next steps: https://github.com/flow-engineer/flow/blob/main/docs/agents.md
88
+ - \`npm run mcp\` serves the same tools as an MCP server (src/mcp.ts) and \`npm run cli -- --help\` is
89
+ its command line (src/cli.ts). \`npx flow check\` checks the tools and the CLI file for both.
78
90
  `;
91
+ /** The longest display name: the join message carries it (flow.engineer's /try cuts it there too). */
92
+ export const MAX_DISPLAY_NAME = 60;
93
+ /** The name people call the agent, from the package name when none was given: "my-shop" is "my shop". */
94
+ export function displayNameOf(given, packageName) {
95
+ const name = (given ?? "").replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
96
+ if (name.length > MAX_DISPLAY_NAME)
97
+ throw new Error(`--display-name is at most ${MAX_DISPLAY_NAME} characters, such as "FlatX agent".`);
98
+ return name || packageName.replace(/-/g, " ");
99
+ }
79
100
  /** Makes `<into>/<name>` (the current folder by default) and returns it. */
80
101
  export function newStandalone(name, options = {}) {
81
102
  checkName(name);
103
+ const displayName = displayNameOf(options.displayName, name);
82
104
  const dir = join(options.into ?? process.cwd(), name);
83
105
  if (existsSync(dir))
84
106
  throw new Error(`${dir} already exists: pick another name or remove it.`);
85
- makeAgent(dir, name, options.root ?? packageRoot());
107
+ makeAgent(dir, name, displayName, options.root ?? packageRoot());
86
108
  return dir;
87
109
  }
88
110
  /** What `flow init` writes into a folder; it refuses a folder that holds any of them, so nothing is overwritten. */
89
111
  const MADE = ["package.json", "tsconfig.json", "src", "agent", "tests", ".env.example", "AGENTS.md", "CLAUDE.md", ".npmrc"];
112
+ /** The package name `flow init` gives the agent in `dir`: the folder's name in lowercase and dashes. */
113
+ export function folderName(dir) {
114
+ return basename(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "my-agent";
115
+ }
90
116
  /** Makes the agent in `dir` (the current folder by default), named after the folder; returns its name. */
91
117
  export function initHere(dir = process.cwd(), options = {}) {
92
118
  const taken = MADE.filter((f) => existsSync(join(dir, f)));
93
119
  if (taken.length)
94
120
  throw new Error(`${dir} has ${taken.join(", ")} already: run flow init in an empty folder, or flow new <name> to make one.`);
95
- const name = basename(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "my-agent";
96
- makeAgent(dir, name, options.root ?? packageRoot());
121
+ const name = folderName(dir);
122
+ makeAgent(dir, name, displayNameOf(options.displayName, name), options.root ?? packageRoot());
97
123
  return name;
98
124
  }
99
- function makeAgent(dir, name, root) {
125
+ function makeAgent(dir, name, displayName, root) {
100
126
  const cafe = join(root, "examples", "cafe");
101
127
  for (const part of ["agent", "src", "tests"])
102
128
  cpSync(join(cafe, part), join(dir, part), { recursive: true });
@@ -105,21 +131,29 @@ function makeAgent(dir, name, root) {
105
131
  const source = readFileSync(path, "utf8")
106
132
  .replace(/(["'])(\.\.\/)+src\/index\.ts\1/g, '"@flow-engineer/flow"')
107
133
  .replace(/(["'])(\.\.\/)+simulators\/src\/index\.ts\1/g, '"@flow-engineer/flow/simulators"')
108
- .replace(/(["'])(\.\.\/)+flow-messaging\/src\/index\.ts\1/g, '"@flow-engineer/flow/flow-messaging"');
109
- if (/["']\.\.\/\.\.\//.test(source))
134
+ .replace(/(["'])(\.\.\/)+flow-messaging\/src\/index\.ts\1/g, '"@flow-engineer/flow/flow-messaging"')
135
+ .replace(/(["'])(\.\.\/)+mcp\/src\/index\.ts\1/g, '"@flow-engineer/flow/mcp"')
136
+ .replace(/(["'])(\.\.\/)+cli\/src\/index\.ts\1/g, '"@flow-engineer/flow/cli"');
137
+ // The CLI is the agent's own command, named after it.
138
+ const text = file === "cli.ts" ? source.replace(/name: "cafe"/, `name: "${name}"`).replace(/CAFE_SERVER/g, `${name.toUpperCase().replace(/-/g, "_")}_SERVER`) : source;
139
+ if (/["']\.\.\/\.\.\//.test(text))
110
140
  throw new Error(`flow: ${file} imports a file outside the agent; only Flow Framework's public entries can be rewritten.`);
111
- writeFileSync(path, source);
141
+ writeFileSync(path, text);
112
142
  }
113
143
  const own = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
114
144
  const pkg = {
115
145
  name,
146
+ // What people call it: the message a phone sends to reach it is "Hi <displayName> (ref <code>)".
147
+ displayName,
116
148
  version: "0.1.0",
117
149
  private: true,
118
150
  description: `${name}: a Flow Framework agent, started from the café example.`,
119
151
  type: "module",
152
+ bin: { [name]: "src/cli.ts" },
120
153
  engines: own.engines,
121
- scripts: { start: "node --env-file-if-exists=.env src/main.ts", test: "flow test", typecheck: "tsc --noEmit" },
122
- dependencies: { "@flow-engineer/flow": `^${own.version}` },
154
+ scripts: { start: "node --env-file-if-exists=.env src/main.ts", mcp: "node src/mcp.ts", cli: "node src/cli.ts", test: "flow test", typecheck: "tsc --noEmit" },
155
+ // The café serves its tools over MCP (src/mcp.ts) and as a CLI (src/cli.ts): their SDKs, pinned as Flow Framework pins them.
156
+ dependencies: { "@flow-engineer/flow": `^${own.version}`, ...own.peerDependencies },
123
157
  devDependencies: { "@types/node": own.devDependencies["@types/node"], typescript: own.devDependencies.typescript },
124
158
  };
125
159
  writeFileSync(join(dir, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
@@ -0,0 +1,4 @@
1
+ export declare const PLUGIN_LINE = " flow plugin --server <url> --out <dir> Write Claude Code and Agent Plugins packages";
2
+ export declare function pluginProblems(dir: string, out: string): string[];
3
+ export declare function writePlugins(dir: string, server: string, out: string, name?: string): void;
4
+ export declare function pluginCli(argv: string[], dir?: string): number;