@mercury-fw/cli 0.47.0 → 0.48.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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @mercury-fw/cli
2
2
 
3
+ ## 0.48.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 62bb3a3: - `persona.name` is the assistant's name, Mercury when left out. A name that's blank, longer than 40 characters or on more than one line stops the app at startup.
8
+ - Every text the model reads refers to the name as `{{assistant}}`, and the core puts the persona's name there: the persona's identity and tone, the system prompt, plugin fragments, skills and tool descriptions. The model no longer takes "Mercury" for its own name when the app calls its assistant something else.
9
+ - Where the one acting is the system around the model, the texts say "the system" or "this instance" instead of Mercury: the wiki tools, the cli-engine's errors, the jira skill and its issue lists.
10
+ - Plugins get `assistantName` in their `build()` context, and channels in their runtime context, for texts they build while running. The login messages, the linking reply and the HTTP login page name the assistant that way.
11
+ - The tools a person runs as the assistant itself are `<tool>AsSelf`, previously `<tool>AsMercury`. The `mercury.act-as-self` roles are unchanged.
12
+ - `mfw create` writes the assistant's name as `persona.name`, with `persona/identity.md` and `persona/tone.md` referring to it. An existing app keeps working as it is: to rename its assistant everywhere, add `name` to its persona and write `{{assistant}}` in its persona files.
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [62bb3a3]
17
+ - @mercury-fw/core@0.48.0
18
+ - @mercury-fw/utils@0.48.0
19
+
20
+ ## 0.47.1
21
+
22
+ ### Patch Changes
23
+
24
+ - e005ca4: - `mfw e2e`: a REPL that exits before its stderr has been read no longer reports "exited with code N:" without the reason.
25
+ - @mercury-fw/core@0.47.1
26
+ - @mercury-fw/utils@0.47.1
27
+
3
28
  ## 0.47.0
4
29
 
5
30
  ### Patch Changes
package/README.md CHANGED
@@ -47,7 +47,7 @@ Without a global install: `bun create mercury-agent my-agent` creates an app, an
47
47
  Writes a new Mercury app into `<folder>`, which has to be missing or empty; the folder's own name is turned into kebab case (`My Agent` becomes `my-agent`, the path above it stays as typed). Without flags it asks:
48
48
 
