@mercury-fw/cli 0.52.0 → 0.53.1

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,26 @@
1
1
  # @mercury-fw/cli
2
2
 
3
+ ## 0.53.1
4
+
5
+ ### Patch Changes
6
+
7
+ - @mercury-fw/core@0.53.1
8
+ - @mercury-fw/utils@0.53.1
9
+
10
+ ## 0.53.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 79a232e: - New package `@mercury-fw/web-search-searxng`: a SearXNG instance as the web search plugin's backend, through its JSON format.
15
+ - `mfw create --plugins web-search` adds the web search plugin with the SearXNG backend, a `searxng` service in the compose file (on the compose network only), its `searxng/settings.yml` with the JSON format on, and `SEARXNG_URL` and `SEARXNG_SECRET` in the env example.
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies [56e4ed5]
20
+ - Updated dependencies [c83b129]
21
+ - @mercury-fw/core@0.53.0
22
+ - @mercury-fw/utils@0.53.0
23
+
3
24
  ## 0.52.0
4
25
 
5
26
  ### Minor Changes
package/README.md CHANGED
@@ -61,7 +61,7 @@ Then it runs `bun install` in the app and creates a git repository on `main` wit
61
61
  | `--assistant-name <name>` | The assistant's name (default `Mercury`). |
62
62
  | `--role <text>` | Completes "You are <name>, …" (default `an internal assistant`). |
63
63
  | `--channels <ids>` | Comma-separated: `google-chat`, `http`. |
64
- | `--plugins <ids>` | Comma-separated: `jira`, `bitbucket`, `atlassian-admin`, `zitadel`. |
64
+ | `--plugins <ids>` | Comma-separated: `jira`, `bitbucket`, `atlassian-admin`, `zitadel`, `web-search` (with a SearXNG service next to the app: set `SEARXNG_SECRET` in the env file). |
65
65
  | `--auth <id>` | The HTTP channel's auth provider, `oidc` or `static`: required with `http`, refused without it, since the channel doesn't start without one. The wizard asks for it once `http` is chosen. |
66
66
  | `--directory <id>` | The user directory, `static` or `zitadel`: who the people are and their roles. Without one, the person is whoever the channel says, with no roles. |
67
67
  | `--local-packages <folder>` | Installs the packages packed in `<folder>` (`bun pm pack`) instead of the registry's, as [`mfw local-packages`](#mfw-local-packages-folder--mfw-local-packages---off) does on an existing app: their versions come from the tarballs, so a package that isn't published yet works too, and no newer `mfw` is looked up on the registry. For trying unreleased packages in a new app; the test bed makes its apps this way. |
@@ -26,6 +26,24 @@ export type CatalogEntry = {
26
26
  /** Dependencies whose install scripts Bun must run for this entry (Bun runs
27
27
  * none it isn't told to trust), on top of a tool plugin's own package. */
28
28
  trusts?: string[];
29
+ /** For a plugin built by calling its export: what the config writes in
30
+ * `plugins` (indented as an element of it) and the other imports that needs,
31
+ * whose packages the app depends on too. */
32
+ build?: {
33
+ expression: string;
34
+ imports: Array<{
35
+ names: string;
36
+ package: string;
37
+ }>;
38
+ };
39
+ /** A compose service the entry needs next to the app (its lines under
40
+ * `services:`), which the app then depends on. */
41
+ service?: {
42
+ name: string;
43
+ lines: string[];
44
+ };
45
+ /** Files the entry adds to the app, by path. */
46
+ files?: Record<string, string>;
29
47
  };
30
48
  /** Starting formatter rules for a plugin that hands the user lists: without a
31
49
  * rule a kind of list isn't shown, so the scaffolded config wraps the plugin
@@ -1,3 +1,4 @@
1
+ import { type CatalogEntry } from "./catalog.ts";
1
2
  /** What the new app is made of. `versions` maps each package the app depends
2
3
  * on to the version its range is written against. */
