@flow-engineer/flow 0.2.3 → 0.3.0
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 +3 -3
- package/coding-agents/skill.md +20 -7
- package/coding-agents/test-skill.md +50 -1
- package/dist/flow-messaging/src/index.js +8 -5
- package/dist/simulators/src/server.d.ts +2 -0
- package/dist/simulators/src/server.js +7 -1
- package/dist/src/agent.d.ts +32 -0
- package/dist/src/agent.js +38 -8
- package/dist/src/channels.d.ts +2 -0
- package/dist/src/channels.js +1 -0
- package/dist/src/conversation.d.ts +2 -0
- package/dist/src/conversation.js +8 -2
- package/dist/src/http.js +5 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/local.d.ts +22 -0
- package/dist/src/local.js +85 -0
- package/dist/src/types.d.ts +6 -0
- package/dist/testing/src/account.d.ts +1 -1
- package/dist/testing/src/account.js +78 -20
- package/dist/testing/src/business.d.ts +42 -0
- package/dist/testing/src/business.js +145 -0
- package/dist/testing/src/cli.js +10 -8
- package/dist/testing/src/client.d.ts +4 -2
- package/dist/testing/src/client.js +20 -0
- package/dist/testing/src/index.d.ts +2 -2
- package/dist/testing/src/index.js +1 -1
- package/dist/testing/src/new.d.ts +9 -0
- package/dist/testing/src/new.js +44 -5
- package/dist/testing/src/protocol.d.ts +99 -1
- package/dist/testing/src/protocol.js +9 -0
- package/dist/testing/src/suite.d.ts +142 -3
- package/dist/testing/src/suite.js +94 -10
- package/docs/agents.md +31 -9
- package/examples/cafe/src/main.ts +23 -8
- package/examples/cafe/src/mcp.ts +24 -6
- package/package.json +1 -1
|
@@ -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] [--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";
|
|
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]] [--display-name <name>] [--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] [--display-name <name>] [--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. --display-name\n (here and on setup) renames the agent: package.json's displayName and\n its prompt, agent/AGENTS.md; the phone link then says \"Hi <name>\"\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>;
|
|
@@ -19,9 +19,10 @@ import { basename, join } from "node:path";
|
|
|
19
19
|
import { parseArgs } from "node:util";
|
|
20
20
|
import { setTimeout as delay } from "node:timers/promises";
|
|
21
21
|
import { currentToken, deleteCredentials, deletePending, LoginRefused, loginUrl, pollToken, readPending, REQUEST_TIMEOUT_MS, revoke, saveCredentials, savePending, modelCredit, SETUP_ENV, setup, startDevice, TimedOut, tryLink, warningsOf, whoami, } from "./flow-login.js";
|
|
22
|
-
import { packageRoot } from "./new.js";
|
|
22
|
+
import { packageRoot, setDisplayName } from "./new.js";
|
|
23
23
|
import { flowModelURL, hasModelKey, MODEL_KEY_NAMES, NO_MODEL_KEY } from "../../src/model.js";
|
|
24
24
|
import { RUN } from "../../src/names.js";
|
|
25
|
+
import { runningAgent } from "../../src/local.js";
|
|
25
26
|
export const ACCOUNT_COMMANDS = new Set(["login", "logout", "whoami", "setup", "link", "status"]);
|
|
26
27
|
export const ACCOUNT_LINES = ` flow login [--wait | --no-wait] [--timeout <s>] [--force] [--no-browser] [--json]
|
|
27
28
|
Sign in to flow.engineer: opens the browser, prints the link and code,
|
|
@@ -29,16 +30,18 @@ export const ACCOUNT_LINES = ` flow login [--wait | --no-wait] [--timeout <s>]
|
|
|
29
30
|
waiting for the same code, --no-wait returns at once
|
|
30
31
|
flow logout [--json] End this machine's sign-in and delete its token
|
|
31
32
|
flow whoami [--json] Show the account you are signed in as
|
|
32
|
-
flow setup [--replace] [--rotate <keys> [--now]] [--ci] [--json]
|
|
33
|
+
flow setup [--replace] [--rotate <keys> [--now]] [--display-name <name>] [--ci] [--json]
|
|
33
34
|
Write this agent's keys (its Flow key FLOW_KEY, which also pays for the
|
|
34
35
|
model on the account's trial credit when there is no provider key, test
|
|
35
36
|
cloud, traces, Flow Messaging) and each open channel's sender into
|
|
36
37
|
./.env (in an agent's folder: package.json); --rotate key,cloud,traces,
|
|
37
38
|
messaging or all makes those keys again (the old FLOW_KEY works 24 h
|
|
38
39
|
more; --now ends it at once)
|
|
39
|
-
flow link [--channel telegram|whatsapp|imessage] [--timeout <s>] [--no-browser] [--json]
|
|
40
|
+
flow link [--channel telegram|whatsapp|imessage] [--display-name <name>] [--timeout <s>] [--no-browser] [--json]
|
|
40
41
|
Print the link that opens a chat with this agent from a phone; signs in
|
|
41
|
-
and runs setup first when this folder is not set up yet
|
|
42
|
+
and runs setup first when this folder is not set up yet. --display-name
|
|
43
|
+
(here and on setup) renames the agent: package.json's displayName and
|
|
44
|
+
its prompt, agent/AGENTS.md; the phone link then says "Hi <name>"
|
|
42
45
|
flow status [--json] What this folder has and lacks: FLOW_KEY and the model key (never their
|
|
43
46
|
values) with the trial credit left, the
|
|
44
47
|
sign-in, each channel's sender in .env, and, signed in, which channels
|
|
@@ -97,6 +100,7 @@ export async function accountCli(command, args) {
|
|
|
97
100
|
: {}),
|
|
98
101
|
...(command === "setup" ? { replace: { type: "boolean", default: false }, rotate: { type: "string" }, now: { type: "boolean", default: false }, ci: { type: "boolean", default: false } } : {}),
|
|
99
102
|
...(command === "link" ? { channel: { type: "string", default: "telegram" } } : {}),
|
|
103
|
+
...(command === "link" || command === "setup" ? { "display-name": { type: "string" } } : {}),
|
|
100
104
|
},
|
|
101
105
|
}));
|
|
102
106
|
}
|
|
@@ -131,7 +135,23 @@ export async function accountCli(command, args) {
|
|
|
131
135
|
// Straight to the file descriptors, never buffered: a coding agent's cut-off command keeps every line.
|
|
132
136
|
const out = (report) => writeAll(1, `${JSON.stringify(report)}\n`);
|
|
133
137
|
const say = (line) => writeAll(json ? 2 : 1, `${line}\n`);
|
|
138
|
+
let renamed = false;
|
|
134
139
|
try {
|
|
140
|
+
if (typeof values["display-name"] === "string" && values.ci !== true) {
|
|
141
|
+
agentFolder(process.cwd());
|
|
142
|
+
const before = agentDisplayName();
|
|
143
|
+
let named;
|
|
144
|
+
try {
|
|
145
|
+
named = setDisplayName(process.cwd(), values["display-name"]);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
throw new Stop("bad_display_name", error.message, undefined, EXIT.usage);
|
|
149
|
+
}
|
|
150
|
+
renamed = named.displayName !== before || named.prompt;
|
|
151
|
+
if (json)
|
|
152
|
+
out({ display_name: named.displayName, prompt_renamed: named.prompt });
|
|
153
|
+
say(`People call it "${named.displayName}" now (package.json displayName${named.prompt ? " and agent/AGENTS.md" : "; agent/AGENTS.md named neither the old name nor Pip, so it is as it was"}).`);
|
|
154
|
+
}
|
|
135
155
|
if (command === "login") {
|
|
136
156
|
await login({ noBrowser: values["no-browser"] === true, wait: values.wait === true, noWait: values["no-wait"] === true, force: values.force === true, timeoutSeconds }, json, out, say);
|
|
137
157
|
}
|
|
@@ -144,7 +164,7 @@ export async function accountCli(command, args) {
|
|
|
144
164
|
else if (command === "setup")
|
|
145
165
|
await setupEnv({ replace: values.replace === true, rotate, now: values.now === true, ci: values.ci === true }, json, out, say);
|
|
146
166
|
else
|
|
147
|
-
await link({ channel, noBrowser: values["no-browser"] === true, timeoutSeconds }, json, out, say);
|
|
167
|
+
await link({ channel, noBrowser: values["no-browser"] === true, timeoutSeconds, renamed }, json, out, say);
|
|
148
168
|
return EXIT.ok;
|
|
149
169
|
}
|
|
150
170
|
catch (error) {
|
|
@@ -519,6 +539,16 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
519
539
|
// (and, for FLOW_KEY, ends) the old one rather than adding a second.
|
|
520
540
|
const rotate = all ? true : rotated.size ? [...rotated] : undefined;
|
|
521
541
|
const rotateNow = options.now === true && rotated.has("flow_key");
|
|
542
|
+
// FLOW_FRAMEWORK_TRY_URL is one channel's page: link's channel when it is open, else the server's (Telegram's).
|
|
543
|
+
const withTryUrl = (fields) => {
|
|
544
|
+
const code = fields.join_code;
|
|
545
|
+
if (!forLink || typeof fields[`${forLink.channel}_sender`] !== "string" || typeof code !== "string" || !code)
|
|
546
|
+
return fields;
|
|
547
|
+
const pages = fields.try_urls;
|
|
548
|
+
const page = typeof pages?.[forLink.channel] === "string" ? pages[forLink.channel] : tryLink(url, code, forLink.channel);
|
|
549
|
+
return { ...fields, try_url: page };
|
|
550
|
+
};
|
|
551
|
+
const chosenPage = (n) => forLink !== undefined && n === SETUP_ENV.try_url;
|
|
522
552
|
let answer;
|
|
523
553
|
try {
|
|
524
554
|
// Every channel: each comes back with its shared sender only while it is open.
|
|
@@ -527,7 +557,7 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
527
557
|
catch (error) {
|
|
528
558
|
// The keys minted anyway are not shown again, so whatever the error carries goes into .env first.
|
|
529
559
|
if (error instanceof LoginRefused) {
|
|
530
|
-
const written = writeEnv(file, lines, before, envEntries(error.fields), (n) => options.replace || rotatedEnv.has(n));
|
|
560
|
+
const written = writeEnv(file, lines, before, envEntries(withTryUrl(error.fields)), (n) => options.replace || rotatedEnv.has(n) || chosenPage(n));
|
|
531
561
|
if (written.some((w) => w.action === "added" || w.action === "replaced"))
|
|
532
562
|
say(`Kept in .env before the error: ${written.map((w) => `${w.name} ${w.action}`).join(", ")}.`);
|
|
533
563
|
}
|
|
@@ -537,7 +567,7 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
537
567
|
const warnings = warningsOf(fields);
|
|
538
568
|
const had = new Map(before);
|
|
539
569
|
// --replace takes every new value but a model key: only --rotate key replaces FLOW_KEY.
|
|
540
|
-
const written = writeEnv(file, lines, before, envEntries(fields), (n) => rotatedEnv.has(n) || (options.replace && !MODEL_KEY_NAMES.includes(n)));
|
|
570
|
+
const written = writeEnv(file, lines, before, envEntries(withTryUrl(fields)), (n) => rotatedEnv.has(n) || (options.replace && !MODEL_KEY_NAMES.includes(n)) || chosenPage(n));
|
|
541
571
|
if (typeof answer.join_code !== "string" || !answer.join_code) {
|
|
542
572
|
const kept = written.filter((w) => w.action === "added" || w.action === "replaced").map((w) => w.name);
|
|
543
573
|
const stop = new Stop("no_join_code", `flow.engineer gave no join code${kept.length ? ` (the keys it made are in .env: ${kept.join(", ")})` : ""}, so there is no phone link yet.`, answer.next?.[0] ?? NEXT.setup, EXIT.refused);
|
|
@@ -592,19 +622,25 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
592
622
|
// Said by link instead, once, with its link.
|
|
593
623
|
if (!forLink)
|
|
594
624
|
say(model.line);
|
|
595
|
-
|
|
596
|
-
if (people && typeof people.used === "number" && typeof people.limit === "number")
|
|
597
|
-
say(`iMessage: ${people.used} of ${people.limit} people`);
|
|
625
|
+
// link says its own channel's trial line instead.
|
|
598
626
|
if (!forLink) {
|
|
627
|
+
const people = peopleLine(trial?.imessage);
|
|
628
|
+
if (people)
|
|
629
|
+
say(people);
|
|
599
630
|
for (const c of open)
|
|
600
631
|
say(`On ${CHANNEL_NAME[c]}: ${direct[c] ? `${direct[c].link} (on a computer: ${links[c]})` : links[c]}${trialNote(trial?.[c])}`);
|
|
601
632
|
const added = addedToRunning((n) => had.get(n) || process.env[n]?.trim(), (n) => before.get(n) || process.env[n]?.trim());
|
|
602
633
|
if (added)
|
|
603
|
-
say(added);
|
|
634
|
+
say(`If npm start is running, restart it to add ${added}.`);
|
|
604
635
|
say(`Next: ${next.join(", then ")}`);
|
|
605
636
|
}
|
|
606
637
|
return { answer };
|
|
607
638
|
}
|
|
639
|
+
/** iMessage's count of the people it may reach on the trial, as a line; undefined when the answer had none. */
|
|
640
|
+
function peopleLine(t) {
|
|
641
|
+
const people = t?.contacts;
|
|
642
|
+
return people && typeof people.used === "number" && typeof people.limit === "number" ? `iMessage: ${people.used} of ${people.limit} people` : undefined;
|
|
643
|
+
}
|
|
608
644
|
const trialNote = (t) => (t && typeof t.limit === "number" ? ` (free trial: ${t.used} of ${t.limit} messages used)` : "");
|
|
609
645
|
/** An answer's field when it is an object (flow_key, model_credit), else undefined (an older server sends none). */
|
|
610
646
|
function objectField(fields, name) {
|
|
@@ -658,8 +694,8 @@ function writeEnv(file, lines, before, entries, replace) {
|
|
|
658
694
|
}
|
|
659
695
|
/**
|
|
660
696
|
* What a running agent (npm start reads .env once, when it starts) lacks after .env changed from
|
|
661
|
-
* `had` to `now`: the channels it is not on yet and a model key, as
|
|
662
|
-
* nothing it uses is new.
|
|
697
|
+
* `had` to `now`: the channels it is not on yet and a model key, as words ("Telegram and the model
|
|
698
|
+
* key"); undefined when nothing it uses is new.
|
|
663
699
|
*/
|
|
664
700
|
function addedToRunning(had, now) {
|
|
665
701
|
// The starter is on Telegram whenever it has the Flow Messaging key, and on WhatsApp and iMessage
|
|
@@ -670,7 +706,7 @@ function addedToRunning(had, now) {
|
|
|
670
706
|
const what = [...channels, ...model];
|
|
671
707
|
if (!what.length)
|
|
672
708
|
return undefined;
|
|
673
|
-
return
|
|
709
|
+
return `${what.length === 1 ? what[0] : `${what.slice(0, -1).join(", ")} and ${what.at(-1)}`}`;
|
|
674
710
|
}
|
|
675
711
|
/**
|
|
676
712
|
* `flow status`: what this folder has and lacks, read from .env and the environment, and, when
|
|
@@ -772,6 +808,7 @@ async function link(options, json, out, say) {
|
|
|
772
808
|
const needsSender = options.channel !== "telegram";
|
|
773
809
|
let value = dotenvValue();
|
|
774
810
|
let credit;
|
|
811
|
+
let trial;
|
|
775
812
|
let had;
|
|
776
813
|
if (!value(SETUP_ENV.join_code) || !value(SETUP_ENV.messaging_key) || (needsSender && !value(SENDER_ENV[options.channel]))) {
|
|
777
814
|
agentFolder(process.cwd());
|
|
@@ -781,7 +818,7 @@ async function link(options, json, out, say) {
|
|
|
781
818
|
}
|
|
782
819
|
say("Setting up this agent's keys in .env.");
|
|
783
820
|
had = value;
|
|
784
|
-
({ answer: { model_credit: credit } } = await setupEnv({ replace: false, rotate: [], ci: false }, json, out, say, { channel: options.channel }));
|
|
821
|
+
({ answer: { model_credit: credit, trial } } = await setupEnv({ replace: false, rotate: [], ci: false }, json, out, say, { channel: options.channel }));
|
|
785
822
|
value = dotenvValue();
|
|
786
823
|
}
|
|
787
824
|
const joinCode = value(SETUP_ENV.join_code);
|
|
@@ -827,11 +864,22 @@ async function link(options, json, out, say) {
|
|
|
827
864
|
const replyOnly = REPLY_ONLY[channel];
|
|
828
865
|
const sayToPerson = `${notOpen ? `${what} isn't open yet. ` : ""}Your ${CHANNEL_NAME[channel]} link: ${first.link} ${OPEN_HINT[channel]} `
|
|
829
866
|
+ `On a computer without ${CHANNEL_NAME[channel]}, open ${first.page} instead.${replyOnly ? ` ${replyOnly}` : ""}`;
|
|
867
|
+
const added = had ? addedToRunning(had, value) : undefined;
|
|
868
|
+
// npm start reads .env and its name once, when it starts. Not running: start it. Running (its
|
|
869
|
+
// .flow/running.json names a live process): restart it when setup wrote .env, the name changed
|
|
870
|
+
// just now or a model key is still to come, else it is ready as it is.
|
|
871
|
+
const running = runningAgent();
|
|
872
|
+
const runStep = !running
|
|
873
|
+
? "npm start, then send the link above"
|
|
874
|
+
: had || options.renamed || model.next
|
|
875
|
+
? `restart npm start (it runs at ${running.url}${added ? ` and lacks ${added}` : " and reads .env and its name only when it starts"}), then send the link above`
|
|
876
|
+
: `send the link above (npm start is running at ${running.url})`;
|
|
877
|
+
const next = [...(model.next ? [model.next] : []), runStep];
|
|
830
878
|
if (json) {
|
|
831
879
|
out({
|
|
832
880
|
url: first.link, page: first.page, join_message: first.message, channel, join_code: joinCode, reply_only: Boolean(replyOnly), say_to_person: sayToPerson,
|
|
833
881
|
...(notOpen ? { not_open: options.channel, message: notOpen, links: Object.fromEntries(links.map(([c, d]) => [c, d.link])) } : {}),
|
|
834
|
-
model_key: model.report,
|
|
882
|
+
model_key: model.report, running: running ? { url: running.url } : false, next: next.join(", then "),
|
|
835
883
|
});
|
|
836
884
|
}
|
|
837
885
|
if (notOpen) {
|
|
@@ -843,6 +891,18 @@ async function link(options, json, out, say) {
|
|
|
843
891
|
say(first.link);
|
|
844
892
|
say(`On a computer without ${CHANNEL_NAME[channel]}: ${first.page}`);
|
|
845
893
|
}
|
|
894
|
+
// The trial's counts for the channel the link is on, when setup just gave them; no other channel's.
|
|
895
|
+
const counts = trial?.[channel];
|
|
896
|
+
if (counts) {
|
|
897
|
+
const note = trialNote(counts);
|
|
898
|
+
if (note)
|
|
899
|
+
say(`${CHANNEL_NAME[channel]} ${note.trim().slice(1, -1)}`); // "WhatsApp free trial: 3 of 300 messages used"
|
|
900
|
+
if (channel === "imessage") {
|
|
901
|
+
const people = peopleLine(counts);
|
|
902
|
+
if (people)
|
|
903
|
+
say(people);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
846
906
|
if (model.report.set)
|
|
847
907
|
say(model.line);
|
|
848
908
|
const one = notOpen && links.length > 1 ? "one" : "it";
|
|
@@ -850,13 +910,11 @@ async function link(options, json, out, say) {
|
|
|
850
910
|
// No model key: the agent cannot answer yet, so the phone does not chat with it until there is one.
|
|
851
911
|
say(model.report.set ? "that phone chats with this agent while it runs (npm start), with no public URL or tunnel." : "that phone reaches this agent while it runs (npm start), but it answers only once .env has a model key:");
|
|
852
912
|
if (model.next)
|
|
853
|
-
say(
|
|
913
|
+
say(model.line);
|
|
854
914
|
for (const [c] of links)
|
|
855
915
|
if (REPLY_ONLY[c])
|
|
856
916
|
say(REPLY_ONLY[c]);
|
|
857
|
-
|
|
858
|
-
if (added)
|
|
859
|
-
say(added);
|
|
917
|
+
say(`Next: ${next.join(", then ")}.`);
|
|
860
918
|
}
|
|
861
919
|
/**
|
|
862
920
|
* Asks flow.engineer's /try page for its direct link as JSON ({"channel", "message", "link", "page"}):
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Agent, Channel, EventSink, Incoming, Tool } from "../../src/index.ts";
|
|
2
|
+
import type { Command } from "./protocol.ts";
|
|
3
|
+
/** What a business run's agent must do: a Flow Framework Agent. */
|
|
4
|
+
export type BusinessAgent = Pick<Agent, "channels" | "channel" | "message" | "reacted" | "nudge" | "reach" | "runDue" | "conversation" | "fetch">;
|
|
5
|
+
/** What buildAgent is given for a business run, beside its tools and events. */
|
|
6
|
+
export interface BusinessBuild {
|
|
7
|
+
channels: Channel[];
|
|
8
|
+
testing: {
|
|
9
|
+
/** The run's clock. */
|
|
10
|
+
now: () => number;
|
|
11
|
+
/** No schedule timer: the clock moves only when the cloud says, and what came due runs then. */
|
|
12
|
+
scheduleCheckMs: 0;
|
|
13
|
+
/** The secret the run's events are signed with (Flow-Signature): made for the run, never a real one. */
|
|
14
|
+
eventsSecret: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** One minute: the clock moves this far at a time, and what came due runs at each step. */
|
|
18
|
+
export declare const STEP_MS = 60000;
|
|
19
|
+
export interface BusinessContext {
|
|
20
|
+
/** The agent the session was opened with: its channels are copied for each run. */
|
|
21
|
+
sessionAgent: BusinessAgent;
|
|
22
|
+
build: (options: {
|
|
23
|
+
tools: Tool[];
|
|
24
|
+
events: EventSink[];
|
|
25
|
+
} & BusinessBuild) => BusinessAgent;
|
|
26
|
+
/** The tunnel's tools (each call asks the cloud), and the agent's own, for tools.real. */
|
|
27
|
+
tunnelTools: Tool[];
|
|
28
|
+
realTools: Tool[];
|
|
29
|
+
/** The session's event uploader. */
|
|
30
|
+
events: EventSink;
|
|
31
|
+
/** A POST to the session, sent again under `key` while it may be. */
|
|
32
|
+
post: (path: string, body: unknown, key: string, what: string) => Promise<unknown>;
|
|
33
|
+
/** Keeps a failure of fire-and-forget work as the session's (refusals allowed). */
|
|
34
|
+
watch: (work: Promise<unknown>, context: string, allowRefusal?: boolean) => void;
|
|
35
|
+
log: (line: string) => void;
|
|
36
|
+
}
|
|
37
|
+
export declare function businessRuns(ctx: BusinessContext): {
|
|
38
|
+
/** A business run's command, or a message or reaction for one: true when it was one. */
|
|
39
|
+
handle(command: Command, withFiles: (incoming: Extract<Command, {
|
|
40
|
+
type: "message";
|
|
41
|
+
}>["incoming"]) => Incoming): boolean;
|
|
42
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Business runs on the developer's side of the tunnel: scenarios the business starts (an event its
|
|
2
|
+
// backend sends, a nudge, time passing). Each run gets an agent of its own, built with a clock the
|
|
3
|
+
// cloud's commands move and with copies of the agent's own channels, so its sends go through what
|
|
4
|
+
// the agent wraps around them and come to the cloud with the run's time. The person writes through
|
|
5
|
+
// agent.message(), as a provider's webhook would, so they are a person the agent can nudge.
|
|
6
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
7
|
+
/** One minute: the clock moves this far at a time, and what came due runs at each step. */
|
|
8
|
+
export const STEP_MS = 60_000;
|
|
9
|
+
export function businessRuns(ctx) {
|
|
10
|
+
const runs = new Map();
|
|
11
|
+
const open = async (command) => {
|
|
12
|
+
const cloudId = command.conversationId;
|
|
13
|
+
const real = new Set(command.realTools ?? []);
|
|
14
|
+
const unknown = [...real].filter((name) => !ctx.realTools.some((t) => t.name === name));
|
|
15
|
+
if (unknown.length)
|
|
16
|
+
return { conversationId: cloudId, step: 0, at: command.now, error: `the agent has no tool ${unknown.join(", ")} to run for real` };
|
|
17
|
+
const run = { clock: command.now, secret: `whsec_test_${randomBytes(18).toString("hex")}`, address: command.address, channelName: command.channelName, conversationId: "", deliveries: new Set(), sent: 0 };
|
|
18
|
+
// The run's events and tool calls are the cloud's conversation, whatever the agent calls it.
|
|
19
|
+
const events = Object.assign((event) => ctx.events({ ...event, conversationId: cloudId }), { context: true });
|
|
20
|
+
const tools = ctx.tunnelTools.map((t) => {
|
|
21
|
+
if (real.has(t.name))
|
|
22
|
+
return ctx.realTools.find((r) => r.name === t.name);
|
|
23
|
+
return { ...t, run: (input, context) => t.run(input, { ...context, conversationId: cloudId }) };
|
|
24
|
+
});
|
|
25
|
+
const channels = ctx.sessionAgent.channels().map(({ name }) => {
|
|
26
|
+
const own = ctx.sessionAgent.channel(name);
|
|
27
|
+
return { ...own, send: (address, outbound) => deliver(run, cloudId, address, outbound) };
|
|
28
|
+
});
|
|
29
|
+
run.agent = ctx.build({ tools, events: [events], channels, testing: { now: () => run.clock, scheduleCheckMs: 0, eventsSecret: run.secret } });
|
|
30
|
+
run.conversationId = (await run.agent.reach({ channel: command.channelName, address: command.address })).conversationId;
|
|
31
|
+
runs.set(cloudId, run);
|
|
32
|
+
return { conversationId: cloudId, step: 0, at: run.clock };
|
|
33
|
+
};
|
|
34
|
+
const deliver = (run, cloudId, address, outbound) => {
|
|
35
|
+
if (address !== run.address) {
|
|
36
|
+
ctx.log(`${cloudId}: the agent sent to ${address}, not the run's person; the cloud does not see it`);
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
}
|
|
39
|
+
const body = { conversationId: cloudId, outbound, at: run.clock };
|
|
40
|
+
const delivery = ctx.post("outbound", body, `outbound:${cloudId}:b${++run.sent}`, "outbound delivery").then(() => { });
|
|
41
|
+
run.deliveries.add(delivery);
|
|
42
|
+
const done = () => void run.deliveries.delete(delivery);
|
|
43
|
+
delivery.then(done, done);
|
|
44
|
+
ctx.watch(delivery, "outbound delivery", true);
|
|
45
|
+
return delivery;
|
|
46
|
+
};
|
|
47
|
+
/** Until nothing more comes due now and everything set off has been sent. */
|
|
48
|
+
const settle = async (run) => {
|
|
49
|
+
for (let i = 0; i < 10; i++) {
|
|
50
|
+
const due = await run.agent.runDue();
|
|
51
|
+
await run.agent.conversation(run.conversationId)?.whenIdle();
|
|
52
|
+
await Promise.allSettled([...run.deliveries]);
|
|
53
|
+
if (!due && !run.agent.conversation(run.conversationId)?.busy)
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const event = async (run, command) => {
|
|
58
|
+
const body = JSON.stringify(command.envelope);
|
|
59
|
+
const t = Math.floor(run.clock / 1000);
|
|
60
|
+
const secret = command.signature === "bad" ? `whsec_test_${randomBytes(18).toString("hex")}` : run.secret;
|
|
61
|
+
const headers = { "content-type": "application/json", "idempotency-key": command.envelope.id };
|
|
62
|
+
if (command.signature !== "missing")
|
|
63
|
+
headers["flow-signature"] = `t=${t},v1=${createHmac("sha256", secret).update(`t.${t}.${body}`).digest("hex")}`;
|
|
64
|
+
const response = await run.agent.fetch(new Request(`http://agent.test${command.path ?? "/v1/events"}`, { method: "POST", headers, body }));
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
let parsed = text;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
catch { /* not JSON: kept as text */ }
|
|
71
|
+
return { answer: { status: response.status, body: parsed } };
|
|
72
|
+
};
|
|
73
|
+
const step = async (run, command) => {
|
|
74
|
+
const base = { conversationId: command.conversationId, step: command.step };
|
|
75
|
+
let result = {};
|
|
76
|
+
try {
|
|
77
|
+
if (command.type === "event")
|
|
78
|
+
result = await event(run, command);
|
|
79
|
+
else if (command.type === "nudge") {
|
|
80
|
+
const { expiresAt, ...rest } = command.nudge;
|
|
81
|
+
const nudged = await run.agent.nudge({ to: { channel: run.channelName, address: run.address }, ...rest, ...(expiresAt !== undefined ? { expiresAt } : {}) });
|
|
82
|
+
result = { nudge: { status: nudged.status, ...(nudged.reason ? { reason: nudged.reason } : {}) } };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
if (command.to < run.clock)
|
|
86
|
+
throw new Error("the clock never moves back");
|
|
87
|
+
while (run.clock < command.to) {
|
|
88
|
+
run.clock = Math.min(command.to, run.clock + STEP_MS);
|
|
89
|
+
await settle(run);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await settle(run);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
result = { error: error instanceof Error ? error.message : String(error) };
|
|
96
|
+
}
|
|
97
|
+
return { ...base, ...result, at: run.clock };
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
/** A business run's command, or a message or reaction for one: true when it was one. */
|
|
101
|
+
handle(command, withFiles) {
|
|
102
|
+
// Steps run beside the stream, so one run's long wait never holds up another's commands; the cloud
|
|
103
|
+
// sends a run's next command only after this one's answer.
|
|
104
|
+
if (command.type === "open") {
|
|
105
|
+
ctx.watch((async () => {
|
|
106
|
+
const answer = await open(command).catch((error) => ({ conversationId: command.conversationId, step: 0, at: command.now, error: error instanceof Error ? error.message : String(error) }));
|
|
107
|
+
await ctx.post("steps", answer, `step:${command.conversationId}:0`, "step answer");
|
|
108
|
+
})(), "business step");
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (command.type === "close") {
|
|
112
|
+
runs.delete(command.conversationId);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (command.type === "event" || command.type === "nudge" || command.type === "advance") {
|
|
116
|
+
ctx.watch((async () => {
|
|
117
|
+
const run = runs.get(command.conversationId);
|
|
118
|
+
const answer = run ? await step(run, command) : { conversationId: command.conversationId, step: command.step, at: 0, error: "no such run open" };
|
|
119
|
+
await ctx.post("steps", answer, `step:${command.conversationId}:${command.step}`, "step answer");
|
|
120
|
+
})(), "business step");
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (command.type !== "message" && command.type !== "reaction")
|
|
124
|
+
return false;
|
|
125
|
+
const run = runs.get(command.conversationId);
|
|
126
|
+
if (!run)
|
|
127
|
+
return false;
|
|
128
|
+
if (command.type === "reaction") {
|
|
129
|
+
run.agent.reacted(run.conversationId, command.emoji, command.toText);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
// As a provider's webhook would: the person by their address on the run's channel.
|
|
133
|
+
const work = (async () => {
|
|
134
|
+
const taken = await run.agent.message({ channel: run.channelName, from: run.address, message: withFiles(command.incoming), messageId: command.messageId });
|
|
135
|
+
// An intercepted message's reply is out before its receipt, as on the other runs.
|
|
136
|
+
if (!taken)
|
|
137
|
+
await Promise.allSettled([...run.deliveries]);
|
|
138
|
+
const receipt = { conversationId: command.conversationId, messageId: command.messageId, status: taken ? "taken" : "intercepted" };
|
|
139
|
+
await ctx.post("receipts", receipt, `receipt:${receipt.conversationId}:${receipt.messageId}`, "delivery receipt");
|
|
140
|
+
})();
|
|
141
|
+
ctx.watch(work, "delivery receipt");
|
|
142
|
+
return true;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
package/dist/testing/src/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ 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
|
|
35
|
+
import { hasModelKey } from "../../src/model.js";
|
|
36
36
|
import { PLUGIN_LINE, pluginCli } from "./plugin.js";
|
|
37
37
|
import { OBSERVE_URL, setting } from "../../src/traces.js";
|
|
38
38
|
import { cloudHeaders, runInCloud, SessionStopped } from "./client.js";
|
|
@@ -122,10 +122,11 @@ const USAGE = `Usage:\n${NEW_LINE}\n${CHAT_LINE}\n${TEST_LINE}\n${SHOW_LINE}\n${
|
|
|
122
122
|
${PLUGIN_LINE}\n${ACCOUNT_LINES}\n flow --version Show the version\n\n${TEST_OPTIONS}`;
|
|
123
123
|
/** What to do after new or init, from step 2; a model key comes first when none is set here. */
|
|
124
124
|
function nextSteps() {
|
|
125
|
+
// link signs in and writes the keys first, FLOW_KEY (the model, on Flow's free trial credit) among
|
|
126
|
+
// them, so npm start comes after it and has the model key and the channels from its first start.
|
|
125
127
|
const steps = [
|
|
126
|
-
|
|
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`,
|
|
128
|
+
`npx @flow-engineer/flow link --channel telegram (or whatsapp, imessage): signs in, writes the keys\n into .env (FLOW_KEY pays for the model on Flow's free trial credit; ${hasModelKey(process.env) ? "your own model key in .env still wins" : "or put your own model key in .env"}) and prints the link for your phone`,
|
|
129
|
+
`npm start, and open the link it prints: try it in the playground as its test people, channels side by side;\n keep it running and text it from your phone`,
|
|
129
130
|
`Tell your coding agent what to build: its skills are in .claude/skills/`,
|
|
130
131
|
];
|
|
131
132
|
return steps.map((step, i) => ` ${i + 2}. ${step}`).join("\n");
|
|
@@ -203,7 +204,7 @@ async function askDisplayName(given, packageName) {
|
|
|
203
204
|
}
|
|
204
205
|
const namedLine = (dir) => {
|
|
205
206
|
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
|
+
return `People call it "${displayName}" (package.json displayName, and its prompt agent/AGENTS.md): a phone reaches it with "Hi ${displayName} (ref <code>)".`;
|
|
207
208
|
};
|
|
208
209
|
const displayNameFlag = values["display-name"];
|
|
209
210
|
if (command === "new") {
|
|
@@ -218,7 +219,7 @@ if (command === "new") {
|
|
|
218
219
|
console.error(error.message);
|
|
219
220
|
process.exit(1);
|
|
220
221
|
}
|
|
221
|
-
console.log(`Made ./${name}/ from the café example
|
|
222
|
+
console.log(`Made ./${name}/ from the café example. ${namedLine(dir)} Next:
|
|
222
223
|
1. cd ${name} && npm install
|
|
223
224
|
${nextSteps()}`);
|
|
224
225
|
process.exit(0);
|
|
@@ -235,7 +236,7 @@ if (command === "init") {
|
|
|
235
236
|
process.exit(1);
|
|
236
237
|
}
|
|
237
238
|
loadEnv(undefined); // a .env already here may hold a model key
|
|
238
|
-
console.log(`Made ${name}, started from the café example
|
|
239
|
+
console.log(`Made ${name}, started from the café example, in this folder. ${namedLine(process.cwd())} Next:
|
|
239
240
|
1. npm install
|
|
240
241
|
${nextSteps()}`);
|
|
241
242
|
process.exit(0);
|
|
@@ -303,7 +304,8 @@ else {
|
|
|
303
304
|
if (typeof module.buildAgent !== "function" || !Array.isArray(module.tools)) {
|
|
304
305
|
invalid("src/agent.ts must export buildAgent({ model, events, tools }) and tools, or pass --agent-url to test an agent by its HTTP endpoint.");
|
|
305
306
|
}
|
|
306
|
-
|
|
307
|
+
// A business run's agent also gets its channels and clock (testing/src/business.ts); buildAgent passes them on to createAgent.
|
|
308
|
+
agent = { tools: module.tools, build: ({ tools, events, channels, testing }) => module.buildAgent({ tools, events, ...(channels ? { channels, testing } : {}) }) };
|
|
307
309
|
}
|
|
308
310
|
/** With --agent-url the agent's own tools run for real: the suite's tool simulators do not apply. */
|
|
309
311
|
function forUrl(files) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Agent, type EventSink, type Tool } from "../../src/index.ts";
|
|
2
2
|
import type { Opened, RunSummary, SessionSummary } from "./protocol.ts";
|
|
3
3
|
import { type SuiteFile } from "./suite.ts";
|
|
4
|
+
import { type BusinessBuild } from "./business.ts";
|
|
4
5
|
export interface ConnectOptions {
|
|
5
6
|
/** The cloud's address, such as http://127.0.0.1:8900. */
|
|
6
7
|
cloud: string;
|
|
@@ -10,11 +11,12 @@ export interface ConnectOptions {
|
|
|
10
11
|
title?: string;
|
|
11
12
|
/** The agent's real tools: only their names, descriptions and inputs are used. */
|
|
12
13
|
tools: Tool[];
|
|
13
|
-
/** Builds the agent with the tools and events it is given: a Flow Framework Agent, or anything that answers like one (http-agent.ts).
|
|
14
|
+
/** Builds the agent with the tools and events it is given: a Flow Framework Agent, or anything that answers like one (http-agent.ts).
|
|
15
|
+
* A business run (protocol.ts) builds one more per run, with its own channels and clock (`BusinessBuild`); that needs a Flow Framework Agent. */
|
|
14
16
|
build: (options: {
|
|
15
17
|
tools: Tool[];
|
|
16
18
|
events: EventSink[];
|
|
17
|
-
}) => AgentUnderTest;
|
|
19
|
+
} & Partial<BusinessBuild>) => AgentUnderTest;
|
|
18
20
|
files: SuiteFile[];
|
|
19
21
|
/** The tests folder: files a scenario attaches are read from here, and never leave this machine. */
|
|
20
22
|
testsDir?: string;
|
|
@@ -10,6 +10,7 @@ import { toSpec } from "../../src/index.js";
|
|
|
10
10
|
import { PROFILE_TOOL, profileTool } from "../../src/conversation.js";
|
|
11
11
|
import { STAND_IN } from "../../src/tools.js";
|
|
12
12
|
import { mimeOf, parseScenario, parseToolFile, suiteHash } from "./suite.js";
|
|
13
|
+
import { businessRuns } from "./business.js";
|
|
13
14
|
/** The session opened, then the cloud ended it with a `failed` command. `code` is the reason's leading
|
|
14
15
|
* code (`capacity: ...` gives capacity), when it starts with one. */
|
|
15
16
|
export class CloudEnded extends Error {
|
|
@@ -351,6 +352,8 @@ async function playSession(options, only) {
|
|
|
351
352
|
events.context = true;
|
|
352
353
|
try {
|
|
353
354
|
const agent = options.build({ tools, events: [events] });
|
|
355
|
+
// Business runs need an agent of this framework: its own channels, routes, clock and nudges.
|
|
356
|
+
const canBusiness = typeof agent.reach === "function" && typeof agent.fetch === "function";
|
|
354
357
|
const open = {
|
|
355
358
|
agent: { name: options.name, title: options.title, tools: [...options.tools, ...(agent.hasProfile?.() ? standIn : [])].map(toSpec), channels: agent.channels() },
|
|
356
359
|
files: options.files.map(({ path, hash }) => ({ path, hash })),
|
|
@@ -359,6 +362,7 @@ async function playSession(options, only) {
|
|
|
359
362
|
retries: true,
|
|
360
363
|
resume: true,
|
|
361
364
|
reopen: true,
|
|
365
|
+
...(canBusiness ? { business: true } : {}),
|
|
362
366
|
...(only ? { only } : {}),
|
|
363
367
|
};
|
|
364
368
|
const opened = await openOnce(`${base}/api/sessions`, open, options.openHeaders, controller.signal, postTimeoutMs, openRetryCapMs, log);
|
|
@@ -382,6 +386,20 @@ async function playSession(options, only) {
|
|
|
382
386
|
options.onDashboard?.(dashboard, opened.sessionId, typeof opened.agent === "string" && opened.agent ? opened.agent : undefined);
|
|
383
387
|
log(`Watch it: ${opened.dashboard}`);
|
|
384
388
|
session = `${tunnel}/api/sessions/${opened.sessionId}`;
|
|
389
|
+
const business = canBusiness && opened.business === true
|
|
390
|
+
? businessRuns({
|
|
391
|
+
sessionAgent: agent,
|
|
392
|
+
build: (built) => options.build(built),
|
|
393
|
+
tunnelTools: tools,
|
|
394
|
+
realTools: options.tools,
|
|
395
|
+
events,
|
|
396
|
+
post: (path, body, key, what) => postRetry(`${session}/${path}`, body, auth, controller.signal, log, postTimeoutMs, {
|
|
397
|
+
mode: idempotent ? "idempotent" : path === "outbound" ? "outbound" : "never", key, budgetMs: retryBudgetMs, what,
|
|
398
|
+
}),
|
|
399
|
+
watch,
|
|
400
|
+
log,
|
|
401
|
+
})
|
|
402
|
+
: null;
|
|
385
403
|
if (failure)
|
|
386
404
|
throw failure;
|
|
387
405
|
startFlush();
|
|
@@ -447,6 +465,8 @@ async function playSession(options, only) {
|
|
|
447
465
|
const handle = async (command) => {
|
|
448
466
|
if (draining && (command.type === "message" || command.type === "reaction"))
|
|
449
467
|
throw new Error("the cloud sent a command after drain");
|
|
468
|
+
if (business && (business.handle(command, (incoming) => withFiles(incoming, options.testsDir, log))))
|
|
469
|
+
return;
|
|
450
470
|
if (command.type === "message") {
|
|
451
471
|
const deliveries = [];
|
|
452
472
|
// Not async: register the promise during receive(), before its fire-and-forget
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { runInCloud, type CloudSummary, type ConnectOptions } from "./client.ts";
|
|
2
|
-
export { hashOf, loadSuite, parseScenario, parseToolFile, suiteHash, type Scenario, type SuiteFile, type ToolFile } from "./suite.ts";
|
|
3
|
-
export type { Check, Command, OpenSession, Opened, RunKey, RunSummary, SessionSummary, ToolCallRequest, ToolCallResult, UploadFile, Usage, WireIncoming } from "./protocol.ts";
|
|
2
|
+
export { hashOf, isBusiness, loadSuite, parseScenario, parseToolFile, suiteHash, waitMs, type OpeningStep, type Scenario, type SentItem, type SuiteFile, type ToolFile } from "./suite.ts";
|
|
3
|
+
export type { BusinessCommand, BusinessNudge, BusinessOutbound, Check, Command, FlowEventEnvelope, StepAnswer, OpenSession, Opened, RunKey, RunSummary, SessionSummary, ToolCallRequest, ToolCallResult, UploadFile, Usage, WireIncoming } from "./protocol.ts";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { runInCloud } from "./client.js";
|
|
2
|
-
export { hashOf, loadSuite, parseScenario, parseToolFile, suiteHash } from "./suite.js";
|
|
2
|
+
export { hashOf, isBusiness, loadSuite, parseScenario, parseToolFile, suiteHash, waitMs } from "./suite.js";
|
|
@@ -6,6 +6,15 @@ export declare function packageRoot(from?: string): string;
|
|
|
6
6
|
export declare const MAX_DISPLAY_NAME = 60;
|
|
7
7
|
/** The name people call the agent, from the package name when none was given: "my-shop" is "my shop". */
|
|
8
8
|
export declare function displayNameOf(given: string | undefined, packageName: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Renames the agent in `dir`: package.json's displayName, and in its prompt the old display name
|
|
11
|
+
* (and the café's Pip) become the new one. Returns the name and whether the prompt changed (a prompt
|
|
12
|
+
* rewritten by hand may name neither).
|
|
13
|
+
*/
|
|
14
|
+
export declare function setDisplayName(dir: string, given: string): {
|
|
15
|
+
displayName: string;
|
|
16
|
+
prompt: boolean;
|
|
17
|
+
};
|
|
9
18
|
interface MakeOptions {
|
|
10
19
|
root?: string;
|
|
11
20
|
/** What people call the agent; the package name in words when not given. */
|