49
49
  - the app name (the `name` in `package.json`, defaulting to the folder's);
50
- - the assistant's name and role, which become `persona/identity.md` ("You are Hermes, the platform team's release assistant.") next to a `persona/tone.md` to edit;
50
+ - the assistant's name and role: the name becomes `persona.name` in the config, the role `persona/identity.md` ("You are {{assistant}}, the platform team's release assistant.", the core putting the name in place of `{{assistant}}`), next to a `persona/tone.md` to edit;
51
51
  - which channels and which tool plugins to include (none is fine: the REPL always works);
52
52
  - the git remote for `origin`, which you can leave empty (most of the time it doesn't exist yet when you scaffold).
53
53
 
@@ -10,6 +10,20 @@ export type ReplSessionOptions = {
10
10
  name: string;
11
11
  /** How long one turn may take. */
12
12
  timeoutMs: number;
13
+ /** Starts the REPL; a test hands in a stand-in process. */
14
+ spawn?: (argv: string[], cwd: string) => ReplProcess;
15
+ };
16
+ /** The parts of a spawned process the session uses. */
17
+ export type ReplProcess = {
18
+ stdin: {
19
+ write: (text: string) => unknown;
20
+ flush: () => unknown;
21
+ end: () => unknown;
22
+ };
23
+ stdout: ReadableStream<Uint8Array>;
24
+ stderr: ReadableStream<Uint8Array>;
25
+ exited: Promise<number>;
26
+ kill: () => void;
13
27
  };
14
28
  /** Starts the REPL and returns the session over it once the REPL is ready
15
29
  * (its first prompt is out), so a turn's time is only the turn's; throws
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/cli",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,20 +41,20 @@
41
41
  "comment:shape": "The Mercury CLI (`mfw`). `mfw create <dir>` writes a new Mercury app from the template. The framework packages move in lockstep with it, so a new app gets them at the CLI's own version; plugins and channels at the registry's latest. The plugin and channel packages are devDependencies only for the catalog test. `main(argv)` is exported for create-mercury-agent.",
42
42
  "dependencies": {
43
43
  "@clack/prompts": "^1.8.1",
44
- "@mercury-fw/core": "0.47.0",
45
- "@mercury-fw/utils": "0.47.0",
44
+ "@mercury-fw/core": "0.48.0",
45
+ "@mercury-fw/utils": "0.48.0",
46
46
  "commander": "^15.0.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@mercury-fw/auth-oidc": "0.2.0",
50
50
  "@mercury-fw/auth-static": "0.2.1",
51
51
  "@mercury-fw/channel-google-chat": "0.7.0",
52
- "@mercury-fw/channel-http": "0.8.3",
52
+ "@mercury-fw/channel-http": "0.8.4",
53
53
  "@mercury-fw/directory-static": "0.1.1",
54
- "@mercury-fw/formatter": "0.47.0",
54
+ "@mercury-fw/formatter": "0.48.0",
55
55
  "@mercury-fw/plugin-atlassian-admin": "0.3.2",
56
56
  "@mercury-fw/plugin-bitbucket": "0.4.1",
57
- "@mercury-fw/plugin-jira": "0.6.2",
57
+ "@mercury-fw/plugin-jira": "0.6.3",
58
58
  "@mercury-fw/plugin-zitadel": "0.3.2",
59
59
  "@mercury-fw/typescript-config": "*",
60
60
  "@types/bun": "^1.4.2",
@@ -22,13 +22,31 @@ export type ReplSessionOptions = {
22
22
  name: string;
23
23
  /** How long one turn may take. */
24
24
  timeoutMs: number;
25
+ /** Starts the REPL; a test hands in a stand-in process. */
26
+ spawn?: (argv: string[], cwd: string) => ReplProcess;
25
27
  };
26
28
 
29
+ /** The parts of a spawned process the session uses. */
30
+ export type ReplProcess = {
31
+ stdin: { write: (text: string) => unknown; flush: () => unknown; end: () => unknown };
32
+ stdout: ReadableStream<Uint8Array>;
33
+ stderr: ReadableStream<Uint8Array>;
34
+ exited: Promise<number>;
35
+ kill: () => void;
36
+ };
37
+
38
+ /** How long, once the REPL is gone, its stderr may take to be read to its end. */
39
+ const STDERR_DRAIN_MS = 1000;
40
+
41
+ function spawnRepl(argv: string[], cwd: string): ReplProcess {
42
+ return Bun.spawn(argv, { cwd, stdin: "pipe", stdout: "pipe", stderr: "pipe" });
43
+ }
44
+
27
45
  /** Starts the REPL and returns the session over it once the REPL is ready
28
46
  * (its first prompt is out), so a turn's time is only the turn's; throws
29
47
  * when the REPL exits or doesn't get there in time. */
30
48
  export async function openReplSession(opts: ReplSessionOptions): Promise<Session<ReplReply>> {
31
- const proc = Bun.spawn(opts.argv, { cwd: opts.cwd, stdin: "pipe", stdout: "pipe", stderr: "pipe" });
49
+ const proc = (opts.spawn ?? spawnRepl)(opts.argv, opts.cwd);
32
50
  let stdout = "";
33
51
  let stderr = "";
34
52
  let exited: number | undefined;
@@ -42,8 +60,11 @@ export async function openReplSession(opts: ReplSessionOptions): Promise<Session
42
60
  }
43
61
  };
44
62
  void pump(proc.stdout, (t) => (stdout += t));
