@flow-engineer/flow 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +167 -10
  2. package/coding-agents/auth-skill.md +137 -0
  3. package/coding-agents/context-skill.md +94 -0
  4. package/coding-agents/skill.md +219 -17
  5. package/coding-agents/test-skill.md +3 -1
  6. package/coding-agents/tools-skill.md +78 -1
  7. package/dist/cli/src/auth.d.ts +11 -0
  8. package/dist/cli/src/auth.js +147 -0
  9. package/dist/cli/src/commands.d.ts +119 -0
  10. package/dist/cli/src/commands.js +212 -0
  11. package/dist/cli/src/index.d.ts +2 -0
  12. package/dist/cli/src/index.js +5 -0
  13. package/dist/cli/src/run.d.ts +28 -0
  14. package/dist/cli/src/run.js +523 -0
  15. package/dist/cli/src/terminal.d.ts +3 -0
  16. package/dist/cli/src/terminal.js +135 -0
  17. package/dist/flow-messaging/src/client.d.ts +2 -2
  18. package/dist/flow-messaging/src/client.js +3 -3
  19. package/dist/flow-messaging/src/index.js +17 -9
  20. package/dist/mcp/src/client.d.ts +54 -0
  21. package/dist/mcp/src/client.js +175 -0
  22. package/dist/mcp/src/describe.d.ts +41 -0
  23. package/dist/mcp/src/describe.js +110 -0
  24. package/dist/mcp/src/index.d.ts +4 -0
  25. package/dist/mcp/src/index.js +7 -0
  26. package/dist/mcp/src/server.d.ts +70 -0
  27. package/dist/mcp/src/server.js +252 -0
  28. package/dist/mcp/src/terminal.d.ts +24 -0
  29. package/dist/mcp/src/terminal.js +54 -0
  30. package/dist/simulators/src/index.d.ts +1 -1
  31. package/dist/simulators/src/page.html +267 -49
  32. package/dist/simulators/src/server.d.ts +27 -0
  33. package/dist/simulators/src/server.js +264 -40
  34. package/dist/src/agent.d.ts +69 -3
  35. package/dist/src/agent.js +162 -8
  36. package/dist/src/channels.d.ts +6 -4
  37. package/dist/src/channels.js +4 -3
  38. package/dist/src/conversation.d.ts +17 -1
  39. package/dist/src/conversation.js +170 -16
  40. package/dist/src/events.d.ts +1 -1
  41. package/dist/src/index.d.ts +2 -2
  42. package/dist/src/index.js +1 -1
  43. package/dist/src/model.d.ts +27 -0
  44. package/dist/src/model.js +85 -13
  45. package/dist/src/skills.d.ts +4 -0
  46. package/dist/src/skills.js +23 -0
  47. package/dist/src/store.d.ts +2 -0
  48. package/dist/src/store.js +18 -0
  49. package/dist/src/tools.d.ts +69 -0
  50. package/dist/src/tools.js +17 -1
  51. package/dist/src/traces.js +32 -4
  52. package/dist/src/types.d.ts +5 -0
  53. package/dist/testing/src/account.d.ts +1 -1
  54. package/dist/testing/src/account.js +368 -57
  55. package/dist/testing/src/chat.js +24 -16
  56. package/dist/testing/src/check.d.ts +4 -0
  57. package/dist/testing/src/check.js +90 -0
  58. package/dist/testing/src/cli.js +77 -19
  59. package/dist/testing/src/flow-login.d.ts +33 -0
  60. package/dist/testing/src/flow-login.js +30 -5
  61. package/dist/testing/src/new.d.ts +14 -5
  62. package/dist/testing/src/new.js +54 -20
  63. package/dist/testing/src/plugin.d.ts +4 -0
  64. package/dist/testing/src/plugin.js +116 -0
  65. package/docs/agents.md +87 -12
  66. package/examples/cafe/src/cli.ts +24 -0
  67. package/examples/cafe/src/mcp.ts +22 -0
  68. package/examples/cafe/src/tools.ts +13 -0
  69. package/flow-messaging/README.md +3 -1
  70. package/package.json +28 -1
