@mercury-fw/cli 0.53.2 → 0.55.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,46 @@
1
1
  # @mercury-fw/cli
2
2
 
3
+ ## 0.55.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [eac72fa]
8
+ - @mercury-fw/core@0.55.0
9
+ - @mercury-fw/utils@0.55.0
10
+
11
+ ## 0.54.0
12
+
13
+ ### Minor Changes
14
+
15
+ - ebde9e2: The texts a person sees can be in their language. The core's own fixed texts (the refusal of someone the agent doesn't know, the account-linking replies, the login errors, the tool status lines, the terminal's own lines) are English, each under a key (`core.access.unknown`, …), and an app adds a language with a file of its own: `i18n: { defaultLocale, locales: { it } }` in `mercury.config.ts`, each file mapping keys to texts. A person picks their language by asking the agent, which saves it with `set_preferred_language` (only the languages the app ships; not offered when it ships English alone) and answers in it from then on. Whoever hasn't picked one gets `defaultLocale`, then English. A key a language's file lacks shows as the key itself, and the log names the missing keys at startup. `GET /me` on the HTTP channel also says the caller's language.
16
+
17
+ The texts that were in Italian (tool status lines, the terminal's lines) are now English by default, and the language is no longer one of the facts inferred from conversations.
18
+
19
+ An existing app keeps working, with these changes:
20
+
21
+ 1. `access.unknownMessage` is gone. To keep a custom refusal, put it in `locales/en.json` as `{ "core.access.unknown": "…" }`, and in `mercury.config.ts` import it (`import en from "./locales/en.json";`) and set `i18n: { locales: { en } }`.
22
+ 2. To keep the status and terminal lines in Italian, add `locales/it.json` with the `core.` keys translated (start it as `{}`: the log lists the missing keys at startup), and in `mercury.config.ts` import it (`import it from "./locales/it.json";`) and set `i18n: { defaultLocale: "it", locales: { it } }` (`locales: { en, it }` when you did step 1 too).
23
+ 3. Whenever the config imports a file from `locales/` (steps 1 and 2), the image needs the folder: add `COPY --chown=mercury:mercury locales ./locales` after the `mercury.config.ts` line of the `Dockerfile`, or the app fails at startup.
24
+ 4. For the terminal (`mfw repl`) to follow the language too, add `texts: app.texts,` to `createTerminalProvider({ … })` in `src/repl.ts`; without it the terminal's own lines stay English.
25
+
26
+ - 5015642: The plugins' texts a person sees come out in their language, like the core's. Plugin contract version 5:
27
+
28
+ - A plugin declares its texts in English in `messages`, every key under its own name (`web-search.choice.once`); the core refuses a plugin with a key outside it (and a plugin named `core` or `app`, whose keys are the core's and the app's), and the app's language files translate them. The startup log names the missing keys of the plugins too.
29
+ - While a turn runs, a plugin gets the texts of its person as `t`: in `SessionToolContext.t`, as the second argument of a status describer (`toolStatusDescribers`, `StatusDescriber`), the third of a post-processor (`CliPostProcessor`), and in a post-turn guard, whose `statusLabel` is now `(t) => string` and whose `run` gets `t` as its third argument. `Messages`, `Params` and `Translate` are part of the contract.
30
+ - The default status line of a CLI command is the core's `core.status.command` ("Running jira issue search"), `core.status.command-bare` without a subcommand ("Running jira"), and an unreadable command's is `core.status.command-unknown`. `createCliTool` (`@mercury-fw/cli-engine`) takes the turn's `t` among its options, handed to the post-processor: a CLI plugin passes `sctx.t`; `createCliStatusDescriber`'s describer takes it as its second argument.
31
+ - The formatter hands a rule `t` as its second argument, and an `empty` text can be `(t) => string`; existing rules keep working as they are. An app's own texts go under `app.`, in its `locales/en.json`, which declares them (the startup log no longer lists them as unknown); `mfw create` writes the Jira rule's empty text as `app.jira.no-issues`.
32
+ - Web search: the two choices ("Only this time", "Always for \<host\>"), the status lines and the sources heading ("Sources") are `web-search.*` texts, English by default; a list of sources the model writes under the heading in the person's language, or the English one, is still replaced by the plugin's.
33
+
34
+ The first-party plugins released with this change are written for contract 5 and need the core released with it: an older core skips them and says why in the log. A plugin written for contract 4 isn't loaded by this core either, and the log says why: rebuild it against `PLUGIN_API_VERSION` 5 (a status describer, a guard's `statusLabel` and `run`, a post-processor take the new `t`). An app that wants the web search and status lines back in Italian adds their keys to `locales/it.json` (the log lists the missing ones at startup).
35
+
36
+ ### Patch Changes
37
+
38
+ - Updated dependencies [e00ff73]
39
+ - Updated dependencies [ebde9e2]
40
+ - Updated dependencies [5015642]
41
+ - @mercury-fw/core@0.54.0
42
+ - @mercury-fw/utils@0.54.0
43
+
3
44
  ## 0.53.2