3
4
  export type RenderInput = {
@@ -28,3 +29,5 @@ export declare function selectionError(channels: string[], plugins: string[], au
28
29
  * Undefined when they go together. Shared with the command, which checks it
29
30
  * before writing when nothing will ask (`--yes`). */
30
31
  export declare function pairingError(channels: string[], auth?: string): string | undefined;
32
+ /** The packages an entry brings into the app: its own, and those its config imports. */
33
+ export declare function entryPackages(e: CatalogEntry): string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/cli",
3
- "version": "0.52.0",
3
+ "version": "0.53.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,22 +41,24 @@
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.52.0",
45
- "@mercury-fw/utils": "0.52.0",
44
+ "@mercury-fw/core": "0.53.1",
45
+ "@mercury-fw/utils": "0.53.1",
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.0",
51
+ "@mercury-fw/channel-google-chat": "0.8.2",
52
52
  "@mercury-fw/channel-http": "0.10.1",
53
53
  "@mercury-fw/directory-static": "0.1.1",
54
- "@mercury-fw/formatter": "0.52.0",
54
+ "@mercury-fw/formatter": "0.53.1",
55
55
  "@mercury-fw/plugin-atlassian-admin": "0.3.2",
56
56
  "@mercury-fw/plugin-bitbucket": "0.4.1",
57
57
  "@mercury-fw/plugin-jira": "0.6.5",
58
+ "@mercury-fw/plugin-web-search": "0.1.2",
58
59
  "@mercury-fw/plugin-zitadel": "0.3.2",
59
60
  "@mercury-fw/typescript-config": "*",
61
+ "@mercury-fw/web-search-searxng": "0.1.0",
60
62
  "@types/bun": "^1.4.2",
61
63
  "typescript": "^6.0.3"
62
64
  }
package/src/catalog.ts CHANGED
@@ -24,8 +24,33 @@ export type CatalogEntry = {
24
24
  /** Dependencies whose install scripts Bun must run for this entry (Bun runs
25
25
  * none it isn't told to trust), on top of a tool plugin's own package. */
26
26
  trusts?: string[];
27
+ /** For a plugin built by calling its export: what the config writes in
28
+ * `plugins` (indented as an element of it) and the other imports that needs,
29
+ * whose packages the app depends on too. */
30
+ build?: { expression: string; imports: Array<{ names: string; package: string }> };
31
+ /** A compose service the entry needs next to the app (its lines under
32
+ * `services:`), which the app then depends on. */
33
+ service?: { name: string; lines: string[] };
34
+ /** Files the entry adds to the app, by path. */
35
+ files?: Record<string, string>;
27
36
  };
28
37
 
38
+ /** SearXNG's settings for the web search plugin, mounted into its container. */
39
+ const SEARXNG_SETTINGS = `# SearXNG for the web search plugin. Only the app reaches it, on the compose
40
+ # network: nothing of it is published on the host. The secret comes from
41
+ # SEARXNG_SECRET in the app's env file.
42
+ use_default_settings: true
43
+ server:
44
+ # Only Mercury asks: no rate limiting, no image proxy.
45
+ limiter: false
46
+ image_proxy: false
47
+ search:
48
+ # The plugin reads the results as JSON, which is off by default.
49
+ formats:
50
+ - html
51
+ - json
52
+ `;
53
+
29
54
  /** Starting formatter rules for a plugin that hands the user lists: without a
30
55
  * rule a kind of list isn't shown, so the scaffolded config wraps the plugin
31
56
  * with these. `displaysType` is the type the plugin exports for its kinds,
@@ -124,6 +149,41 @@ export const CATALOG: CatalogEntry[] = [
124
149
  exportName: "zitadelPlugin",
125
150
  env: [],
126
151
  },
152
+ {
153
+ id: "web-search",
154
+ kind: "tool",
155
+ package: "@mercury-fw/plugin-web-search",
156
+ exportName: "webSearchPlugin",
157
+ env: [
158
+ { name: "SEARXNG_URL", comment: "SearXNG, the compose service the web search asks", value: "http://searxng:8080" },
159
+ { name: "SEARXNG_SECRET", comment: "Required: SearXNG's secret key, any long random string (openssl rand -hex 32)" },
160
+ ],
161
+ build: {
162
+ expression: [
163
+ " webSearchPlugin({",
164
+ ' backend: searxngBackend({ url: process.env.SEARXNG_URL || "http://searxng:8080" }),',
165
+ " // Hosts whose pages anyone may read without being asked; each covers its subdomains.",
166
+ " allowedHosts: [],",
167
+ " })",
168
+ ].join("\n"),
169
+ imports: [{ names: "searxngBackend", package: "@mercury-fw/web-search-searxng" }],
170
+ },
171
+ service: {
172
+ name: "searxng",
173
+ lines: [
174
+ " searxng:",
175
+ " # SearXNG publishes no major version tags, only latest and dated ones.",
176
+ " image: searxng/searxng:latest",
177
+ " restart: unless-stopped",
178
+ " environment:",
179
+ " # SearXNG exits on its default key: without one, compose says so instead.",
180
+ " SEARXNG_SECRET: ${SEARXNG_SECRET:?set SEARXNG_SECRET in .env (see .env.example)}",
181
+ " volumes:",
182
+ " - ./searxng/settings.yml:/etc/searxng/settings.yml:ro",
183
+ ],
184
+ },
185
+ files: { "searxng/settings.yml": SEARXNG_SETTINGS },
186
+ },
127
187
  {
128
188
  id: "static",
129
189
  kind: "directory",
package/src/main.ts CHANGED
@@ -11,7 +11,7 @@ import type { CreateArgs } from "./args.ts";
11
11
  import { CATALOG } from "./catalog.ts";
12
12
  import { kebabCase } from "./naming.ts";
13
13
  import { runProgram } from "./program.ts";
14
- import { pairingError, renderApp, selectionError } from "./render.ts";
14
+ import { entryPackages, pairingError, renderApp, selectionError } from "./render.ts";
15
15
  import { appVersions, cliVersion, newerCli, registryFrom } from "./versions.ts";
16
16
  import { appCommands, terminalDeps, type AppDeps } from "./app/commands.ts";
17
17
  import { copyPacks, readPacks, withLocalOverrides } from "./app/local-packages.ts";
@@ -147,7 +147,7 @@ async function create(args: CreateArgs, rawArgs: string[], relaunch: Relaunch, g
147
147
  : e.kind === "directory"
148
148
  ? e.id === answers.directory
149
149
  : (e.kind === "channel" ? answers.channels : answers.plugins).includes(e.id),
150
- ).map((e) => e.package),
150
+ ).flatMap(entryPackages),
151
151
  ),
152
152
  ];
153
153
  const versions = await appVersions(chosen, {
package/src/render.ts CHANGED
@@ -76,13 +76,16 @@ export function renderApp(input: RenderInput): Map<string, string> {
76
76
  const directory = selected("directory", input.directory === undefined ? [] : [input.directory])[0];
77
77
  const assistantName = input.assistantName.trim();
78
78
 
79
+ const extraFiles = [...channels, ...tools].flatMap((e) => Object.entries(e.files ?? {}));
80
+ const services = tools.flatMap((t) => (t.service ? [t.service] : []));
79
81
  return new Map([
82
+ ...extraFiles,
80
83
  [".dockerignore", dockerignore],
81
84
  [".env.example", renderEnv(channels, auth, tools, directory)],
82
85
  [".gitignore", gitignore],
83
86
  ["Dockerfile", dockerfile],
84
87
  ["README.md", renderReadme(input.name, channels, auth, tools, directory)],
85
- ["docker-compose.yml", renderCompose(input.name, channels.some((c) => c.id === "http"))],
88
+ ["docker-compose.yml", renderCompose(input.name, channels.some((c) => c.id === "http"), services)],
86
89
  ["markdown.d.ts", markdownDts],
87
90
  ["mercury.config.ts", renderConfig(assistantName, channels, auth, tools, directory)],
88
91
  ["package.json", renderPackageJson(input.name, channels, auth, tools, input.versions, directory)],
@@ -184,6 +187,7 @@ function renderConfig(
184
187
  for (const t of tools) {
185
188
  const names = t.formatter ? `${t.exportName}, type ${t.formatter.displaysType}` : t.exportName;
186
189
  imports.push(`import { ${names} } from "${t.package}";`);
190
+ for (const extra of t.build?.imports ?? []) imports.push(`import { ${extra.names} } from "${extra.package}";`);
187
191
  }
188
192
  for (const c of [...channels, ...(auth ? [auth] : []), ...(directory ? [directory] : [])]) {
189
193
  imports.push(`import { ${c.exportName} } from "${c.package}";`);
@@ -216,6 +220,7 @@ function renderConfig(
216
220
 
217
221
  /** One element of the config's `plugins` array. */
218
222
  function renderPluginEntry(t: CatalogEntry): string {
223
+ if (t.build) return `${t.build.expression},`;
219
224
  if (!t.formatter) {
220
225
  return ` ${t.exportName},`;
221
226
  }
@@ -229,6 +234,11 @@ function renderPluginEntry(t: CatalogEntry): string {
229
234
  ].join("\n");
230
235
  }
231
236
 
237
+ /** The packages an entry brings into the app: its own, and those its config imports. */
238
+ export function entryPackages(e: CatalogEntry): string[] {
239
+ return [e.package, ...(e.build?.imports ?? []).map((i) => i.package)];
240
+ }
241
+
232
242
  /** `package.json`: the core, the formatter when a plugin is wrapped in it, the
233
243
  * chosen packages, and every tool plugin trusted to run its postinstall (it
234
244
  * downloads the plugin's CLI), plus what a chosen entry says it needs trusted. */
@@ -241,7 +251,7 @@ function renderPackageJson(
241
251
  directory: CatalogEntry | undefined,
242
252
  ): string {
243
253
  const chosen = [...channels, ...(auth ? [auth] : []), ...tools, ...(directory ? [directory] : [])];
244
- const packages = [...new Set(["@mercury-fw/core", ...chosen.map((e) => e.package)])];
254
+ const packages = [...new Set(["@mercury-fw/core", ...chosen.flatMap(entryPackages)])];
245
255
  if (tools.some((t) => t.formatter)) {
246
256
  packages.push("@mercury-fw/formatter");
247
257
  }
@@ -297,8 +307,8 @@ function renderEnv(
297
307
  /** `docker-compose.yml`: the app and Qdrant, with named volumes prefixed by the
298
308
  * app's name, the CLI credentials one included whatever was chosen (a plugin
299
309
  * with a CLI can come later); the HTTP surface's port published on the host
300
- * only with the HTTP channel. */
301
- function renderCompose(name: string, hasHttp: boolean): string {
310
+ * only with the HTTP channel; and the services chosen plugins need. */
311
+ function renderCompose(name: string, hasHttp: boolean, services: Array<{ name: string; lines: string[] }>): string {
302
312
  const httpPort = hasHttp
303
313
  ? [
304
314
  " # The HTTP surface, on the host: every route but its OpenAPI document needs a token.",
@@ -325,12 +335,14 @@ function renderCompose(name: string, hasHttp: boolean): string {
325
335
  ' - "host.docker.internal:host-gateway"',
326
336
  " depends_on:",
327
337
  " - qdrant",
338
+ ...services.map((s) => ` - ${s.name}`),
328
339
  "",
329
340
  " qdrant:",
330
341
  " image: qdrant/qdrant:v1",
331
342
  " volumes:",
332
343
  " - qdrant-data:/qdrant/storage",
333
344
  "",
345
+ ...services.flatMap((s) => [...s.lines, ""]),
334
346
  "volumes:",
335
347
  " wiki-vault:",
336
348
  ` name: ${name}_wiki-vault`,
@@ -351,6 +363,13 @@ function renderReadme(
351
363
  directory: CatalogEntry | undefined,
352
364
  ): string {
353
365
  const list = (entries: CatalogEntry[]) => (entries.length > 0 ? entries.map((e) => e.id).join(", ") : "none");
366
+ // What the app can't start without, named where the reader is about to start it.
367
+ const required = [...channels, ...(auth ? [auth] : []), ...tools, ...(directory ? [directory] : [])]
368
+ .flatMap((e) => e.env)
369
+ .filter((v) => v.comment.startsWith("Required:"))
370
+ .map((v) => `\`${v.name}\``);
371
+ const requiredNote =
372
+ required.length > 0 ? `\nBefore \`mfw start\`, fill in the variables the chosen pieces require: ${required.join(", ")}.\n` : "";
354
373
  return `# ${name}
355
374
 
356
375
  A Mercury app, scaffolded by \`mfw create\`.
@@ -381,7 +400,7 @@ cp .env.example .env
381
400
  mfw start
382
401
  mfw repl
383
402
  \`\`\`
384
-
403
+ ${requiredNote}
385
404
  \`bun install\` here gives your editor, \`bun run typecheck\` and the app's own \`mfw\` (the one a global \`mfw\` runs inside the app) the packages (tool plugins download their CLI binary as they install); the image installs its own copy when it builds. \`mfw start\` builds the image and starts the app with Qdrant in the background, \`mfw repl\` opens a terminal conversation with the assistant. \`mfw --help\` lists the rest: stopping and restarting, logs, a shell in the container, the wiki and the memory, and resetting them.
386
405
  ${renderHttpSection(channels, auth)}${CREDENTIALS_SECTION}`;
387
406
  }