package/dist/src/store.js CHANGED
@@ -153,6 +153,24 @@ export class Store {
153
153
  async addIdentity(conversationId, channel, address, verifiedBy) {
154
154
  await this.db.run("INSERT INTO identities (agent_id, channel, address, conversation_id, verified_by) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO NOTHING", [this.agentId, channel, address, conversationId, verifiedBy]);
155
155
  }
156
+ /** Bind verified login proof; an identity already owned by another account cannot move. */
157
+ async linkIdentity(conversationId, channel, address) {
158
+ return this.db.transaction(async (tx) => {
159
+ const lock = this.db.dialect === "postgres" ? " FOR UPDATE" : "";
160
+ const [previous] = await tx.query(`SELECT conversation_id FROM identities WHERE agent_id = ? AND channel = ? AND address = ?${lock}`, [this.agentId, channel, address]);
161
+ const rows = await tx.query(`INSERT INTO identities (agent_id, channel, address, conversation_id, verified_by)
162
+ VALUES (?, ?, ?, ?, 'resolver') ON CONFLICT (agent_id, channel, address) DO UPDATE SET
163
+ conversation_id = excluded.conversation_id, verified_by = excluded.verified_by
164
+ WHERE identities.conversation_id = excluded.conversation_id OR EXISTS (
165
+ SELECT 1 FROM conversations c WHERE c.agent_id = identities.agent_id
166
+ AND c.id = identities.conversation_id AND c.person_key NOT LIKE 'key:%') RETURNING conversation_id`, [this.agentId, channel, address, conversationId]);
167
+ if (rows.length && previous && previous.conversation_id !== conversationId) {
168
+ // Signing in must not undo an anonymous sender's STOP or other no-contact preferences.
169
+ await tx.run("UPDATE no_contact SET conversation_id = ? WHERE agent_id = ? AND conversation_id = ?", [conversationId, this.agentId, previous.conversation_id]);
170
+ }
171
+ return rows.length > 0;
172
+ });
173
+ }
156
174
  async touchIdentity(channel, address, at) {
157
175
  await this.db.run("UPDATE identities SET last_message_at = ? WHERE agent_id = ? AND channel = ? AND address = ?", [at, this.agentId, channel, address]);
158
176
  }
@@ -13,6 +13,8 @@ export interface ToolContext {
13
13
  key: string | null;
14
14
  /** The channel of their latest message. */
15
15
  channel: string;
16
+ /** Provider/server-issued address on a chat channel; absent for standalone MCP calls. */
17
+ address?: string;
16
18
  /** The model's id for this call, as the history keeps it. */
17
19
  callId: string;
18
20
  /**
@@ -23,7 +25,40 @@ export interface ToolContext {
23
25
  idempotencyKey: string;
24
26
  /** Aborted when the conversation closes (the person was forgotten): stop and drop the work. */
25
27
  signal: AbortSignal;
28
+ /** Server-side backend access for this verified person. Never supplied by the model or serialized into history or traces. */
29
+ credentials?: Credentials;
26
30
  }
31
+ /** A business supplies named backend credentials or clients, resolved afresh for the verified person. */
32
+ export type CredentialProvider = (name: string, context: Readonly<Omit<ToolContext, "credentials">>) => unknown | Promise<unknown>;
33
+ export interface Credentials {
34
+ /** The business decides how to obtain, refresh and revoke access. The name is written by the tool author, not an account id from the model. */
35
+ get<T = unknown>(name: string): Promise<T>;
36
+ }
37
+ /** Server-side identity at a chat tool boundary. No credentials or model-supplied account ids. */
38
+ export interface ChatAuthContext {
39
+ conversationId: string;
40
+ person: string;
41
+ key: string | null;
42
+ channel: string;
43
+ address: string;
44
+ tool: string;
45
+ signal: AbortSignal;
46
+ }
47
+ /** Brand-owned login: OAuth, an existing login page, verified phone, or a custom challenge. */
48
+ export interface ChatAuth {
49
+ /** Recheck a resolved account's current access. Without this, a non-null resolved key is enough. */
50
+ verify?: (context: Readonly<ChatAuthContext>) => boolean | Promise<boolean>;
51
+ /** Called only when a protected operation needs authentication. A custom in-chat prompt needs no URL. */
52
+ signIn: (context: Readonly<ChatAuthContext>) => {
53
+ message: string;
54
+ url?: string;
55
+ } | Promise<{
56
+ message: string;
57
+ url?: string;
58
+ }>;
59
+ }
60
+ /** Bind backend access without putting secrets on an enumerable context or in any event. */
61
+ export declare function toolContext(context: Omit<ToolContext, "credentials">, provider?: CredentialProvider): ToolContext;
27
62
  export interface Tool<Input = any> {
28
63
  name: string;
29
64
  description: string;
@@ -31,6 +66,35 @@ export interface Tool<Input = any> {
31
66
  run(input: Input, context: ToolContext): Promise<unknown> | unknown;
32
67
  /** The longest the reply waits for this tool before it moves to the background (default: the agent's, 60 s). */
33
68
  deadlineMs?: number;
69
+ /** A name for people ("Place an order"); derived from `name` when absent. */
70
+ title?: string;
71
+ /** The result's shape. Over MCP the result is checked against it and sent as structured content too. */
72
+ output?: z.ZodType;
73
+ /** Short lines a host shows while the tool runs and once it is done (at most 64 characters each). */
74
+ status?: {
75
+ running?: string;
76
+ done?: string;
77
+ };
78
+ /**
79
+ * Whether the person must be signed in. "required" (the default) refuses a call without sign-in,
80
+ * "optional" runs either way, "none" never needs it. Enforced where sign-in is configured:
81
+ * createAgent({ auth }) for chat, or mcpServer({ auth }) for MCP. Existing agents without auth
82
+ * keep their current behaviour.
83
+ */
84
+ signIn?: SignIn;
85
+ /** How the tool behaves, for hosts to decide what to ask the person first. Nothing enforces them. */
86
+ hints?: ToolHints;
87
+ }
88
+ export type SignIn = "required" | "optional" | "none";
89
+ export interface ToolHints {
90
+ /** It changes nothing (default false: a tool counts as a write). */
91
+ readOnly?: boolean;
92
+ /** A write that may delete or overwrite (default true for writes; always false for reads). */
93
+ destructive?: boolean;
94
+ /** Calling it again with the same input changes nothing more (default false). */
95
+ idempotent?: boolean;
96
+ /** It reaches things outside the business's own systems, such as the web (default true). */
97
+ openWorld?: boolean;
34
98
  }
35
99
  /**
36
100
  * Marks a tool the test cloud plays in place of a built-in (get_user_profile): it is offered only
@@ -38,6 +102,11 @@ export interface Tool<Input = any> {
38
102
  */
39
103
  export declare const STAND_IN: unique symbol;
40
104
  export declare function tool<Input>(definition: Tool<Input>): Tool<Input>;
105
+ /**
106
+ * Marks a tool whose input schema is given as JSON Schema (a tool another MCP server defines): the
107
+ * model is shown that schema exactly, not one rebuilt from the zod that validates it.
108
+ */
109
+ export declare const INPUT_SCHEMA: unique symbol;
41
110
  export declare function toSpec(t: Tool): ToolSpec;
42
111
  /** A tool's output as the model reads it. */
43
112
  export declare function render(output: unknown): string;
package/dist/src/tools.js CHANGED
@@ -1,6 +1,16 @@
1
1
  // Tools are plain functions with a zod input. The runtime decides at call time whether one runs
2
2
  // in the reply or carries on in the background; tool authors mark nothing.
3
3
  import { z } from "zod";
4
+ /** Bind backend access without putting secrets on an enumerable context or in any event. */
5
+ export function toolContext(context, provider) {
6
+ const identity = Object.freeze({ ...context });
7
+ if (provider)
8
+ Object.defineProperty(context, "credentials", {
9
+ value: Object.freeze({ get: (name) => Promise.resolve().then(() => provider(name, identity)) }),
10
+ enumerable: false,
11
+ });
12
+ return context;
13
+ }
4
14
  /**
5
15
  * Marks a tool the test cloud plays in place of a built-in (get_user_profile): it is offered only
6
16
  * to an agent that has that built-in, and then instead of it, so a test never runs the real one.
@@ -9,8 +19,14 @@ export const STAND_IN = Symbol("chinwag.standIn");
9
19
  export function tool(definition) {
10
20
  return definition;
11
21
  }
22
+ /**
23
+ * Marks a tool whose input schema is given as JSON Schema (a tool another MCP server defines): the
24
+ * model is shown that schema exactly, not one rebuilt from the zod that validates it.
25
+ */
26
+ export const INPUT_SCHEMA = Symbol("chinwag.inputSchema");
12
27
  export function toSpec(t) {
13
- const { $schema: _ignored, ...parameters } = z.toJSONSchema(t.input);
28
+ const given = t[INPUT_SCHEMA];
29
+ const { $schema: _ignored, ...parameters } = given ?? z.toJSONSchema(t.input);
14
30
  return { name: t.name, description: t.description, parameters };
15
31
  }
16
32
  /** A tool's output as the model reads it. */
@@ -41,10 +41,14 @@ export function tracesFromEnv(env = process.env) {
41
41
  return { key, url: setting("TRACES_URL", env)?.trim() || OBSERVE_URL, content: setting("TRACES_CONTENT", env) === "1" };
42
42
  }
43
43
  /** The endpoint's channel names: Flow Framework's custom UIs are web or app chats. */
44
- const CHANNEL_KIND = { sms: "sms", whatsapp: "whatsapp", custom: "web" };
44
+ const CHANNEL_KIND = { sms: "sms", mms: "mms", whatsapp: "whatsapp", telegram: "telegram", imessage: "imessage", web: "web", voice: "voice", other: "other", custom: "web", cli: "cli" };
45
45
  let warnedMetadataOnly = false;
46
46
  export function traces(options, agent) {
47
- const base = (options.url ?? "").replace(/\/+$/, "");
47
+ const url = options.url ?? "";
48
+ let end = url.length;
49
+ while (end > 0 && url.charCodeAt(end - 1) === 47)
50
+ end--;
51
+ const base = url.slice(0, end);
48
52
  if (!options.key)
49
53
  throw new Error("flow: traces need a key");
50
54
  if (!base)
@@ -58,6 +62,7 @@ export function traces(options, agent) {
58
62
  console.warn("flow: traces are metadata only. Set FLOW_FRAMEWORK_TRACES_CONTENT=1 to send what was written.");
59
63
  }
60
64
  const open = new Map();
65
+ const calls = new Map();
61
66
  /** Messages and background results that came while no turn was open: they join the next one. */
62
67
  const waiting = new Map();
63
68
  const queue = [];
@@ -168,6 +173,29 @@ export function traces(options, agent) {
168
173
  }
169
174
  }
170
175
  function handle(e) {
176
+ // An MCP/CLI call has no conversation or model turn. Reuse the same bounded exporter.
177
+ if (["mcp", "mcp-plugin", "cli"].includes(String(e.surface))) {
178
+ const call = String(e.callId);
179
+ if (e.type === "tool.start") {
180
+ calls.set(call, { at: e.at, ...(content ? { input: e.input } : {}) });
181
+ if (calls.size > MAX_QUEUED)
182
+ calls.delete(calls.keys().next().value);
183
+ }
184
+ else if (e.type === "tool.end") {
185
+ const started = calls.get(call);
186
+ calls.delete(call);
187
+ const attributes = toolAttributes(e, started?.input);
188
+ attributes.push(attr("chinwag.surface", String(e.surface)), attr("openinference.span.kind", "TOOL"));
189
+ for (const [field, key] of Object.entries({ client: "chinwag.mcp.client", clientVersion: "chinwag.mcp.client_version", session: "chinwag.mcp.session", person: "chinwag.person", command: "chinwag.command" })) {
190
+ if (typeof e[field] === "string")
191
+ attributes.push(attr(key, e[field]));
192
+ }
193
+ if (e.ok === false)
194
+ attributes.push(attr("error", typeof e.error === "string" ? clip(e.error) : "Tool failed"));
195
+ enqueue([span(hex(16), `mcp.call ${e.tool}`, started?.at ?? e.at - Number(e.ms ?? 0), e.at, attributes, undefined, e.ok === false)]);
196
+ }
197
+ return;
198
+ }
171
199
  const id = e.conversationId;
172
200
  const turn = open.get(id);
173
201
  switch (e.type) {
@@ -241,8 +269,8 @@ export function traces(options, agent) {
241
269
  const attributes = [attr("gen_ai.operation.name", "execute_tool"), attr("gen_ai.tool.name", String(e.tool)), attr("gen_ai.tool.call.id", String(e.callId))];
242
270
  if (content && input !== undefined)
243
271
  attributes.push(attr("gen_ai.tool.call.arguments", clip(JSON.stringify(input))));
244
- if (content && typeof e.output === "string")
245
- attributes.push(attr("gen_ai.tool.call.result", clip(e.output)));
272
+ if (content && e.output !== undefined)
273
+ attributes.push(attr("gen_ai.tool.call.result", clip(typeof e.output === "string" ? e.output : JSON.stringify(e.output))));
246
274
  return attributes;
247
275
  }
248
276
  return {
@@ -85,6 +85,8 @@ export interface ListRow {
85
85
  export type Outbound = {
86
86
  type: "message";
87
87
  text: string;
88
+ /** A framework-generated login challenge; its link opens real authentication in simulators. */
89
+ authentication?: boolean;
88
90
  /** The person's message this one quotes (channels with replies). */
89
91
  replyTo?: string;
90
92
  /** Reply buttons; a tap comes back as the person's message. */
@@ -105,6 +107,8 @@ export type Outbound = {
105
107
  };
106
108
  /** The channel's own params (see Channel.params), as the agent filled them in. */
107
109
  fields?: Record<string, unknown>;
110
+ /** The same in every process for one send the business starts (a nudge, a scheduled text), for providers that take one. */
111
+ idempotencyKey?: string;
108
112
  }
109
113
  /** The agent has picked these messages up (WhatsApp's blue ticks). */
110
114
  | {
@@ -120,6 +124,7 @@ export type Outbound = {
120
124
  /** A template the owner set up with the channel (WhatsApp), sent as is, even outside its window. */
121
125
  | ({
122
126
  type: "template";
127
+ idempotencyKey?: string;
123
128
  } & Template)
124
129
  /** A short line of what the agent is doing, shown in the loading indicator; null clears it. */
125
130
  | {
@@ -1,4 +1,4 @@
1
1
  export declare const ACCOUNT_COMMANDS: Set<string>;
2
- export declare const ACCOUNT_LINES = " flow login [--wait | --no-wait] [--timeout <s>] [--force] [--no-browser] [--json]\n Sign in to flow.engineer: opens the browser, prints the link and code,\n and waits up to --timeout (default 120) for approval; --wait goes on\n waiting for the same code, --no-wait returns at once\n flow logout [--json] End this machine's sign-in and delete its token\n flow whoami [--json] Show the account you are signed in as\n flow setup [--replace] [--ci] [--json]\n Write this agent's keys (test cloud, traces, Flow Messaging) and each\n open channel's sender into ./.env (in an agent's folder: package.json)\n flow link [--channel telegram|whatsapp|imessage] [--timeout <s>] [--no-browser] [--json]\n Print the link that opens a chat with this agent from a phone; signs in\n and runs setup first when this folder is not set up yet";
2
+ export declare const ACCOUNT_LINES = " flow login [--wait | --no-wait] [--timeout <s>] [--force] [--no-browser] [--json]\n Sign in to flow.engineer: opens the browser, prints the link and code,\n and waits up to --timeout (default 120) for approval; --wait goes on\n waiting for the same code, --no-wait returns at once\n flow logout [--json] End this machine's sign-in and delete its token\n flow whoami [--json] Show the account you are signed in as\n flow setup [--replace] [--rotate <keys> [--now]] [--ci] [--json]\n Write this agent's keys (its Flow key FLOW_KEY, which also pays for the\n model on the account's trial credit when there is no provider key, test\n cloud, traces, Flow Messaging) and each open channel's sender into\n ./.env (in an agent's folder: package.json); --rotate key,cloud,traces,\n messaging or all makes those keys again (the old FLOW_KEY works 24 h\n more; --now ends it at once)\n flow link [--channel telegram|whatsapp|imessage] [--timeout <s>] [--no-browser] [--json]\n Print the link that opens a chat with this agent from a phone; signs in\n and runs setup first when this folder is not set up yet\n flow status [--json] What this folder has and lacks: FLOW_KEY and the model key (never their\n values) with the trial credit left, the\n sign-in, each channel's sender in .env, and, signed in, which channels\n are open now and their free-trial counts";
3
3
  /** Runs one account command; resolves to the exit status. */
4
4
  export declare function accountCli(command: string, args: string[]): Promise<number>;