@yaag/extension 0.2.1 → 0.4.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 +18 -3
- package/package.json +4 -4
- package/src/cli-child.ts +34 -4
- package/src/eval-pipe.ts +25 -0
- package/src/event-reader.ts +8 -0
- package/src/fake-extension-ui.ts +6 -12
- package/src/fake-run-start.ts +25 -0
- package/src/resume-source.ts +77 -0
- package/src/run-argv.ts +39 -0
- package/src/run-call-render.ts +58 -0
- package/src/run-program-param.ts +178 -0
- package/src/run-record.ts +109 -20
- package/src/run-registry.ts +16 -0
- package/src/run-store.ts +22 -5
- package/src/run-summary-parse.ts +265 -0
- package/src/run-tool-test-support.ts +1 -6
- package/src/run-tool.ts +66 -22
- package/src/run-tree-host.ts +5 -4
- package/src/spawn-run.ts +20 -7
- package/src/status-tool.ts +32 -9
- package/src/test-tui-context.ts +4 -14
- package/src/yaag-command.ts +1 -1
package/src/run-tool.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { access } from "node:fs/promises";
|
|
2
1
|
import { resolve } from "node:path";
|
|
3
2
|
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
@@ -13,14 +12,33 @@ import { RunTreeStore } from "./run-trees.ts";
|
|
|
13
12
|
export type { RunDetails } from "./run-details.ts";
|
|
14
13
|
|
|
15
14
|
import { type ProcessIdentity, readProcessStart } from "./process-liveness.ts";
|
|
15
|
+
import { resolveProgramParams } from "./resume-source.ts";
|
|
16
|
+
import type { ProgramTarget } from "./run-argv.ts";
|
|
16
17
|
import { mintRunId } from "./run-id.ts";
|
|
18
|
+
import {
|
|
19
|
+
confirmProgramTarget,
|
|
20
|
+
programExpansion,
|
|
21
|
+
programLabel,
|
|
22
|
+
programTarget,
|
|
23
|
+
} from "./run-program-param.ts";
|
|
24
|
+
import type { RunLaunch } from "./run-record.ts";
|
|
17
25
|
import type { LiveRun, RunRegistry, RunSettlement } from "./run-registry.ts";
|
|
18
26
|
import { errorText, failure, observedSettlement } from "./run-settlement.ts";
|
|
19
27
|
import { type RunHandle, type RunOutcome, type StartRunOptions, startRun } from "./spawn-run.ts";
|
|
20
28
|
import { statusReport } from "./status.ts";
|
|
21
29
|
|
|
22
30
|
const parameters = Type.Object({
|
|
23
|
-
file: Type.
|
|
31
|
+
file: Type.Optional(
|
|
32
|
+
Type.String({
|
|
33
|
+
description: "Path to the Orchestration Program file; use script instead for inline source",
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
script: Type.Optional(
|
|
37
|
+
Type.String({
|
|
38
|
+
description:
|
|
39
|
+
'The Orchestration Program source text; it can import "@yaag/runtime" and "typebox" only',
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
24
42
|
args: Type.Optional(
|
|
25
43
|
Type.String({ description: "The program's arguments, as a JSON object string" }),
|
|
26
44
|
),
|
|
@@ -33,7 +51,7 @@ const parameters = Type.Object({
|
|
|
33
51
|
resume: Type.Optional(
|
|
34
52
|
Type.String({
|
|
35
53
|
description:
|
|
36
|
-
"Resume from this Cassette: matching Asks replay free, then the Run continues live",
|
|
54
|
+
"Resume from this Cassette: matching Asks replay free, then the Run continues live. For an inline Run, give resume with no file and no script, and yaag reuses the stored source",
|
|
37
55
|
}),
|
|
38
56
|
),
|
|
39
57
|
});
|
|
@@ -72,6 +90,22 @@ const DESCRIPTION = [
|
|
|
72
90
|
"later message. Every Run gets an id and can be inspected with `yaag_status`;",
|
|
73
91
|
"background Runs may overlap and each is stopped with `yaag_stop({ id })`.",
|
|
74
92
|
"",
|
|
93
|
+
"`script` runs a program that you give as source text. It needs no file.",
|
|
94
|
+
"Give `file` or `script` to start a new Run. Do not give both. A call with",
|
|
95
|
+
"both, or a new Run with neither, fails before yaag starts the Run. A call that",
|
|
96
|
+
"gives `resume` alone is the one exception: it repeats a stored inline Run.",
|
|
97
|
+
"A `script` program has the same shape as a file program. It default-exports",
|
|
98
|
+
"`defineRun(...)`.",
|
|
99
|
+
'A `script` program can import "@yaag/runtime" and "typebox" only. Make a file',
|
|
100
|
+
"program if the program needs other modules.",
|
|
101
|
+
"A `script` program must be 65536 bytes (64 KiB) or smaller. Write a larger",
|
|
102
|
+
"program to a file and give `file`.",
|
|
103
|
+
"`args`, `background`, `record` and `resume` work the same way for `script`.",
|
|
104
|
+
"To resume an inline Run, give `resume` and give no `file` and no `script`.",
|
|
105
|
+
"yaag reads the program source from its own Run record.",
|
|
106
|
+
"Give `script` again only when you want to change the program.",
|
|
107
|
+
"`yaag_describe` reads a file only.",
|
|
108
|
+
"",
|
|
75
109
|
"Set `record` to write the Run's Cassette artifact. If a recorded Run fails,",
|
|
76
110
|
"pass its artifact as `resume` on the retry: Asks that already succeeded",
|
|
77
111
|
"replay instantly and free, and the Run goes live where it diverges. Record",
|
|
@@ -82,9 +116,13 @@ const DESCRIPTION = [
|
|
|
82
116
|
* The `yaag_run` tool: runs one Orchestration Program through the Bun CLI and
|
|
83
117
|
* returns its value (ADR-0005).
|
|
84
118
|
*
|
|
85
|
-
* `execute` throws on any failure
|
|
86
|
-
*
|
|
87
|
-
*
|
|
119
|
+
* `execute` throws on any failure, which is how pi marks a tool result as an
|
|
120
|
+
* error. It refuses the call before any child starts when the parameters are
|
|
121
|
+
* not one program: both `file` and `script`, a new Run with neither, a blank
|
|
122
|
+
* or oversize `script`, or a `resume` alone whose Run record holds no Inline
|
|
123
|
+
* Program source (ADR-0033). It then throws for a missing Bun, a program file
|
|
124
|
+
* that is not there, or a Run that failed. On a Run failure the message
|
|
125
|
+
* carries the tail of the CLI's own error output.
|
|
88
126
|
*/
|
|
89
127
|
export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof parameters, RunDetails> {
|
|
90
128
|
const { bun, cli, registry, sendMessage } = deps;
|
|
@@ -99,9 +137,11 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
|
|
|
99
137
|
renderCall(params, theme, context) {
|
|
100
138
|
const text =
|
|
101
139
|
context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
102
|
-
|
|
103
|
-
theme.fg("toolTitle", theme.bold("yaag_run")) +
|
|
104
|
-
|
|
140
|
+
const head =
|
|
141
|
+
theme.fg("toolTitle", theme.bold("yaag_run")) +
|
|
142
|
+
theme.fg("muted", `(${programLabel(params)})`);
|
|
143
|
+
const body = context.expanded ? programExpansion(params) : "";
|
|
144
|
+
text.setText(body === "" ? head : `${head}\n${theme.fg("muted", body)}`);
|
|
105
145
|
return text;
|
|
106
146
|
},
|
|
107
147
|
renderResult(result, _options, _theme, context) {
|
|
@@ -115,14 +155,15 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
|
|
|
115
155
|
return tree;
|
|
116
156
|
},
|
|
117
157
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
158
|
+
// Shape only, before the Bun check: an exactly-one-of violation, an empty
|
|
159
|
+
// or oversize script is refused identically with or without Bun. The file
|
|
160
|
+
// probe comes after, so a Host Session with no Bun is told to install Bun
|
|
161
|
+
// instead of being told its path is wrong (ADR-0033).
|
|
162
|
+
// A resume with no file and no script takes its source from the Run
|
|
163
|
+
// record, before the shape check, so the shape it produces is valid.
|
|
164
|
+
const target = programTarget(await resolveProgramParams(params, registry));
|
|
118
165
|
if (bun === null) throw new Error(statusReport(null, cli));
|
|
119
|
-
|
|
120
|
-
const file = resolve(params.file);
|
|
121
|
-
try {
|
|
122
|
-
await access(file);
|
|
123
|
-
} catch {
|
|
124
|
-
throw new Error(`yaag_run: no such Orchestration Program: ${file}`);
|
|
125
|
-
}
|
|
166
|
+
const program = await confirmProgramTarget(target);
|
|
126
167
|
|
|
127
168
|
const id = mintRunId();
|
|
128
169
|
const background = params.background === true;
|
|
@@ -133,7 +174,7 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
|
|
|
133
174
|
const run = registeredRun({
|
|
134
175
|
bun,
|
|
135
176
|
cli,
|
|
136
|
-
|
|
177
|
+
program,
|
|
137
178
|
args: params.args,
|
|
138
179
|
...cassetteOptions(params),
|
|
139
180
|
id,
|
|
@@ -169,7 +210,7 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
|
|
|
169
210
|
}
|
|
170
211
|
|
|
171
212
|
/**
|
|
172
|
-
* Cassette paths
|
|
213
|
+
* Cassette paths are `resolve`d here; the CLI owns every rule about them —
|
|
173
214
|
* combinations, validation, refusals — and its own message surfaces on failure.
|
|
174
215
|
*/
|
|
175
216
|
function cassetteOptions(params: { readonly record?: string; readonly resume?: string }): {
|
|
@@ -207,7 +248,7 @@ async function announce(
|
|
|
207
248
|
function registeredRun(options: {
|
|
208
249
|
readonly bun: string;
|
|
209
250
|
readonly cli: string;
|
|
210
|
-
readonly
|
|
251
|
+
readonly program: ProgramTarget;
|
|
211
252
|
readonly args?: string;
|
|
212
253
|
readonly record?: string;
|
|
213
254
|
readonly resume?: string;
|
|
@@ -221,7 +262,7 @@ function registeredRun(options: {
|
|
|
221
262
|
const handle = options.start({
|
|
222
263
|
bun: options.bun,
|
|
223
264
|
cli: options.cli,
|
|
224
|
-
|
|
265
|
+
program: options.program,
|
|
225
266
|
args: options.args,
|
|
226
267
|
record: options.record,
|
|
227
268
|
resume: options.resume,
|
|
@@ -237,12 +278,15 @@ function registeredRun(options: {
|
|
|
237
278
|
outcome: handle.outcome,
|
|
238
279
|
summary: initialSummary(),
|
|
239
280
|
};
|
|
240
|
-
const
|
|
241
|
-
file: options.file,
|
|
281
|
+
const context = {
|
|
242
282
|
...(options.args === undefined ? {} : { args: options.args }),
|
|
243
283
|
...(options.record === undefined ? {} : { record: options.record }),
|
|
244
284
|
...(options.resume === undefined ? {} : { resume: options.resume }),
|
|
245
285
|
};
|
|
286
|
+
const launch: RunLaunch =
|
|
287
|
+
options.program.kind === "file"
|
|
288
|
+
? { kind: "file", file: options.program.file, ...context }
|
|
289
|
+
: { kind: "inline", script: options.program.source, ...context };
|
|
246
290
|
options.registry.add(run, launch, processIdentity(handle.pid));
|
|
247
291
|
return run;
|
|
248
292
|
}
|
package/src/run-tree-host.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { NamedKeybindings, RunTreeViewHost, TreeState } from "@yaag/tui";
|
|
|
13
13
|
|
|
14
14
|
/** The pi services one open view holds. */
|
|
15
15
|
export interface RunTreeHostOptions {
|
|
16
|
-
readonly ui: Pick<ExtensionUIContext, "notify"
|
|
16
|
+
readonly ui: Pick<ExtensionUIContext, "notify">;
|
|
17
17
|
readonly tui: TUI;
|
|
18
18
|
readonly keybindings: NamedKeybindings;
|
|
19
19
|
/** The Run's projection; the host only reads it. */
|
|
@@ -31,10 +31,11 @@ export function createRunTreeHost(options: RunTreeHostOptions): ReadOnlyRunTreeH
|
|
|
31
31
|
agentInfo: (agent) => state.summary.agents[agent],
|
|
32
32
|
readSession: (agent) => readSession(state.summary.agents[agent]),
|
|
33
33
|
sessionPath: (agent) => state.summary.agents[agent]?.sessionFile ?? null,
|
|
34
|
+
// No capability here opens a pi dialog: `ui.editor`, `select`, `input`, and
|
|
35
|
+
// `confirm` all clear the editor container, which removes the non-overlay
|
|
36
|
+
// `ctx.ui.custom()` view and stops pi's input loop (ADR-0034). Every drill
|
|
37
|
+
// layer draws itself instead.
|
|
34
38
|
copyPath: (text) => copyToClipboard(text),
|
|
35
|
-
openEditor: async (title, body) => {
|
|
36
|
-
await ui.editor(title, body);
|
|
37
|
-
},
|
|
38
39
|
notify: (message, level) => ui.notify(message, level),
|
|
39
40
|
requestRender: () => tui.requestRender(),
|
|
40
41
|
rows: () => Math.max(1, tui.terminal.rows - 1),
|
package/src/spawn-run.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { applyEvent, initialSummary, type LifecycleEvent, type RunSummary } from "@yaag/runtime";
|
|
2
2
|
import { startCliChild } from "./cli-child.ts";
|
|
3
3
|
import { readEvents } from "./event-reader.ts";
|
|
4
|
+
import { type ProgramTarget, runArgv } from "./run-argv.ts";
|
|
5
|
+
|
|
6
|
+
export type { ProgramTarget } from "./run-argv.ts";
|
|
4
7
|
|
|
5
8
|
/** Everything one Run of the CLI produced, once the child has exited. */
|
|
6
9
|
export interface RunOutcome {
|
|
@@ -15,7 +18,8 @@ export interface RunOutcome {
|
|
|
15
18
|
export interface StartRunOptions {
|
|
16
19
|
readonly bun: string;
|
|
17
20
|
readonly cli: string;
|
|
18
|
-
|
|
21
|
+
/** A program file, or an Inline Program whose source travels on fd 4 as `--eval-fd` (ADR-0033). */
|
|
22
|
+
readonly program: ProgramTarget;
|
|
19
23
|
/** Opaque JSON, forwarded untouched: the program validates it (ADR-0010). */
|
|
20
24
|
readonly args?: string;
|
|
21
25
|
/** Cassette path to write, forwarded as `--record` (ADR-0013). */
|
|
@@ -44,17 +48,26 @@ export interface RunHandle {
|
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
50
|
* Starts one Orchestration Program as a child `bun` process without waiting for
|
|
47
|
-
* it. Descriptor 3 remains exclusive to Run Lifecycle Events (ADR-0016)
|
|
51
|
+
* it. Descriptor 3 remains exclusive to Run Lifecycle Events out (ADR-0016),
|
|
52
|
+
* and descriptor 4 carries the Inline Program source in (ADR-0033).
|
|
48
53
|
*
|
|
49
54
|
* Never throws for a failed Run — the exit code and error tail are part of its
|
|
50
55
|
* outcome; callers choose how that failure reaches the Host Session.
|
|
51
56
|
*/
|
|
52
57
|
export function startRun(options: StartRunOptions): RunHandle {
|
|
53
|
-
const argv =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
const argv = runArgv({
|
|
59
|
+
program: options.program,
|
|
60
|
+
...(options.args === undefined ? {} : { args: options.args }),
|
|
61
|
+
...(options.record === undefined ? {} : { record: options.record }),
|
|
62
|
+
...(options.resume === undefined ? {} : { resume: options.resume }),
|
|
63
|
+
});
|
|
64
|
+
const child = startCliChild({
|
|
65
|
+
bun: options.bun,
|
|
66
|
+
cli: options.cli,
|
|
67
|
+
argv,
|
|
68
|
+
events: true,
|
|
69
|
+
...(options.program.kind === "inline" ? { evalSource: options.program.source } : {}),
|
|
70
|
+
});
|
|
58
71
|
let summary: RunSummary = initialSummary();
|
|
59
72
|
let sequence = 0;
|
|
60
73
|
const outcome = Promise.all([
|
package/src/status-tool.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AgentToolResult, ToolDefinition } from "@earendil-works/pi-coding-
|
|
|
2
2
|
import type { RunSummary } from "@yaag/runtime";
|
|
3
3
|
import { renderSnapshot, TreeState } from "@yaag/tui";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
-
import type {
|
|
5
|
+
import type { RunRecord } from "./run-record.ts";
|
|
6
6
|
import type { RegisteredRun, RestoredRun, RunRegistry } from "./run-registry.ts";
|
|
7
7
|
import { toUsage } from "./usage.ts";
|
|
8
8
|
|
|
@@ -35,6 +35,9 @@ const DESCRIPTION = [
|
|
|
35
35
|
"",
|
|
36
36
|
"Without an id, lists this session's Runs plus every orphaned or interrupted",
|
|
37
37
|
"Run left by an earlier session.",
|
|
38
|
+
"",
|
|
39
|
+
"An interrupted inline Run resumes with `resume` alone: yaag reads the program",
|
|
40
|
+
"source from its own Run record.",
|
|
38
41
|
].join("\n");
|
|
39
42
|
|
|
40
43
|
/**
|
|
@@ -114,20 +117,40 @@ function restoredDetails(run: RestoredRun): SnapshotDetails {
|
|
|
114
117
|
record.state === "orphaned" && record.process !== null
|
|
115
118
|
? ` Its process ${record.process.pid} is still running; stop it with yaag_stop.`
|
|
116
119
|
: "";
|
|
117
|
-
const detail = `${alive}${resumeHint(record
|
|
120
|
+
const detail = `${alive}${resumeHint(record)}`;
|
|
118
121
|
const result = record.outcome?.kind === "fulfilled" ? `\n${record.outcome.result}` : "";
|
|
119
122
|
return { id: record.id, summary: run.summary, result: `${state}${detail}${result}` };
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
/**
|
|
123
|
-
* Names the Checkpoint an unowned Run left behind (ADR-0031)
|
|
124
|
-
*
|
|
125
|
-
* one, for a dead Run and for a still-running
|
|
126
|
+
* Names the Checkpoint an unowned Run left behind (ADR-0031), and the call that
|
|
127
|
+
* resumes it. The Run republishes the Checkpoint at each Ask boundary, so it
|
|
128
|
+
* exists whenever the Run reached one, for a dead Run and for a still-running
|
|
129
|
+
* orphan alike.
|
|
130
|
+
*
|
|
131
|
+
* The hint names the Checkpoint path and never the stored program source: the
|
|
132
|
+
* path is enough, because yaag reads the source back from the Run record
|
|
133
|
+
* (ADR-0033). A whole source here would flood the transcript and repeat a
|
|
134
|
+
* private text the record already holds.
|
|
126
135
|
*/
|
|
127
|
-
function resumeHint(
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
|
|
136
|
+
function resumeHint(record: RunRecord): string {
|
|
137
|
+
const artifact = record.summary.artifact;
|
|
138
|
+
if (artifact === null || artifact === undefined) return "";
|
|
139
|
+
if (record.state !== "interrupted" && record.state !== "orphaned") return "";
|
|
140
|
+
// Each path becomes a JSON string literal: a path can hold a quote, a
|
|
141
|
+
// backslash, or a newline, and the hint must stay a call the model can copy.
|
|
142
|
+
const resume = JSON.stringify(artifact);
|
|
143
|
+
const { launch } = record;
|
|
144
|
+
switch (launch.kind) {
|
|
145
|
+
case "inline":
|
|
146
|
+
return ` Resume it with yaag_run({ resume: ${resume} }) and no file and no script; yaag reuses the program source it stored.`;
|
|
147
|
+
case "file":
|
|
148
|
+
return ` Resume it with yaag_run({ file: ${JSON.stringify(launch.file)}, resume: ${resume} }).`;
|
|
149
|
+
default: {
|
|
150
|
+
const never: never = launch;
|
|
151
|
+
return never;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
function snapshot(
|
package/src/test-tui-context.ts
CHANGED
|
@@ -7,12 +7,7 @@
|
|
|
7
7
|
* covers the rpc/json path, where every `ui` access must throw.
|
|
8
8
|
*/
|
|
9
9
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
11
|
-
FakeExtensionUi,
|
|
12
|
-
type Notice,
|
|
13
|
-
type OpenedComponent,
|
|
14
|
-
type OpenedEditor,
|
|
15
|
-
} from "./fake-extension-ui.ts";
|
|
10
|
+
import { FakeExtensionUi, type Notice, type OpenedComponent } from "./fake-extension-ui.ts";
|
|
16
11
|
|
|
17
12
|
export type { OpenedComponent } from "./fake-extension-ui.ts";
|
|
18
13
|
|
|
@@ -22,9 +17,9 @@ export interface TestTuiContextOptions {
|
|
|
22
17
|
}
|
|
23
18
|
|
|
24
19
|
/**
|
|
25
|
-
* A tui context whose UI is `FakeExtensionUi`: `custom
|
|
26
|
-
*
|
|
27
|
-
*
|
|
20
|
+
* A tui context whose UI is `FakeExtensionUi`: `custom` and `notify` work, and
|
|
21
|
+
* every other capability throws on access, so an accidental dependency fails
|
|
22
|
+
* loudly. `ui.editor` throws with the rest (ADR-0034).
|
|
28
23
|
*/
|
|
29
24
|
export class TestTuiContext implements ExtensionContext {
|
|
30
25
|
readonly mode: ExtensionContext["mode"] = "tui";
|
|
@@ -53,11 +48,6 @@ export class TestTuiContext implements ExtensionContext {
|
|
|
53
48
|
return this.ui.notices;
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
/** Editors the view opened. */
|
|
57
|
-
get edits(): readonly OpenedEditor[] {
|
|
58
|
-
return this.ui.edits;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
51
|
/** The latest inline widget frame per key. */
|
|
62
52
|
get widgets(): ReadonlyMap<string, string[] | undefined> {
|
|
63
53
|
return this.ui.widgets;
|
package/src/yaag-command.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface YaagCommandOptions {
|
|
|
33
33
|
/** Everything `/yaag` needs from its pi context, and nothing more. */
|
|
34
34
|
export interface YaagContext {
|
|
35
35
|
readonly mode: ExtensionContext["mode"];
|
|
36
|
-
readonly ui: Pick<ExtensionUIContext, "notify" | "select" | "custom"
|
|
36
|
+
readonly ui: Pick<ExtensionUIContext, "notify" | "select" | "custom">;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/** Builds the `/yaag` command bound to this session's registry and projections. */
|