@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
package/dist/testing/src/chat.js
CHANGED
|
@@ -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
|
|
8
|
-
//
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 (!
|
|
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
|
+
}
|
package/dist/testing/src/cli.js
CHANGED
|
@@ -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 {
|
|
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>
|
|
47
|
-
|
|
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${
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
${
|
|
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
|
-
|
|
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
|
-
${
|
|
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,
|
|
11
|
-
// <channel>_sender?, <channel>_link? (telegram, whatsapp, imessage: each only while
|
|
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
|
-
//
|
|
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
|
-
|
|
13
|
-
}): string;
|
|
21
|
+
export declare function initHere(dir?: string, options?: MakeOptions): string;
|
|
22
|
+
export {};
|
package/dist/testing/src/new.js
CHANGED
|
@@ -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 = `#
|
|
42
|
-
#
|
|
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
|
|
68
|
-
for its tools and their test files, \`flow-framework-tests\` for its test
|
|
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
|
|
71
|
-
\`
|
|
72
|
-
|
|
73
|
-
signs in, puts its other keys in \`.env\` and prints the link that
|
|
74
|
-
|
|
75
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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;
|