4
45
 
5
46
  ### Patch Changes
package/README.md CHANGED
@@ -51,7 +51,7 @@ Writes a new Mercury app into `<folder>`, which has to be missing or empty; the
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
 
54
- It writes the `mercury.config.ts` for that selection, the persona, the service and REPL entrypoints, a Dockerfile, a compose file with Qdrant, and an env example listing every variable the chosen pieces read. A plugin that hands over lists comes wrapped in the formatter with a starting rule, yours to change.
54
+ It writes the `mercury.config.ts` for that selection, the persona, a `locales/` folder with an `en.json` for the texts people see (empty, or the starting formatter rules' own texts: `app.jira.no-issues` with Jira), the service and REPL entrypoints, a Dockerfile, a compose file with Qdrant, and an env example listing every variable the chosen pieces read. A plugin that hands over lists comes wrapped in the formatter with a starting rule, yours to change.
55
55
 
56
56
  Then it runs `bun install` in the app and creates a git repository on `main` with a first commit (`bun.lock` included), adding `origin` when you gave one; nothing is pushed. Each step is best effort and never undoes the ones before it: an install that fails still leaves the app committed, without the lockfile, and the closing message says what to run. The repository is skipped when the folder is already inside one (an app created in a monorepo belongs to it). Whether git can commit is git's call: when it refuses (no identity, a hook) or isn't installed, the message reports why and lists the commands to finish by hand. `bun install` prints its own output as it runs.
57
57
 
@@ -341,7 +341,7 @@ export default e2e({
341
341
  "Delete the issue SUP-1 on Jira",
342
342
  { text: (previous) => (previous.calls.find((c) => c.pending)?.output as { token: string }).token, as: "bob" },
343
343
  ],
344
- check: (run, expect) => expect.answerNot("Confermato"),
344
+ check: (run, expect) => expect.answerNot("Confirmed"),
345
345
  },
346
346
  ],
347
347
  });