45
- void pump(proc.stderr, (t) => (stderr += t));
46
- void proc.exited.then((code) => {
63
+ const stderrRead = pump(proc.stderr, (t) => (stderr += t)).catch(() => {});
64
+ // The exit counts once stderr is read to its end, or the reason it gives is
65
+ // lost; a grandchild still holding the pipe open bounds the wait.
66
+ void proc.exited.then(async (code) => {
67
+ await Promise.race([stderrRead, new Promise((r) => setTimeout(r, STDERR_DRAIN_MS))]);
47
68
  exited = code;
48
69
  notify();
49
70
  });
package/src/program.ts CHANGED
@@ -52,7 +52,7 @@ function buildProgram(handlers: ProgramHandlers, result: { code: number }): Comm
52
52
  .command("create")
53
53
  .summary("writes a new Mercury app")
54
54
  .description(
55
- "Writes a new Mercury app into <folder>, which has to be missing or empty (its own name is turned into kebab case). Without options it asks for the app name, the assistant's name and role, and which channels (with the HTTP channel's auth provider) and tool plugins to include; then it writes mercury.config.ts for that selection, the persona (persona/identity.md, persona/tone.md), the service and REPL entrypoints, a Dockerfile, a compose file with Qdrant, and an env example listing every variable the app reads. Then it runs bun install, and creates a git repository on main with a first commit, adding the origin when given (nothing is pushed).",
55
+ "Writes a new Mercury app into <folder>, which has to be missing or empty (its own name is turned into kebab case). Without options it asks for the app name, the assistant's name and role, and which channels (with the HTTP channel's auth provider) and tool plugins to include; then it writes mercury.config.ts for that selection, with the assistant's name as persona.name, the persona (persona/identity.md, persona/tone.md, which refer to the name as {{assistant}}), the service and REPL entrypoints, a Dockerfile, a compose file with Qdrant, and an env example listing every variable the app reads. Then it runs bun install, and creates a git repository on main with a first commit, adding the origin when given (nothing is pushed).",
56
56
  )
57
57
  .argument("<folder>", "where to write the app")
58
58
  .option("--name <name>", "app name, as in package.json (default: the folder's name)")
package/src/render.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * written in catalog order, whatever order they were chosen in; the auth
9
9
  * provider, when there is one, after the channels.
10
10
  */
11
- import { DEFAULT_PERSONA_TONE } from "@mercury-fw/core";
11
+ import { DEFAULT_PERSONA_IDENTITY, DEFAULT_PERSONA_TONE, assistantNameOf } from "@mercury-fw/core";
12
12
  import { CATALOG, type CatalogEntry, type EnvVar } from "./catalog.ts";
13
13
  import indexTs from "../template/src/index.ts.tpl" with { type: "text" };
14
14
  import replTs from "../template/src/repl.ts.tpl" with { type: "text" };
@@ -79,12 +79,11 @@ export function renderApp(input: RenderInput): Map<string, string> {
79
79
  ["README.md", renderReadme(input.name, channels, auth, tools, directory)],
80
80
  ["docker-compose.yml", renderCompose(input.name, channels.some((c) => c.id === "http"))],
81
81
  ["markdown.d.ts", markdownDts],
82
- ["mercury.config.ts", renderConfig(channels, auth, tools, directory)],
82
+ ["mercury.config.ts", renderConfig(assistantName, channels, auth, tools, directory)],
83
83
  ["package.json", renderPackageJson(input.name, channels, auth, tools, input.versions, directory)],
84
- ["persona/identity.md", `You are ${assistantName}, ${input.role.trim().replace(/\.+$/, "")}.\n`],
85
- // A replacer function, not a string: in a replacement string "$&" and the
86
- // like are patterns, and the name must land verbatim.
87
- ["persona/tone.md", `${DEFAULT_PERSONA_TONE.replaceAll("Mercury", () => assistantName)}\n`],
84
+ // The name is persona.name in the config; the persona files refer to it.
85
+ ["persona/identity.md", `You are {{assistant}}, ${input.role.trim().replace(/\.+$/, "")}.\n`],
86
+ ["persona/tone.md", `${DEFAULT_PERSONA_TONE}\n`],
88
87
  ["src/index.ts", indexTs],
89
88
  ["src/repl.ts", replTs],
90
89
  ["tsconfig.json", tsconfigJson],
@@ -115,6 +114,8 @@ function validate(input: RenderInput): void {
115
114
  if (/[\r\n]/.test(input.assistantName) || /[\r\n]/.test(input.role)) {
116
115
  throw new Error("The assistant name and role must each be one line");
117
116
  }
117
+ // What the app would refuse at startup.
118
+ assistantNameOf({ name: input.assistantName });
118
119
  const selection =
119
120
  selectionError(input.channels, input.plugins, input.auth, input.directory) ?? pairingError(input.channels, input.auth);
120
121
  if (selection !== undefined) {
@@ -164,6 +165,7 @@ function selected(kind: CatalogEntry["kind"], ids: string[]): CatalogEntry[] {
164
165
  * any, then the config with each plugin (wrapped in the formatter when it has
165
166
  * starting rules) and channel. */
166
167
  function renderConfig(
168
+ assistantName: string,
167
169
  channels: CatalogEntry[],
168
170
  auth: CatalogEntry | undefined,
169
171
  tools: CatalogEntry[],
@@ -197,7 +199,7 @@ function renderConfig(
197
199
  ...imports,
198
200
  "",
199
201
  `${helpers}export default defineMercuryConfig({`,
200
- " persona: { identity, tone },",
202
+ ` persona: { name: ${JSON.stringify(assistantName)}, identity, tone },`,
201
203
  plugins,
202
204
  channelList,
203
205
  ...(auth ? [` auth: ${auth.exportName},`] : []),