@executablemd/cli 0.4.2 → 0.5.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/esm/deno.js CHANGED
@@ -1,12 +1,13 @@
1
1
  export default {
2
2
  "name": "@executablemd/cli",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "exports": "./src/cli.ts",
5
5
  "imports": {
6
6
  "@types/node": "npm:@types/node@^24.5.2",
7
7
  "@durable-streams/client": "npm:@durable-streams/client@^0.2.2",
8
- "@effectionx/bdd/node": "file:///home/runner/work/executable.md/executable.md/test-support/bdd.ts",
9
- "@effectionx/bdd/expect": "file:///home/runner/work/executable.md/executable.md/test-support/expect.ts",
8
+ "@durable-streams/server": "npm:@durable-streams/server@^0.3.8",
9
+ "@effectionx/bdd/node": "file:///home/runner/work/executable.md/executable.md/packages/test-support/bdd.ts",
10
+ "@effectionx/bdd/expect": "file:///home/runner/work/executable.md/executable.md/packages/test-support/expect.ts",
10
11
  "@effectionx/test-adapter": "npm:@effectionx/test-adapter@0.7.4",
11
12
  "effection": "npm:effection@4.1.0-alpha.7",
12
13
  "effection/experimental": "npm:effection@4.1.0-alpha.7/experimental",
@@ -30,15 +31,19 @@ export default {
30
31
  "oxlint-tsgolint": "npm:oxlint-tsgolint@^0.17",
31
32
  "unist-util-select": "npm:unist-util-select@^5",
32
33
  "mdast-util-to-string": "npm:mdast-util-to-string@^4",
33
- "@executablemd/core": "npm:@executablemd/core@^0.4.2",
34
- "@executablemd/core/": "npm:@executablemd/core@^0.4.2/",
35
- "@executablemd/durable-streams": "npm:@executablemd/durable-streams@^0.4.2",
36
- "@executablemd/durable-streams/": "npm:@executablemd/durable-streams@^0.4.2/",
37
- "@executablemd/runtime": "npm:@executablemd/runtime@^0.4.2",
38
- "@executablemd/runtime/": "npm:@executablemd/runtime@^0.4.2/",
39
- "@executablemd/testing": "npm:@executablemd/testing@^0.4.2",
40
- "@executablemd/testing/": "npm:@executablemd/testing@^0.4.2/",
41
- "@executablemd/code-review-agent": "npm:@executablemd/code-review-agent@^0.4.2",
42
- "@executablemd/code-review-agent/": "npm:@executablemd/code-review-agent@^0.4.2/"
34
+ "@executablemd/acp": "npm:@executablemd/acp@^0.5.1",
35
+ "@executablemd/acp/": "npm:@executablemd/acp@^0.5.1/",
36
+ "@executablemd/code-review-agent": "npm:@executablemd/code-review-agent@^0.5.1",
37
+ "@executablemd/code-review-agent/": "npm:@executablemd/code-review-agent@^0.5.1/",
38
+ "@executablemd/core": "npm:@executablemd/core@^0.5.1",
39
+ "@executablemd/core/": "npm:@executablemd/core@^0.5.1/",
40
+ "@executablemd/durable-streams": "npm:@executablemd/durable-streams@^0.5.1",
41
+ "@executablemd/durable-streams/": "npm:@executablemd/durable-streams@^0.5.1/",
42
+ "@executablemd/runtime": "npm:@executablemd/runtime@^0.5.1",
43
+ "@executablemd/runtime/": "npm:@executablemd/runtime@^0.5.1/",
44
+ "@executablemd/test-agent": "npm:@executablemd/test-agent@^0.5.1",
45
+ "@executablemd/test-agent/": "npm:@executablemd/test-agent@^0.5.1/",
46
+ "@executablemd/testing": "npm:@executablemd/testing@^0.5.1",
47
+ "@executablemd/testing/": "npm:@executablemd/testing@^0.5.1/"
43
48
  }
44
49
  };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Agent flag parsing for `xmd run` (specs/acp-client-spec.md
3
+ * §Command-line configuration).
4
+ *
5
+ * A pure function over the parsed flags: it reads no Context Apis, no
6
+ * environment, and no configuration. The `DEFAULT_AGENT_NAME` lookup and
7
+ * every install step stay in the CLI, so the flag-to-configuration
8
+ * mapping can be asserted on its own.
9
+ */
10
+ /** `digits` or `digits "." digits` — nothing else is a number of seconds. */
11
+ const SECONDS = /^\d+(\.\d+)?$/;
12
+ function parseTimeout(value) {
13
+ const invalid = {
14
+ error: `--timeout must be a positive number of seconds, got "${value}"`,
15
+ };
16
+ const trimmed = value.trim();
17
+ if (!SECONDS.test(trimmed)) {
18
+ return invalid;
19
+ }
20
+ const seconds = Number(trimmed);
21
+ if (!Number.isFinite(seconds) || seconds <= 0) {
22
+ return invalid;
23
+ }
24
+ return seconds * 1000;
25
+ }
26
+ export function resolveAgentConfig(flags) {
27
+ const selected = [flags.approveAll, flags.approveReads, flags.denyAll].filter(Boolean);
28
+ if (selected.length > 1) {
29
+ return { error: "--approve-all, --approve-reads, and --deny-all are mutually exclusive" };
30
+ }
31
+ const permissionMode = flags.approveAll
32
+ ? "approve-all"
33
+ : flags.denyAll
34
+ ? "deny-all"
35
+ : "approve-reads";
36
+ let timeoutMs;
37
+ if (flags.timeout !== undefined) {
38
+ const parsed = parseTimeout(flags.timeout);
39
+ if (typeof parsed !== "number") {
40
+ return parsed;
41
+ }
42
+ timeoutMs = parsed;
43
+ }
44
+ return { permissionMode, defaultAgent: flags.defaultAgent, timeoutMs };
45
+ }
package/esm/src/cli.js CHANGED
@@ -7,20 +7,25 @@
7
7
  * xmd <document.md> [options] (run is the default command)
8
8
  *
9
9
  * Examples:
10
- * xmd run core/examples/hello-world.md
11
- * xmd core/examples/hello-world.md --verbose
12
- * xmd run core/examples/hello-world.md --journal events.jsonl
10
+ * xmd run packages/core/examples/hello-world.md
11
+ * xmd packages/core/examples/hello-world.md --verbose
12
+ * xmd run packages/core/examples/hello-world.md --journal events.jsonl
13
13
  */
14
14
  import { main, exit, spawn, each, createSignal, until } from "effection";
15
15
  import { InMemoryStream, } from "@executablemd/durable-streams";
16
16
  import { forEach } from "@effectionx/stream-helpers";
17
17
  import { open } from "node:fs/promises";
18
+ import { basename, extname } from "node:path";
18
19
  import { inspect } from "node:util";
19
20
  import process from "node:process";
20
21
  import { program, object, field, cli, commands } from "configliere";
21
22
  import { z } from "zod";
22
- import { execute, useNormalizedOutput, useTerminalOutput } from "@executablemd/core";
23
+ import { AgentProviders, Config, execute, installAgentVocabulary, installPermissionMode, registerAgentProvider, useNormalizedOutput, useTerminalOutput, } from "@executablemd/core";
24
+ import { env as readEnv } from "@executablemd/runtime";
25
+ import { createAcpxProvider, DEFAULT_AGENT_NAME } from "@executablemd/acp";
23
26
  import { installTestingVocabulary, TestFailureError, useTesting } from "@executablemd/testing";
27
+ import { installTestAgentVocabulary, runTestAgentWorker } from "@executablemd/test-agent";
28
+ import { resolveAgentConfig } from "./agent-config.js";
24
29
  import { FileStream } from "./file-stream.js";
25
30
  import denoJson from "../deno.js";
26
31
  const defaults = (value) => (mods) => ({ ...mods, default: value });
@@ -47,6 +52,30 @@ const runConfig = object({
47
52
  description: "output raw markdown without normalization or terminal formatting",
48
53
  ...field(z.boolean(), defaults(false)),
49
54
  },
55
+ agentProvider: {
56
+ description: "agent provider for agent components",
57
+ ...field(z.string(), defaults("acpx")),
58
+ },
59
+ defaultAgent: {
60
+ description: "default agent name (overrides DEFAULT_AGENT_NAME)",
61
+ ...field(z.string().optional()),
62
+ },
63
+ timeout: {
64
+ description: "shared timeout in seconds for process, fetch, and agent operations",
65
+ ...field(z.union([z.string(), z.number()]).optional()),
66
+ },
67
+ approveAll: {
68
+ description: "approve every agent permission request",
69
+ ...field(z.boolean(), defaults(false)),
70
+ },
71
+ approveReads: {
72
+ description: "approve read and search agent permissions, ask for the rest (default)",
73
+ ...field(z.boolean(), defaults(false)),
74
+ },
75
+ denyAll: {
76
+ description: "deny every agent permission request",
77
+ ...field(z.boolean(), defaults(false)),
78
+ },
50
79
  });