@@ -49,11 +49,14 @@ export type CatalogEntry = {
49
49
  * rule a kind of list isn't shown, so the scaffolded config wraps the plugin
50
50
  * with these. `displaysType` is the type the plugin exports for its kinds,
51
51
  * `helpers` the code the rules use (written above the config), `rules` the
52
- * `kind: rule` lines. Once written, they're the app's to change. */
52
+ * `kind: rule` lines, `messages` the English texts they show the person
53
+ * (`app.` keys, written into the app's `locales/en.json`). Once written,
54
+ * they're the app's to change. */
53
55
  export type FormatterExample = {
54
56
  displaysType: string;
55
57
  helpers: string;
56
58
  rules: string[];
59
+ messages?: Record<string, string>;
57
60
  };
58
61
  export declare const CATALOG: CatalogEntry[];
59
62
  /** The catalog entry of `kind` with `id`, or undefined when there is none. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/cli",
3
- "version": "0.53.2",
3
+ "version": "0.55.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,22 +41,22 @@
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.53.2",
45
- "@mercury-fw/utils": "0.53.2",
44
+ "@mercury-fw/core": "0.55.0",
45
+ "@mercury-fw/utils": "0.55.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
- "@mercury-fw/channel-google-chat": "0.8.2",
52
- "@mercury-fw/channel-http": "0.10.1",
51
+ "@mercury-fw/channel-google-chat": "0.9.0",
52
+ "@mercury-fw/channel-http": "0.11.0",
53
53
  "@mercury-fw/directory-static": "0.1.1",
54
- "@mercury-fw/formatter": "0.53.2",
55
- "@mercury-fw/plugin-atlassian-admin": "0.3.2",
56
- "@mercury-fw/plugin-bitbucket": "0.4.1",
57
- "@mercury-fw/plugin-jira": "0.6.5",
58
- "@mercury-fw/plugin-web-search": "0.1.2",
59
- "@mercury-fw/plugin-zitadel": "0.3.2",
54
+ "@mercury-fw/formatter": "0.55.0",
55
+ "@mercury-fw/plugin-atlassian-admin": "0.5.0",
56
+ "@mercury-fw/plugin-bitbucket": "0.6.0",
57
+ "@mercury-fw/plugin-jira": "0.8.0",
58
+ "@mercury-fw/plugin-web-search": "0.3.0",
59
+ "@mercury-fw/plugin-zitadel": "0.5.0",
60
60
  "@mercury-fw/typescript-config": "*",
61
61
  "@mercury-fw/web-search-searxng": "0.1.0",
62
62
  "@types/bun": "^1.4.2",
package/src/catalog.ts CHANGED
@@ -55,8 +55,10 @@ search:
55
55
  * rule a kind of list isn't shown, so the scaffolded config wraps the plugin
56
56
  * with these. `displaysType` is the type the plugin exports for its kinds,
57
57
  * `helpers` the code the rules use (written above the config), `rules` the
58
- * `kind: rule` lines. Once written, they're the app's to change. */
59
- export type FormatterExample = { displaysType: string; helpers: string; rules: string[] };
58
+ * `kind: rule` lines, `messages` the English texts they show the person
59
+ * (`app.` keys, written into the app's `locales/en.json`). Once written,
60
+ * they're the app's to change. */
61
+ export type FormatterExample = { displaysType: string; helpers: string; rules: string[]; messages?: Record<string, string> };
60
62
 
61
63
  export const CATALOG: CatalogEntry[] = [
62
64
  {
@@ -125,7 +127,8 @@ export const CATALOG: CatalogEntry[] = [
125
127
  'const jiraIssueLine = (issue: JiraDisplays["issue-list"]) =>',
126
128
  ' `${issue.key} ${issue.status ? `[${issue.status}] ` : ""}${issue.summary}\\n${issue.url}`;',
127
129
  ].join("\n"),
128
- rules: ['"issue-list": { item: jiraIssueLine, empty: "No matching issues." },'],
130
+ rules: ['"issue-list": { item: jiraIssueLine, empty: (t) => t("app.jira.no-issues") },'],
131
+ messages: { "app.jira.no-issues": "No matching issues." },
129
132
  },
130
133
  },
131
134
  {
package/src/render.ts CHANGED
@@ -58,7 +58,8 @@ const CORE_ENV: EnvVar[] = [
58
58
  const CONFIG_HEADER = `/**
59
59
  * This app's composition: the tool plugins and channels it runs, the auth
60
60
  * provider when a channel needs one, how the lists its plugins hand over read,
61
- * and the assistant's persona. The entrypoints
61
+ * the assistant's persona and the languages it speaks (\`i18n\`: a file per
62
+ * language in \`locales/\`, \`en.json\` overriding the English texts). The entrypoints
62
63
  * (\`src/index.ts\`, \`src/repl.ts\`) hand this config to \`composeMercury\`.
63
64
  *
64
65
  * Every tool plugin and channel declared here is active.
@@ -85,6 +86,7 @@ export function renderApp(input: RenderInput): Map<string, string> {
85
86
  ["Dockerfile", dockerfile],
86
87
  ["README.md", renderReadme(input.name, channels, auth, tools, directory)],
87
88
  ["docker-compose.yml", renderCompose(input.name, channels.some((c) => c.id === "http"), services)],
89
+ ["locales/en.json", renderEnglish(tools)],
88
90
  ["mercury.config.ts", renderConfig(assistantName, input.role, channels, auth, tools, directory)],
89
91
  ["package.json", renderPackageJson(input.name, channels, auth, tools, input.versions, directory)],
90
92
  ["src/index.ts", indexTs],
@@ -193,6 +195,13 @@ function renderPersona(role: string): string[] {
193
195
  ];
194
196
  }
195
197
 
198
+ /** `locales/en.json`: the English texts of the chosen plugins' starting
199
+ * formatter rules (the app's own `app.` keys), `{}` when there are none. */
200
+ function renderEnglish(tools: CatalogEntry[]): string {
201
+ const messages = Object.assign({}, ...tools.map((t) => t.formatter?.messages ?? {})) as Record<string, string>;
202
+ return Object.keys(messages).length === 0 ? "{}\n" : `${JSON.stringify(messages, null, 2)}\n`;
203
+ }
204
+
196
205
  /** `mercury.config.ts`: imports, the assistant's name, the formatter helpers of
197
206
  * the plugins that have any, then the config with the persona, each plugin
198
207
  * (wrapped in the formatter when it has starting rules) and channel. */
@@ -205,7 +214,7 @@ function renderConfig(
205
214
  directory: CatalogEntry | undefined,
206
215
  ): string {
207
216
  const withRules = tools.filter((t) => t.formatter);
208
- const imports = ['import { defineMercuryConfig } from "@mercury-fw/core";'];
217
+ const imports = ['import { defineMercuryConfig } from "@mercury-fw/core";', 'import en from "./locales/en.json";'];
209
218
  if (withRules.length > 0) {
210
219
  imports.push('import { formatterPlugin, formatter } from "@mercury-fw/formatter";');
211
220
  }
@@ -239,6 +248,7 @@ function renderConfig(
239
248
  channelList,
240
249
  ...(auth ? [` auth: ${auth.exportName},`] : []),
241
250
  ...(directory ? [` directory: ${directory.exportName},`] : []),
251
+ " i18n: { locales: { en } },",
242
252
  "});",
243
253
  "",
244
254
  ].join("\n");
@@ -406,6 +416,7 @@ ${directory ? `- Directory: ${directory.id}\n` : ""}
406
416
  ## Layout
407
417
 
408
418
  - \`mercury.config.ts\`: what the app is made of (tool plugins, channels, how their lists read) and the assistant's persona: its name, who it is and how it answers. Edit them freely.
419
+ - \`locales/\`: the texts people see, one file per language the assistant speaks. \`en.json\` overrides the English ones; add \`it.json\` (and \`it\` to \`i18n.locales\` in the config) for Italian, \`i18n.defaultLocale\` for the language people get when they haven't picked one. The app's own texts (a formatter rule's) are \`app.\` keys, written in English in \`en.json\` and translated in every other file. Mercury logs at startup the keys a file is missing.
409
420
  - \`src/index.ts\`: the service (channels, crons). \`src/repl.ts\`: an interactive terminal for trying things out.
410
421
  - \`.env.example\`: every variable the app reads. Copy it to \`.env\` and fill it in.
411
422
 
@@ -21,6 +21,7 @@ RUN bun install --production && chown -R mercury:mercury node_modules
21
21
  RUN find /app/node_modules -path '*/@mercury-fw/*/bin/*' -type f -exec ln -sf {} /usr/local/bin/ \;
22
22
 
23
23
  COPY --chown=mercury:mercury mercury.config.ts ./
24
+ COPY --chown=mercury:mercury locales ./locales
24
25
  COPY --chown=mercury:mercury src ./src
25
26
 
26
27
  # Mount points of the named volumes, owned by the runtime user before a fresh
@@ -13,6 +13,7 @@ await createTerminalProvider({
13
13
  confirmDeps: app.confirmDeps,
14
14
  ollamaHost: app.ollamaHost,
15
15
  ollamaModel: app.ollamaModel,
16
+ texts: app.texts,
16
17
  }).start(app.handleTurn);
17
18
 
18
19
  // The REPL ended (Ctrl+D); the model and Qdrant clients keep pooled sockets