51
80
  const testConfig = object({
52
81
  docPath: {
@@ -72,10 +101,16 @@ const testConfig = object({
72
101
  ...field(z.boolean(), defaults(false)),
73
102
  },
74
103
  });
104
+ const testAgentConfig = object({
105
+ connect: {
106
+ description: "opaque controller route (controller-launched workers only)",
107
+ ...field(z.string()),
108
+ },
109
+ });
75
110
  const xmd = program({
76
111
  name: "xmd",
77
112
  version: denoJson.version,
78
- config: commands({ run: runConfig, test: testConfig }, { default: "run" }),
113
+ config: commands({ run: runConfig, test: testConfig, "test-agent": testAgentConfig }, { default: "run" }),
79
114
  });
80
115
  const pretty = (value) => inspect(value, {
81
116
  colors: true,
@@ -129,6 +164,115 @@ function* createJournalFile(filePath) {
129
164
  }
130
165
  yield* until(handle.close());
131
166
  }
167
+ /**
168
+ * The CLI entry script to hand back to a relaunched runtime. Deno and
169
+ * Node both report the module they started as `process.argv[1]`: the
170
+ * source `cli.ts` under `deno run`, the generated bin under npm. Only
171
+ * the compiled binary carries no entry script, and it never asks.
172
+ */
173
+ function cliEntrypoint() {
174
+ const entrypoint = process.argv[1];
175
+ if (!entrypoint) {
176
+ throw new Error("cannot start the test-agent worker: this xmd was launched without a CLI entry script, so there is nothing for the worker process to run");
177
+ }
178
+ return entrypoint;
179
+ }
180
+ /**
181
+ * A worker inheriting the parent's `--inspect` would exit immediately on
182
+ * the debug port the parent already holds, so those options do not carry
183
+ * across the relaunch. Everything else the parent runs under does.
184
+ */
185
+ function workerExecArgv() {
186
+ return process.execArgv.filter((option) => !option.startsWith("--inspect"));
187
+ }
188
+ /**
189
+ * The command that relaunches this xmd as a test-agent worker. Three
190
+ * builds reach this: the compiled binary invokes itself, the Deno source
191
+ * CLI reconstructs `deno run`, and the npm package reconstructs `node`.
192
+ * `process.execPath` names the running executable under both runtimes —
193
+ * `Deno.execPath()` exists in neither the Node build nor Node itself.
194
+ */
195
+ function resolveWorkerCommand() {
196
+ const execPath = process.execPath;
197
+ const runtime = basename(execPath, extname(execPath));
198
+ if (runtime === "deno") {
199
+ return [execPath, "run", "--allow-all", cliEntrypoint(), "test-agent"];
200
+ }
201
+ if (runtime === "node") {
202
+ return [execPath, ...workerExecArgv(), cliEntrypoint(), "test-agent"];
203
+ }
204
+ return [execPath, "test-agent"];
205
+ }
206
+ const AGENT_ONLY_FLAGS = [
207
+ "--agent-provider",
208
+ "--default-agent",
209
+ "--timeout",
210
+ "--approve-all",
211
+ "--approve-reads",
212
+ "--deny-all",
213
+ ];
214
+ /**
215
+ * Agent options belong to `xmd run`. The argument parser ignores options
216
+ * it does not define rather than rejecting them, so `xmd test` has to
217
+ * reject these itself instead of silently running without them.
218
+ */
219
+ function findAgentOnlyFlag(args) {
220
+ return args.find((arg) => AGENT_ONLY_FLAGS.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
221
+ }
222
+ /**
223
+ * The text an option carried on the command line. `--timeout` validates
224
+ * this rather than the parsed value: the parser coerces `1e3` and `0x10`
225
+ * into numbers and drops `.5`, `+1` and `Infinity`, so forms the grammar
226
+ * rejects would otherwise reach the stack as valid seconds.
227
+ */
228
+ function findFlagText(args, flag) {
229
+ for (const [index, arg] of args.entries()) {
230
+ if (arg === flag) {
231
+ return args[index + 1] ?? "";
232
+ }
233
+ if (arg.startsWith(`${flag}=`)) {
234
+ return arg.slice(flag.length + 1);
235
+ }
236
+ }
237
+ return undefined;
238
+ }
239
+ /**
240
+ * Install the agent stack for `xmd run`: permission mode, contextual
241
+ * timeout, the ACPX registration, and the vocabulary with the resolved
242
+ * root provider. Invalid flags and an unknown --agent-provider fail here
243
+ * — before any document executes. Nothing starts an agent: the provider
244
+ * validates availability on first use.
245
+ */
246
+ function* installAgentStack(flags) {
247
+ const config = resolveAgentConfig(flags);
248
+ if ("error" in config) {
249
+ console.error(config.error);
250
+ yield* exit(1);
251
+ return;
252
+ }
253
+ if (config.timeoutMs !== undefined) {
254
+ const ms = config.timeoutMs;
255
+ yield* Config.around({ timeout: () => ms }, { at: "min" });
256
+ }
257
+ yield* registerAgentProvider("acpx", createAcpxProvider());
258
+ const defaultAgent = config.defaultAgent ?? (yield* readEnv("DEFAULT_AGENT_NAME")) ?? DEFAULT_AGENT_NAME;
259
+ let factory;
260
+ try {
261
+ factory = yield* AgentProviders.operations.resolve(flags.agentProvider);
262
+ }
263
+ catch (error) {
264
+ console.error(error instanceof Error ? error.message : String(error));
265
+ yield* exit(1);
266
+ return;
267
+ }
268
+ const permissionMode = config.permissionMode;
269
+ yield* installAgentVocabulary({
270
+ defaultAgent,
271
+ permissionMode,
272
+ rootProvider: { factory, options: { defaultAgent, permissionMode } },
273
+ });
274
+ yield* installPermissionMode(permissionMode);
275
+ }
132
276
  function* run(config, mode) {
133
277
  const { docPath, componentDir, verbose, journal, raw } = config;
134
278
  // Every CLI invocation starts from an empty stream. --journal writes
@@ -173,10 +317,19 @@ function* run(config, mode) {
173
317
  // additionally activates root testing through a useTesting() session.
174
318
  if (mode.testing) {
175
319
  yield* useTesting({ verbose });
320
+ // TestAgent installs before the agent vocabulary so its <Prompt>
321
+ // interceptor runs first.
322
+ yield* installTestAgentVocabulary({ workerCommand: resolveWorkerCommand() });
323
+ yield* installAgentVocabulary();
176
324
  }
177
325
  else {
178
326
  yield* installTestingVocabulary({ verbose });
179
327
  }
328
+ // Agent flags are exclusive to `xmd run` — `xmd test` drives agents
329
+ // through the deterministic TestAgent stack instead.
330
+ if (mode.agent) {
331
+ yield* installAgentStack(mode.agent);
332
+ }
180
333
  const execution = yield* execute({
181
334
  docPath,
182
335
  stream,
@@ -231,12 +384,34 @@ await main(function* (args) {
231
384
  break;
232
385
  }
233
386
  switch (parsed.value.name) {
234
- case "run":
235
- yield* run(parsed.value.config, { testing: false });
387
+ case "run": {
388
+ const config = parsed.value.config;
389
+ yield* run(config, {
390
+ testing: false,
391
+ agent: {
392
+ agentProvider: config.agentProvider,
393
+ defaultAgent: config.defaultAgent,
394
+ timeout: findFlagText(args, "--timeout"),
395
+ approveAll: config.approveAll,
396
+ approveReads: config.approveReads,
397
+ denyAll: config.denyAll,
398
+ },
399
+ });
236
400
  break;
237
- case "test":
401
+ }
402
+ case "test": {
403
+ const agentFlag = findAgentOnlyFlag(args);
404
+ if (agentFlag) {
405
+ console.error(`unrecognized option for xmd test: ${agentFlag} — agent options are exclusive to xmd run`);
406
+ yield* exit(1);
407
+ break;
408
+ }
238
409
  yield* run(parsed.value.config, { testing: true });
239
410
  break;
411
+ }
412
+ case "test-agent":
413
+ yield* runTestAgentWorker({ connect: parsed.value.config.connect });
414
+ break;
240
415
  }
241
416
  }
242
417
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@executablemd/cli",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "The xmd command-line interface for executable.md.",
5
5
  "homepage": "https://executable.md",
6
6
  "repository": {
@@ -17,9 +17,12 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@effectionx/stream-helpers": "0.8.3",
20
- "@executablemd/core": "^0.4.2",
21
- "@executablemd/durable-streams": "^0.4.2",
22
- "@executablemd/testing": "^0.4.2",
20
+ "@executablemd/acp": "^0.5.1",
21
+ "@executablemd/core": "^0.5.1",
22
+ "@executablemd/durable-streams": "^0.5.1",
23
+ "@executablemd/runtime": "^0.5.1",
24
+ "@executablemd/test-agent": "^0.5.1",
25
+ "@executablemd/testing": "^0.5.1",
23
26
  "configliere": "^0.2.3",
24
27
  "effection": "4.1.0-alpha.7",
25
28
  "zod": "^4.3.6"
package/types/deno.d.ts CHANGED
@@ -5,6 +5,7 @@ declare namespace _default {
5
5
  let imports: {
6
6
  "@types/node": string;
7
7
  "@durable-streams/client": string;
8
+ "@durable-streams/server": string;
8
9
  "@effectionx/bdd/node": string;
9
10
  "@effectionx/bdd/expect": string;
10
11
  "@effectionx/test-adapter": string;
@@ -30,16 +31,20 @@ declare namespace _default {
30
31
  "oxlint-tsgolint": string;
31
32
  "unist-util-select": string;
32
33
  "mdast-util-to-string": string;
34
+ "@executablemd/acp": string;
35
+ "@executablemd/acp/": string;
36
+ "@executablemd/code-review-agent": string;
37
+ "@executablemd/code-review-agent/": string;
33
38
  "@executablemd/core": string;
34
39
  "@executablemd/core/": string;
35
40
  "@executablemd/durable-streams": string;
36
41
  "@executablemd/durable-streams/": string;
37
42
  "@executablemd/runtime": string;
38
43
  "@executablemd/runtime/": string;
44
+ "@executablemd/test-agent": string;
45
+ "@executablemd/test-agent/": string;
39
46
  "@executablemd/testing": string;
40
47
  "@executablemd/testing/": string;
41
- "@executablemd/code-review-agent": string;
42
- "@executablemd/code-review-agent/": string;
43
48
  };
44
49
  }
45
50
  export default _default;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Agent flag parsing for `xmd run` (specs/acp-client-spec.md
3
+ * §Command-line configuration).
4
+ *
5
+ * A pure function over the parsed flags: it reads no Context Apis, no
6
+ * environment, and no configuration. The `DEFAULT_AGENT_NAME` lookup and
7
+ * every install step stay in the CLI, so the flag-to-configuration
8
+ * mapping can be asserted on its own.
9
+ */
10
+ import type { PermissionMode } from "@executablemd/core";
11
+ export interface AgentFlags {
12
+ agentProvider: string;
13
+ defaultAgent: string | undefined;
14
+ /**
15
+ * The raw `--timeout` text. The argument parser coerces or drops
16
+ * lexical forms — `1e3`, `0x10`, `.5`, `+1` and `Infinity` all reach a
17
+ * number-typed field as something this grammar would have rejected —
18
+ * so the untransformed text is what gets validated.
19
+ */
20
+ timeout: string | undefined;
21
+ approveAll: boolean;
22
+ approveReads: boolean;
23
+ denyAll: boolean;
24
+ }
25
+ export interface AgentConfig {
26
+ permissionMode: PermissionMode;
27
+ /** The `--default-agent` value; the environment fallback is applied by the caller. */
28
+ defaultAgent: string | undefined;
29
+ timeoutMs: number | undefined;
30
+ }
31
+ export declare function resolveAgentConfig(flags: AgentFlags): AgentConfig | {
32
+ error: string;
33
+ };
@@ -7,8 +7,8 @@
7
7
  * xmd <document.md> [options] (run is the default command)
8
8
  *
9
9
  * Examples:
10
- * xmd run core/examples/hello-world.md
11
- * xmd core/examples/hello-world.md --verbose
12
- * xmd run core/examples/hello-world.md --journal events.jsonl
10
+ * xmd run packages/core/examples/hello-world.md
11
+ * xmd packages/core/examples/hello-world.md --verbose
12
+ * xmd run packages/core/examples/hello-world.md --journal events.jsonl
13
13
  */
14
14
  export {};