@loomweaver/mcp 0.7.5 → 0.7.7
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/dist/main.mjs +701 -136
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -31006,7 +31006,7 @@ function isUnsafePath(path) {
|
|
|
31006
31006
|
}
|
|
31007
31007
|
function generate(recipe, input) {
|
|
31008
31008
|
const files = recipe.build(input);
|
|
31009
|
-
const unsafe = Object.keys(files).find(isUnsafePath);
|
|
31009
|
+
const unsafe = Object.keys(files).find((path) => isUnsafePath(path));
|
|
31010
31010
|
if (unsafe !== void 0) {
|
|
31011
31011
|
throw new Error(`Recipe "${recipe.id}" produced an unsafe path: "${unsafe}".`);
|
|
31012
31012
|
}
|
|
@@ -31016,11 +31016,24 @@ function amendments(recipe, input) {
|
|
|
31016
31016
|
return recipe.amend?.(input) ?? [];
|
|
31017
31017
|
}
|
|
31018
31018
|
|
|
31019
|
+
// ../devkit/src/lib/amend/merge.ts
|
|
31020
|
+
function normalizeProjectRoot(value) {
|
|
31021
|
+
const rooted = value.replace(/^\.?\/*/, "");
|
|
31022
|
+
let end = rooted.length;
|
|
31023
|
+
while (end > 0 && rooted[end - 1] === "/") {
|
|
31024
|
+
end -= 1;
|
|
31025
|
+
}
|
|
31026
|
+
return rooted.slice(0, end);
|
|
31027
|
+
}
|
|
31028
|
+
|
|
31019
31029
|
// ../devkit/src/lib/amend/describe.ts
|
|
31020
31030
|
function describeAmendment(amendment) {
|
|
31021
31031
|
if (amendment.kind === "postcss") {
|
|
31022
31032
|
return `Write ${amendment.file} beside your package.json, naming ${amendment.plugin}. Without it the stylesheet is read as plain CSS: no utility class is emitted, the workbench renders unstyled, and the build still reports success.`;
|
|
31023
31033
|
}
|
|
31034
|
+
if (amendment.kind === "package") {
|
|
31035
|
+
return `Add ${amendment.name}@${amendment.version} to your project's dependencies and install it. Without it the generated files import a package that is not there, so the very first build fails.`;
|
|
31036
|
+
}
|
|
31024
31037
|
if (amendment.kind === "stylesheet-source") {
|
|
31025
31038
|
return `Add an @source entry for '${amendment.sourceRoot}' to the application's entry stylesheet, resolved from that stylesheet. Without it none of that code's utilities are emitted.`;
|
|
31026
31039
|
}
|
|
@@ -31032,7 +31045,9 @@ function describeAmendment(amendment) {
|
|
|
31032
31045
|
return [
|
|
31033
31046
|
...amendment.styles.length > 0 ? [`name ${amendment.styles.join(", ")} in styles`] : [],
|
|
31034
31047
|
...amendment.assets.length > 0 ? [
|
|
31035
|
-
`add assets for ${amendment.assets.map((asset) => asset.input).join(
|
|
31048
|
+
`add assets for ${amendment.assets.map((asset) => asset.input).join(
|
|
31049
|
+
", "
|
|
31050
|
+
)} (the shell fetches its own strings at runtime, so without that glob every label in the chrome renders as its raw translation key)`
|
|
31036
31051
|
] : [],
|
|
31037
31052
|
...amendment.serviceWorker ? [
|
|
31038
31053
|
`set serviceWorker to ${amendment.serviceWorker} in the production configuration (provideShell registers a worker that 404s otherwise)`
|
|
@@ -31131,6 +31146,589 @@ function validateCapabilities(capabilities, known) {
|
|
|
31131
31146
|
return findings;
|
|
31132
31147
|
}
|
|
31133
31148
|
|
|
31149
|
+
// ../devkit/src/recipes/angular-weaver/agent-panel.ts
|
|
31150
|
+
function panelFile(w) {
|
|
31151
|
+
return `import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
|
31152
|
+
import { EventType, type BaseEvent, type Tool } from '@ag-ui/core';
|
|
31153
|
+
import { ${w.propertyName}Agent } from './${w.id}-agent';
|
|
31154
|
+
import { askAgent } from './${w.id}-agent-source';
|
|
31155
|
+
|
|
31156
|
+
interface Line {
|
|
31157
|
+
readonly kind: 'you' | 'agent' | 'call' | 'result' | 'note';
|
|
31158
|
+
readonly text: string;
|
|
31159
|
+
readonly args?: string;
|
|
31160
|
+
readonly failed?: boolean;
|
|
31161
|
+
}
|
|
31162
|
+
|
|
31163
|
+
@Component({
|
|
31164
|
+
selector: '${w.prefix}-${w.id}-agent-panel',
|
|
31165
|
+
templateUrl: './${w.id}-agent-panel.html',
|
|
31166
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
31167
|
+
})
|
|
31168
|
+
export class ${w.className}AgentPanel {
|
|
31169
|
+
protected readonly offered = signal<readonly Tool[]>([]);
|
|
31170
|
+
|
|
31171
|
+
protected readonly lines = signal<readonly Line[]>([]);
|
|
31172
|
+
|
|
31173
|
+
protected readonly busy = signal(false);
|
|
31174
|
+
|
|
31175
|
+
private runs = 0;
|
|
31176
|
+
|
|
31177
|
+
constructor() {
|
|
31178
|
+
this.offered.set(${w.propertyName}Agent()?.list() ?? []);
|
|
31179
|
+
}
|
|
31180
|
+
|
|
31181
|
+
protected async ask(name: string): Promise<void> {
|
|
31182
|
+
const tools = ${w.propertyName}Agent();
|
|
31183
|
+
if (!tools || this.busy()) {
|
|
31184
|
+
return;
|
|
31185
|
+
}
|
|
31186
|
+
this.busy.set(true);
|
|
31187
|
+
try {
|
|
31188
|
+
// Ask for the list again, every run. What a plugin may reach changes as plugins load and the
|
|
31189
|
+
// session changes, and a list kept from earlier offers actions that are no longer there.
|
|
31190
|
+
const offered = tools.list();
|
|
31191
|
+
this.offered.set(offered);
|
|
31192
|
+
this.push({ kind: 'you', text: \`Run \${name}\` });
|
|
31193
|
+
this.push({ kind: 'note', text: \`\${offered.length} tool(s) offered right now\` });
|
|
31194
|
+
|
|
31195
|
+
const request = {
|
|
31196
|
+
runId: \`run-\${++this.runs}\`,
|
|
31197
|
+
prompt: name,
|
|
31198
|
+
tools: offered,
|
|
31199
|
+
};
|
|
31200
|
+
for await (const event of askAgent(request)) {
|
|
31201
|
+
this.draw(event);
|
|
31202
|
+
// Every event goes over, unfiltered. The adapter decides which ones matter; a filter here is
|
|
31203
|
+
// how a call ends up half-assembled.
|
|
31204
|
+
const message = await tools.receive(event);
|
|
31205
|
+
if (message) {
|
|
31206
|
+
this.push({
|
|
31207
|
+
kind: 'result',
|
|
31208
|
+
text: message.error ?? message.content,
|
|
31209
|
+
failed: Boolean(message.error),
|
|
31210
|
+
});
|
|
31211
|
+
}
|
|
31212
|
+
}
|
|
31213
|
+
// A run that ends without its closing event leaves a call open; flush() answers it.
|
|
31214
|
+
const last = await tools.flush();
|
|
31215
|
+
if (last) {
|
|
31216
|
+
this.push({
|
|
31217
|
+
kind: 'result',
|
|
31218
|
+
text: last.error ?? last.content,
|
|
31219
|
+
failed: Boolean(last.error),
|
|
31220
|
+
});
|
|
31221
|
+
}
|
|
31222
|
+
} finally {
|
|
31223
|
+
this.busy.set(false);
|
|
31224
|
+
}
|
|
31225
|
+
}
|
|
31226
|
+
|
|
31227
|
+
private draw(event: BaseEvent): void {
|
|
31228
|
+
const raw = event as unknown as Record<string, unknown>;
|
|
31229
|
+
switch (event.type) {
|
|
31230
|
+
case EventType.TEXT_MESSAGE_START:
|
|
31231
|
+
this.push({ kind: 'agent', text: '' });
|
|
31232
|
+
break;
|
|
31233
|
+
case EventType.TEXT_MESSAGE_CONTENT:
|
|
31234
|
+
this.grow('text', String(raw['delta'] ?? ''));
|
|
31235
|
+
break;
|
|
31236
|
+
case EventType.TOOL_CALL_START:
|
|
31237
|
+
this.push({
|
|
31238
|
+
kind: 'call',
|
|
31239
|
+
text: String(raw['toolCallName'] ?? ''),
|
|
31240
|
+
args: '',
|
|
31241
|
+
});
|
|
31242
|
+
break;
|
|
31243
|
+
case EventType.TOOL_CALL_ARGS:
|
|
31244
|
+
this.grow('args', String(raw['delta'] ?? ''));
|
|
31245
|
+
break;
|
|
31246
|
+
default:
|
|
31247
|
+
break;
|
|
31248
|
+
}
|
|
31249
|
+
}
|
|
31250
|
+
|
|
31251
|
+
private push(line: Line): void {
|
|
31252
|
+
this.lines.update((all) => [...all, line]);
|
|
31253
|
+
}
|
|
31254
|
+
|
|
31255
|
+
private grow(field: 'text' | 'args', delta: string): void {
|
|
31256
|
+
this.lines.update((all) => {
|
|
31257
|
+
const last = all[all.length - 1];
|
|
31258
|
+
return [
|
|
31259
|
+
...all.slice(0, -1),
|
|
31260
|
+
{ ...last, [field]: \`\${last[field] ?? ''}\${delta}\` },
|
|
31261
|
+
];
|
|
31262
|
+
});
|
|
31263
|
+
}
|
|
31264
|
+
}
|
|
31265
|
+
`;
|
|
31266
|
+
}
|
|
31267
|
+
function panelTemplateFile(w) {
|
|
31268
|
+
return `<div class="flex h-full flex-col gap-3">
|
|
31269
|
+
<p
|
|
31270
|
+
class="shrink-0 rounded-md border border-border bg-surface-raised px-3 py-2 text-xs text-content-muted"
|
|
31271
|
+
>
|
|
31272
|
+
This is a stand-in, not an assistant. It speaks the protocol so you can watch the whole path;
|
|
31273
|
+
replace <code class="text-content">${w.id}-agent-source.ts</code> with your own transport.
|
|
31274
|
+
</p>
|
|
31275
|
+
|
|
31276
|
+
<div class="min-h-0 flex-1 overflow-auto">
|
|
31277
|
+
<ul class="flex flex-col gap-2.5">
|
|
31278
|
+
@for (line of lines(); track $index) {
|
|
31279
|
+
@if (line.kind === 'you') {
|
|
31280
|
+
<li class="max-w-full self-end rounded-lg bg-brand-fill px-3 py-2 text-sm text-on-brand">
|
|
31281
|
+
{{ line.text }}
|
|
31282
|
+
</li>
|
|
31283
|
+
} @else if (line.kind === 'agent') {
|
|
31284
|
+
<li
|
|
31285
|
+
class="max-w-full self-start rounded-lg bg-surface-raised px-3 py-2 text-sm text-content"
|
|
31286
|
+
>
|
|
31287
|
+
{{ line.text }}
|
|
31288
|
+
</li>
|
|
31289
|
+
} @else if (line.kind === 'call') {
|
|
31290
|
+
<li
|
|
31291
|
+
class="rounded-md border border-border px-3 py-2 font-mono text-xs break-all text-content-muted"
|
|
31292
|
+
>
|
|
31293
|
+
{{ line.text }}<span class="text-content-faint">({{ line.args }})</span>
|
|
31294
|
+
</li>
|
|
31295
|
+
} @else if (line.kind === 'result') {
|
|
31296
|
+
<li
|
|
31297
|
+
class="px-1 text-xs"
|
|
31298
|
+
[class.text-negative]="line.failed"
|
|
31299
|
+
[class.text-content-muted]="!line.failed"
|
|
31300
|
+
>
|
|
31301
|
+
{{ line.text }}
|
|
31302
|
+
</li>
|
|
31303
|
+
} @else {
|
|
31304
|
+
<li class="px-1 text-xs text-content-faint">{{ line.text }}</li>
|
|
31305
|
+
}
|
|
31306
|
+
} @empty {
|
|
31307
|
+
<li class="px-1 py-6 text-center text-sm text-content-muted">
|
|
31308
|
+
Pick something below and watch the call go through.
|
|
31309
|
+
</li>
|
|
31310
|
+
}
|
|
31311
|
+
</ul>
|
|
31312
|
+
</div>
|
|
31313
|
+
|
|
31314
|
+
<div class="flex shrink-0 flex-col gap-2 border-t border-border pt-3">
|
|
31315
|
+
<span class="px-1 text-xs font-medium text-content-muted">
|
|
31316
|
+
Offered right now ({{ offered().length }})
|
|
31317
|
+
</span>
|
|
31318
|
+
@for (tool of offered(); track tool.name) {
|
|
31319
|
+
<button
|
|
31320
|
+
type="button"
|
|
31321
|
+
class="lw-btn lw-btn--default lw-btn--sm h-auto justify-start py-2 text-left whitespace-normal"
|
|
31322
|
+
[disabled]="busy()"
|
|
31323
|
+
(click)="ask(tool.name)"
|
|
31324
|
+
>
|
|
31325
|
+
{{ tool.description }}
|
|
31326
|
+
</button>
|
|
31327
|
+
} @empty {
|
|
31328
|
+
<span class="px-1 text-xs text-content-faint">
|
|
31329
|
+
Nothing is offered. A command reaches this list only when it is registered with
|
|
31330
|
+
<code class="text-content">callable: true</code>, and reaching another plugin's commands
|
|
31331
|
+
needs the <code class="text-content">automation</code> capability granted.
|
|
31332
|
+
</span>
|
|
31333
|
+
}
|
|
31334
|
+
</div>
|
|
31335
|
+
</div>
|
|
31336
|
+
`;
|
|
31337
|
+
}
|
|
31338
|
+
|
|
31339
|
+
// ../devkit/src/recipes/angular-weaver/agent-stand-in.ts
|
|
31340
|
+
function standInFile(w) {
|
|
31341
|
+
return `import { EventType, type BaseEvent, type Tool } from '@ag-ui/core';
|
|
31342
|
+
|
|
31343
|
+
// WHAT THIS IS: a stand-in for an agent, and not an agent. It speaks the AG-UI protocol and nothing
|
|
31344
|
+
// else \u2014 no model, no network, no judgement \u2014 so that the whole path from the offered tools through
|
|
31345
|
+
// a call to its outcome runs on the first serve, before you have connected anything.
|
|
31346
|
+
//
|
|
31347
|
+
// WHAT REPLACES IT: this file, and only this file. Point askAgent at your own endpoint and yield the
|
|
31348
|
+
// events it streams back. The panel and the connection beside it stay exactly as they are.
|
|
31349
|
+
|
|
31350
|
+
const PACE = 40;
|
|
31351
|
+
|
|
31352
|
+
export interface AgentRequest {
|
|
31353
|
+
readonly runId: string;
|
|
31354
|
+
readonly prompt: string;
|
|
31355
|
+
/** What the workbench offers right now \u2014 the same list a real agent would be handed. */
|
|
31356
|
+
readonly tools: readonly Tool[];
|
|
31357
|
+
}
|
|
31358
|
+
|
|
31359
|
+
export async function* askAgent(
|
|
31360
|
+
request: AgentRequest,
|
|
31361
|
+
): AsyncGenerator<BaseEvent> {
|
|
31362
|
+
const picked = request.tools.find((tool) =>
|
|
31363
|
+
request.prompt.includes(tool.name),
|
|
31364
|
+
);
|
|
31365
|
+
|
|
31366
|
+
yield event(EventType.RUN_STARTED, {
|
|
31367
|
+
threadId: '${w.id}-stand-in',
|
|
31368
|
+
runId: request.runId,
|
|
31369
|
+
});
|
|
31370
|
+
yield* speak(
|
|
31371
|
+
\`\${request.runId}.says\`,
|
|
31372
|
+
picked
|
|
31373
|
+
? \`You asked for "\${request.prompt}". I can see \${request.tools.length} tool(s) and \${picked.name} is one of them, so I will call it.\`
|
|
31374
|
+
: \`You asked for "\${request.prompt}", and nothing among the \${request.tools.length} tool(s) I was offered matches it, so I will not call anything.\`,
|
|
31375
|
+
);
|
|
31376
|
+
|
|
31377
|
+
if (picked) {
|
|
31378
|
+
const toolCallId = \`\${request.runId}.call\`;
|
|
31379
|
+
yield event(EventType.TOOL_CALL_START, {
|
|
31380
|
+
toolCallId,
|
|
31381
|
+
toolCallName: picked.name,
|
|
31382
|
+
});
|
|
31383
|
+
// Arguments arrive in pieces, exactly as they do from a real model. The adapter assembles them;
|
|
31384
|
+
// nothing downstream ever sees a half-written call. A real agent fills them from the JSON Schema
|
|
31385
|
+
// the workbench described in picked.parameters; a stand-in has nothing to fill them from, so it
|
|
31386
|
+
// sends none and lets the command apply its own defaults.
|
|
31387
|
+
for (const piece of chunks(JSON.stringify({}), 4)) {
|
|
31388
|
+
yield event(EventType.TOOL_CALL_ARGS, { toolCallId, delta: piece });
|
|
31389
|
+
await pause();
|
|
31390
|
+
}
|
|
31391
|
+
yield event(EventType.TOOL_CALL_END, { toolCallId });
|
|
31392
|
+
}
|
|
31393
|
+
|
|
31394
|
+
yield event(EventType.RUN_FINISHED, {
|
|
31395
|
+
threadId: '${w.id}-stand-in',
|
|
31396
|
+
runId: request.runId,
|
|
31397
|
+
});
|
|
31398
|
+
}
|
|
31399
|
+
|
|
31400
|
+
async function* speak(
|
|
31401
|
+
messageId: string,
|
|
31402
|
+
text: string,
|
|
31403
|
+
): AsyncGenerator<BaseEvent> {
|
|
31404
|
+
yield event(EventType.TEXT_MESSAGE_START, { messageId, role: 'assistant' });
|
|
31405
|
+
for (const piece of chunks(text, 10)) {
|
|
31406
|
+
yield event(EventType.TEXT_MESSAGE_CONTENT, { messageId, delta: piece });
|
|
31407
|
+
await pause();
|
|
31408
|
+
}
|
|
31409
|
+
yield event(EventType.TEXT_MESSAGE_END, { messageId });
|
|
31410
|
+
}
|
|
31411
|
+
|
|
31412
|
+
function chunks(text: string, size: number): readonly string[] {
|
|
31413
|
+
const pieces: string[] = [];
|
|
31414
|
+
for (let at = 0; at < text.length; at += size) {
|
|
31415
|
+
pieces.push(text.slice(at, at + size));
|
|
31416
|
+
}
|
|
31417
|
+
return pieces;
|
|
31418
|
+
}
|
|
31419
|
+
|
|
31420
|
+
function event(type: EventType, fields: Record<string, unknown>): BaseEvent {
|
|
31421
|
+
return { type, ...fields } as unknown as BaseEvent;
|
|
31422
|
+
}
|
|
31423
|
+
|
|
31424
|
+
function pause(): Promise<void> {
|
|
31425
|
+
return new Promise((done) => setTimeout(done, PACE));
|
|
31426
|
+
}
|
|
31427
|
+
`;
|
|
31428
|
+
}
|
|
31429
|
+
|
|
31430
|
+
// ../devkit/src/recipes/angular-weaver/agent-files.ts
|
|
31431
|
+
var AG_UI_ADAPTER_VERSION = "0.7.7";
|
|
31432
|
+
var AG_UI_PROTOCOL_VERSION = "0.0.x";
|
|
31433
|
+
function connectionFile(w) {
|
|
31434
|
+
return `import { signal } from '@angular/core';
|
|
31435
|
+
import {
|
|
31436
|
+
commandTools,
|
|
31437
|
+
type CommandTools,
|
|
31438
|
+
type PendingToolCall,
|
|
31439
|
+
type ToolDecision,
|
|
31440
|
+
} from '@loomweaver/ag-ui';
|
|
31441
|
+
import type { PluginContext } from '@loomweaver/plugin-sdk';
|
|
31442
|
+
|
|
31443
|
+
// Commands an agent may not run on its own word. Each one asks the person at the keyboard first, and
|
|
31444
|
+
// declining stops it: the workbench never sees the call. This weaver's own command is listed as an
|
|
31445
|
+
// example \u2014 replace it with the ones that actually cost something.
|
|
31446
|
+
const CONSEQUENTIAL = new Set(['${w.id}.hello']);
|
|
31447
|
+
|
|
31448
|
+
// A factory, not a module-level connection: everything a run needs lives in the closure, so a second
|
|
31449
|
+
// one never shares state with the first.
|
|
31450
|
+
export function ${w.propertyName}Connection(ctx: PluginContext): CommandTools {
|
|
31451
|
+
return commandTools(ctx, { before: (call) => decide(ctx, call) });
|
|
31452
|
+
}
|
|
31453
|
+
|
|
31454
|
+
// The one connection this plugin activates, published for its panel. Set in activate(), cleared in
|
|
31455
|
+
// deactivate(), so the panel renders an honest empty state either side of that.
|
|
31456
|
+
export const ${w.propertyName}Agent = signal<CommandTools | null>(null);
|
|
31457
|
+
|
|
31458
|
+
async function decide(
|
|
31459
|
+
ctx: PluginContext,
|
|
31460
|
+
call: PendingToolCall,
|
|
31461
|
+
): Promise<ToolDecision> {
|
|
31462
|
+
if (!CONSEQUENTIAL.has(call.commandId)) {
|
|
31463
|
+
return { decision: 'run' };
|
|
31464
|
+
}
|
|
31465
|
+
const yes = await ctx.ui.confirm({
|
|
31466
|
+
title: '${w.id}.agent.confirm.title',
|
|
31467
|
+
message: '${w.id}.agent.confirm.message',
|
|
31468
|
+
confirmLabel: '${w.id}.agent.confirm.yes',
|
|
31469
|
+
cancelLabel: '${w.id}.agent.confirm.no',
|
|
31470
|
+
tone: 'warning',
|
|
31471
|
+
});
|
|
31472
|
+
// A decision can only narrow. Letting a call through does not make it reachable: the workbench
|
|
31473
|
+
// still refuses whatever it always refused.
|
|
31474
|
+
return yes
|
|
31475
|
+
? { decision: 'run' }
|
|
31476
|
+
: { decision: 'decline', reason: 'the person at the keyboard said no.' };
|
|
31477
|
+
}
|
|
31478
|
+
`;
|
|
31479
|
+
}
|
|
31480
|
+
function specFile(w) {
|
|
31481
|
+
return `import { EventType, type BaseEvent } from '@ag-ui/core';
|
|
31482
|
+
import type { CommandArguments, PluginContext } from '@loomweaver/plugin-sdk';
|
|
31483
|
+
import { ${w.propertyName}Connection } from './${w.id}-agent';
|
|
31484
|
+
|
|
31485
|
+
interface Asked {
|
|
31486
|
+
readonly id: string;
|
|
31487
|
+
readonly args?: CommandArguments;
|
|
31488
|
+
}
|
|
31489
|
+
|
|
31490
|
+
function contextThat(confirms: boolean, ran: Asked[]): PluginContext {
|
|
31491
|
+
return {
|
|
31492
|
+
invocableCommands: () => [
|
|
31493
|
+
{ id: '${w.id}.hello', title: '${w.name} action', description: 'Shows a short message.' },
|
|
31494
|
+
],
|
|
31495
|
+
invokeCommand: (id: string, args?: CommandArguments) => {
|
|
31496
|
+
ran.push({ id, args });
|
|
31497
|
+
return Promise.resolve({ outcome: 'answered', value: 'it ran' });
|
|
31498
|
+
},
|
|
31499
|
+
ui: { confirm: () => Promise.resolve(confirms) },
|
|
31500
|
+
} as unknown as PluginContext;
|
|
31501
|
+
}
|
|
31502
|
+
|
|
31503
|
+
function event(type: EventType, fields: Record<string, unknown>): BaseEvent {
|
|
31504
|
+
return { type, ...fields } as unknown as BaseEvent;
|
|
31505
|
+
}
|
|
31506
|
+
|
|
31507
|
+
describe('${w.propertyName}Connection', () => {
|
|
31508
|
+
it('offers what the workbench offers', () => {
|
|
31509
|
+
const tools = ${w.propertyName}Connection(contextThat(true, []));
|
|
31510
|
+
expect(tools.list().map((tool) => tool.name)).toEqual(['${w.id}.hello']);
|
|
31511
|
+
});
|
|
31512
|
+
|
|
31513
|
+
it('assembles a call from its events and answers with the outcome', async () => {
|
|
31514
|
+
const ran: Asked[] = [];
|
|
31515
|
+
const tools = ${w.propertyName}Connection(contextThat(true, ran));
|
|
31516
|
+
|
|
31517
|
+
expect(
|
|
31518
|
+
await tools.receive(
|
|
31519
|
+
event(EventType.TOOL_CALL_START, {
|
|
31520
|
+
toolCallId: 'c1',
|
|
31521
|
+
toolCallName: '${w.id}.hello',
|
|
31522
|
+
}),
|
|
31523
|
+
),
|
|
31524
|
+
).toBeNull();
|
|
31525
|
+
await tools.receive(
|
|
31526
|
+
event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: '{"who"' }),
|
|
31527
|
+
);
|
|
31528
|
+
await tools.receive(
|
|
31529
|
+
event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: ':"you"}' }),
|
|
31530
|
+
);
|
|
31531
|
+
const answer = await tools.receive(
|
|
31532
|
+
event(EventType.TOOL_CALL_END, { toolCallId: 'c1' }),
|
|
31533
|
+
);
|
|
31534
|
+
|
|
31535
|
+
expect(ran).toEqual([{ id: '${w.id}.hello', args: { who: 'you' } }]);
|
|
31536
|
+
expect(answer?.content).toBe('it ran');
|
|
31537
|
+
expect(answer?.error).toBeUndefined();
|
|
31538
|
+
});
|
|
31539
|
+
|
|
31540
|
+
it('never reaches the workbench when a consequential call is declined', async () => {
|
|
31541
|
+
const ran: Asked[] = [];
|
|
31542
|
+
const tools = ${w.propertyName}Connection(contextThat(false, ran));
|
|
31543
|
+
|
|
31544
|
+
await tools.receive(
|
|
31545
|
+
event(EventType.TOOL_CALL_START, {
|
|
31546
|
+
toolCallId: 'c2',
|
|
31547
|
+
toolCallName: '${w.id}.hello',
|
|
31548
|
+
}),
|
|
31549
|
+
);
|
|
31550
|
+
await tools.receive(
|
|
31551
|
+
event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c2', delta: '{}' }),
|
|
31552
|
+
);
|
|
31553
|
+
const answer = await tools.receive(
|
|
31554
|
+
event(EventType.TOOL_CALL_END, { toolCallId: 'c2' }),
|
|
31555
|
+
);
|
|
31556
|
+
|
|
31557
|
+
expect(ran).toEqual([]);
|
|
31558
|
+
expect(answer?.error).toContain('did not run');
|
|
31559
|
+
});
|
|
31560
|
+
});
|
|
31561
|
+
`;
|
|
31562
|
+
}
|
|
31563
|
+
function agentSurfaceBlock(w) {
|
|
31564
|
+
return [
|
|
31565
|
+
" ctx.registerSurface({",
|
|
31566
|
+
` id: '${w.id}.agent',`,
|
|
31567
|
+
` title: '${w.id}.agent.title',`,
|
|
31568
|
+
` icon: '${w.id}',`,
|
|
31569
|
+
" docks: ['right-panel'],",
|
|
31570
|
+
` component: ${w.className}AgentPanel,`,
|
|
31571
|
+
" });"
|
|
31572
|
+
].join("\n");
|
|
31573
|
+
}
|
|
31574
|
+
function agentFiles(w) {
|
|
31575
|
+
const files = {
|
|
31576
|
+
[`src/lib/agent/${w.id}-agent.ts`]: connectionFile(w),
|
|
31577
|
+
[`src/lib/agent/${w.id}-agent-source.ts`]: standInFile(w),
|
|
31578
|
+
[`src/lib/agent/${w.id}-agent-panel.ts`]: panelFile(w),
|
|
31579
|
+
[`src/lib/agent/${w.id}-agent-panel.html`]: panelTemplateFile(w)
|
|
31580
|
+
};
|
|
31581
|
+
if (w.features.spec) {
|
|
31582
|
+
files[`src/lib/agent/${w.id}-agent.spec.ts`] = specFile(w);
|
|
31583
|
+
}
|
|
31584
|
+
return files;
|
|
31585
|
+
}
|
|
31586
|
+
|
|
31587
|
+
// ../devkit/src/recipes/angular-weaver/weaver-terms.ts
|
|
31588
|
+
var CONTAINER_EXAMPLE_ID = "example";
|
|
31589
|
+
function capabilityItems(capabilities) {
|
|
31590
|
+
return capabilities.map((capability) => `'${capability}'`).join(", ");
|
|
31591
|
+
}
|
|
31592
|
+
|
|
31593
|
+
// ../devkit/src/recipes/angular-weaver/weaver-readme.ts
|
|
31594
|
+
function surfaceNotes(w) {
|
|
31595
|
+
const railNote = "Rail and bar items reference region ids (`primary`, `status`) that must exist in your layout.";
|
|
31596
|
+
if (w.features.container) {
|
|
31597
|
+
return [
|
|
31598
|
+
`The surface is a **container**: it is routable at \`/${w.id}/:id\`, and its tab holds a`,
|
|
31599
|
+
"nested pane tree of child surfaces. The host draws the inner tabs, splits and drag targets; this",
|
|
31600
|
+
"weaver only declares which children it offers.",
|
|
31601
|
+
"",
|
|
31602
|
+
`- \`children\` is what the inner "new tab" picker lists \u2014 the host access-gates it for you.`,
|
|
31603
|
+
"- `initial` is what a freshly opened container tab starts with.",
|
|
31604
|
+
`- The children declare \`docks: []\`. That is the container-only convention: they are never seeded`,
|
|
31605
|
+
" into a sidebar, they exist solely inside this container.",
|
|
31606
|
+
`- Each child reads the container's \`:id\` from an injected \`ActivatedRoute\` \u2014 the host supplies a`,
|
|
31607
|
+
" synthetic one, so a child needs no knowledge of where it is mounted. Two open container tabs are",
|
|
31608
|
+
" two independent trees, each scoped to its own id.",
|
|
31609
|
+
`- The inner tree is **sealed**: a child cannot be dragged out, and nothing can be dragged in. It`,
|
|
31610
|
+
" travels with the tab, including into a sidebar or a pop-out window.",
|
|
31611
|
+
"",
|
|
31612
|
+
`The rail item opens the fixed id \`${CONTAINER_EXAMPLE_ID}\`. Replace that with whatever the user`,
|
|
31613
|
+
"actually picked \u2014 a document, a run, a project.",
|
|
31614
|
+
"",
|
|
31615
|
+
railNote
|
|
31616
|
+
];
|
|
31617
|
+
}
|
|
31618
|
+
if (w.features.instanceable) {
|
|
31619
|
+
return [
|
|
31620
|
+
`The surface is **docked** into the \`primary\` region and marked \`instanceable\`, so the host shows a`,
|
|
31621
|
+
"switcher for saving, naming, renaming and deleting several configurations of it, each with its own",
|
|
31622
|
+
"`VIEW_STATE` blob.",
|
|
31623
|
+
"",
|
|
31624
|
+
"It is deliberately **not** routable. Named instances exist only for a docked surface \u2014 a routable",
|
|
31625
|
+
"one holds the URL pane instead, and the host drops `instanceable` on that path. The rail item",
|
|
31626
|
+
"therefore reveals the surface (`ctx.revealSurface`) rather than navigating to a URL, which focuses",
|
|
31627
|
+
"it wherever the user has since moved it.",
|
|
31628
|
+
"",
|
|
31629
|
+
"The generated view already uses that blob for its sort order, because a hidden surface is destroyed",
|
|
31630
|
+
"as soon as it is clean: state kept in a component field survives neither a tab switch nor",
|
|
31631
|
+
"a collapsed sidebar, and never survived a reload. The rule is *evictable = reload-safe* \u2014 anything",
|
|
31632
|
+
"that must not be lost goes through `VIEW_STATE`, and `set()` replaces the whole blob, so spread it.",
|
|
31633
|
+
"",
|
|
31634
|
+
railNote
|
|
31635
|
+
];
|
|
31636
|
+
}
|
|
31637
|
+
return [
|
|
31638
|
+
`The surface is routable at \`/${w.id}\`; ${railNote.charAt(0).toLowerCase()}${railNote.slice(1)}`,
|
|
31639
|
+
"",
|
|
31640
|
+
"A routable surface has **no `VIEW_STATE` handle** \u2014 injecting the token there throws. It owns a URL,",
|
|
31641
|
+
"so anything shareable (a filter, the active sub-tab) belongs in route params or `subRoutes`, where it",
|
|
31642
|
+
"survives a deep link too; unsaved edits are `DirtySurface`, and an instance that is expensive to",
|
|
31643
|
+
"rebuild declares `retain: 'always'`. Generate with `--instanceable` for the docked, `VIEW_STATE`",
|
|
31644
|
+
"flavour instead."
|
|
31645
|
+
];
|
|
31646
|
+
}
|
|
31647
|
+
function agentNotes(w) {
|
|
31648
|
+
return [
|
|
31649
|
+
"",
|
|
31650
|
+
"## The agent connection",
|
|
31651
|
+
"",
|
|
31652
|
+
`\`src/lib/agent/\` holds three files and one of them is meant to be thrown away.`,
|
|
31653
|
+
"",
|
|
31654
|
+
`- \`${w.id}-agent.ts\` is the connection: the workbench's own commands offered as tools, and a`,
|
|
31655
|
+
" seam where this weaver decides about a call before it runs. Nothing is registered twice \u2014 the",
|
|
31656
|
+
" list comes from the workbench, already narrowed by everything that would refuse the call.",
|
|
31657
|
+
`- \`${w.id}-agent-panel.ts\` shows what is offered, the call as it streams, and the outcome.`,
|
|
31658
|
+
`- \`${w.id}-agent-source.ts\` is a **stand-in**, not an assistant: it produces the protocol's own`,
|
|
31659
|
+
" events so the whole path runs before you have connected anything. Replace that one file with",
|
|
31660
|
+
" your transport and nothing else changes. No transport, credential or model is generated for",
|
|
31661
|
+
" you, because none of them can be guessed.",
|
|
31662
|
+
"",
|
|
31663
|
+
"Three things are easy to get wrong and invisible when they are, so the generated code does them",
|
|
31664
|
+
"rather than explaining them: the offered list is asked for again every run, every event is handed",
|
|
31665
|
+
"over unfiltered, and a decision before a call can only narrow what the workbench would have",
|
|
31666
|
+
"allowed anyway.",
|
|
31667
|
+
"",
|
|
31668
|
+
`The generated weaver needs two packages your project may not carry yet:`,
|
|
31669
|
+
"",
|
|
31670
|
+
"```bash",
|
|
31671
|
+
`npm i @loomweaver/ag-ui @ag-ui/core`,
|
|
31672
|
+
"```",
|
|
31673
|
+
"",
|
|
31674
|
+
"The Nx generator and the CLI record them for you; the MCP route names them instead."
|
|
31675
|
+
];
|
|
31676
|
+
}
|
|
31677
|
+
function readmeFile(w) {
|
|
31678
|
+
return [
|
|
31679
|
+
`# ${w.name} weaver`,
|
|
31680
|
+
"",
|
|
31681
|
+
`A LoomWeaver weaver (a domain plugin bundle). It consumes only the public \`@loomweaver/plugin-sdk\` contract.`,
|
|
31682
|
+
"",
|
|
31683
|
+
"## Wire it into a distribution",
|
|
31684
|
+
"",
|
|
31685
|
+
`1. Add the plugin to \`providePlugins\` in \`src/app/app.config.ts\`. It is **variadic** and`,
|
|
31686
|
+
` returns an array, so spread it:`,
|
|
31687
|
+
"",
|
|
31688
|
+
" ```ts",
|
|
31689
|
+
` import { ${w.propertyName}Plugin } from '${w.importPath}'; // Nx: the workspace alias; without one, a relative path to this library's src/index.ts`,
|
|
31690
|
+
` ...providePlugins(${w.propertyName}Plugin),`,
|
|
31691
|
+
" ```",
|
|
31692
|
+
"",
|
|
31693
|
+
`2. Grant its capabilities (default-deny) via \`provideCapabilityGrants\`:`,
|
|
31694
|
+
"",
|
|
31695
|
+
" ```ts",
|
|
31696
|
+
` provideCapabilityGrants({ '${w.id}': [${capabilityItems(w.capabilities)}] });`,
|
|
31697
|
+
" ```",
|
|
31698
|
+
"",
|
|
31699
|
+
`3. Compose its translations with \`provideTranslationNamespaces('${w.id}')\` \u2014 and serve the`,
|
|
31700
|
+
` bundle by adding an assets glob to your application's build target, so the loader can fetch`,
|
|
31701
|
+
` \`/i18n/${w.id}/<lang>.json\` (the Nx generator adds this glob for you):`,
|
|
31702
|
+
"",
|
|
31703
|
+
" ```json",
|
|
31704
|
+
` { "glob": "**/*.json", "input": "<path to this library>/src/lib/i18n", "output": "i18n/${w.id}" }`,
|
|
31705
|
+
" ```",
|
|
31706
|
+
"",
|
|
31707
|
+
`4. If your application compiles the shell's theme with Tailwind, name this library as a source`,
|
|
31708
|
+
` for it, so the utility classes in these templates are emitted. Tailwind also detects sources`,
|
|
31709
|
+
` by itself, but that depends on where it resolves the project root and on \`.gitignore\`, and`,
|
|
31710
|
+
` what the scaffold names covers the application alone (the Nx generator adds this line for`,
|
|
31711
|
+
` you). Applications scaffolded with \`--styles precompiled\` run no Tailwind and need nothing:`,
|
|
31712
|
+
"",
|
|
31713
|
+
" ```css",
|
|
31714
|
+
` @source '<path from that stylesheet to this library>/src';`,
|
|
31715
|
+
" ```",
|
|
31716
|
+
"",
|
|
31717
|
+
...surfaceNotes(w),
|
|
31718
|
+
...w.features.agent ? agentNotes(w) : [],
|
|
31719
|
+
"",
|
|
31720
|
+
"## After scaffolding",
|
|
31721
|
+
"",
|
|
31722
|
+
"- `src/lib/i18n/de.json` starts as a copy of the English strings \u2014 translate it.",
|
|
31723
|
+
`- A scaffolded command defaults its shortcut to \`mod+shift+<first letter of the id>\` \u2014 two weavers whose ids share a first letter collide; pass \`--shortcut\` or edit the command.`,
|
|
31724
|
+
"- The project is generated **untagged**: Nx tags belong to your `depConstraints`, and inventing",
|
|
31725
|
+
" one would fail a lint policy you never opted this project into. If your workspace enforces",
|
|
31726
|
+
" module boundaries, give it tags your constraints allow \u2014 `--tags` at generation time, or",
|
|
31727
|
+
" `tags` in `project.json` afterwards.",
|
|
31728
|
+
""
|
|
31729
|
+
].join("\n");
|
|
31730
|
+
}
|
|
31731
|
+
|
|
31134
31732
|
// ../devkit/src/recipes/angular-weaver/weaver-i18n.ts
|
|
31135
31733
|
function i18nBundle(w) {
|
|
31136
31734
|
const bundle = { title: w.name };
|
|
@@ -31143,6 +31741,17 @@ function i18nBundle(w) {
|
|
|
31143
31741
|
bundle["actionDescription"] = `Shows a short ${w.name} message.`;
|
|
31144
31742
|
}
|
|
31145
31743
|
if (w.features.about) bundle["about"] = `About ${w.name}`;
|
|
31744
|
+
if (w.features.agent) {
|
|
31745
|
+
bundle["agent"] = {
|
|
31746
|
+
title: `${w.name} assistant`,
|
|
31747
|
+
confirm: {
|
|
31748
|
+
title: "Run this command?",
|
|
31749
|
+
message: "An agent asked to run a command that was marked consequential.",
|
|
31750
|
+
yes: "Run it",
|
|
31751
|
+
no: "Not now"
|
|
31752
|
+
}
|
|
31753
|
+
};
|
|
31754
|
+
}
|
|
31146
31755
|
if (w.features.settings)
|
|
31147
31756
|
bundle["settings"] = { title: w.name, enabled: "Enabled", note: "Note" };
|
|
31148
31757
|
return bundle;
|
|
@@ -31177,18 +31786,16 @@ function resolveMenuSlot(menu) {
|
|
|
31177
31786
|
}
|
|
31178
31787
|
return typeof menu === "string" && menu.length ? menu : void 0;
|
|
31179
31788
|
}
|
|
31180
|
-
var PLATFORM_BOUND_CHORD_TOKENS = [
|
|
31789
|
+
var PLATFORM_BOUND_CHORD_TOKENS = /* @__PURE__ */ new Set([
|
|
31181
31790
|
"cmd",
|
|
31182
31791
|
"command",
|
|
31183
31792
|
"ctrl",
|
|
31184
31793
|
"control",
|
|
31185
31794
|
"meta"
|
|
31186
|
-
];
|
|
31795
|
+
]);
|
|
31187
31796
|
function assertPlatformNeutralChord(shortcut) {
|
|
31188
31797
|
const tokens = shortcut.toLowerCase().split("+").map((token) => token.trim());
|
|
31189
|
-
const bound = tokens.find(
|
|
31190
|
-
(token) => PLATFORM_BOUND_CHORD_TOKENS.includes(token)
|
|
31191
|
-
);
|
|
31798
|
+
const bound = tokens.find((token) => PLATFORM_BOUND_CHORD_TOKENS.has(token));
|
|
31192
31799
|
if (bound) {
|
|
31193
31800
|
throw new Error(
|
|
31194
31801
|
`Shortcut "${shortcut}" binds the platform-specific "${bound}" key. Use the neutral 'mod' token (e.g. 'mod+shift+k') \u2014 the host renders it as \u2318 on macOS and Ctrl elsewhere.`
|
|
@@ -31209,8 +31816,9 @@ function resolveFeatures(id, input) {
|
|
|
31209
31816
|
'A surface cannot be both a container and instanceable: a container tab holds its own ":id" and is therefore routable, while named instances exist only for a docked, non-routable surface. Pick one.'
|
|
31210
31817
|
);
|
|
31211
31818
|
}
|
|
31819
|
+
const agent = Boolean(input?.agent);
|
|
31212
31820
|
return {
|
|
31213
|
-
command: Boolean(input?.command) || menuSlot !== void 0 || barItem || hasShortcut,
|
|
31821
|
+
command: Boolean(input?.command) || menuSlot !== void 0 || barItem || hasShortcut || agent,
|
|
31214
31822
|
menuSlot,
|
|
31215
31823
|
settings: Boolean(input?.settings),
|
|
31216
31824
|
access: input?.access ? accessLiteral(input.access) : void 0,
|
|
@@ -31219,6 +31827,7 @@ function resolveFeatures(id, input) {
|
|
|
31219
31827
|
about: Boolean(input?.about),
|
|
31220
31828
|
instanceable,
|
|
31221
31829
|
container,
|
|
31830
|
+
agent,
|
|
31222
31831
|
spec: input?.spec !== false
|
|
31223
31832
|
};
|
|
31224
31833
|
}
|
|
@@ -31229,6 +31838,10 @@ function deriveCapabilities(features) {
|
|
|
31229
31838
|
set2.add("ui");
|
|
31230
31839
|
set2.add("host");
|
|
31231
31840
|
}
|
|
31841
|
+
if (features.agent) {
|
|
31842
|
+
set2.add("ui");
|
|
31843
|
+
set2.add("automation");
|
|
31844
|
+
}
|
|
31232
31845
|
return KNOWN_CAPABILITIES.filter((capability) => set2.has(capability));
|
|
31233
31846
|
}
|
|
31234
31847
|
function resolveWeaverInput(input) {
|
|
@@ -31250,10 +31863,6 @@ function resolveWeaverInput(input) {
|
|
|
31250
31863
|
importPath: input.importPath?.trim() || `@loomweaver/${input.id}-weaver`
|
|
31251
31864
|
};
|
|
31252
31865
|
}
|
|
31253
|
-
function capabilityItems(capabilities) {
|
|
31254
|
-
return capabilities.map((capability) => `'${capability}'`).join(", ");
|
|
31255
|
-
}
|
|
31256
|
-
var CONTAINER_EXAMPLE_ID = "example";
|
|
31257
31866
|
function containerChildIds(w) {
|
|
31258
31867
|
return [`${w.id}.canvas`, `${w.id}.details`];
|
|
31259
31868
|
}
|
|
@@ -31417,6 +32026,12 @@ function pluginFile(w) {
|
|
|
31417
32026
|
`import { ${w.className}AboutDialog } from '../dialogs/${w.id}-about-dialog';`
|
|
31418
32027
|
);
|
|
31419
32028
|
}
|
|
32029
|
+
if (w.features.agent) {
|
|
32030
|
+
imports.push(
|
|
32031
|
+
`import { ${w.propertyName}Agent, ${w.propertyName}Connection } from '../agent/${w.id}-agent';`,
|
|
32032
|
+
`import { ${w.className}AgentPanel } from '../agent/${w.id}-agent-panel';`
|
|
32033
|
+
);
|
|
32034
|
+
}
|
|
31420
32035
|
if (w.features.settings) {
|
|
31421
32036
|
imports.unshift("import { signal } from '@angular/core';");
|
|
31422
32037
|
}
|
|
@@ -31429,6 +32044,11 @@ function pluginFile(w) {
|
|
|
31429
32044
|
);
|
|
31430
32045
|
}
|
|
31431
32046
|
const body = [` ctx.contributeIcons({ '${w.id}': icon });`];
|
|
32047
|
+
if (w.features.agent) {
|
|
32048
|
+
body.push(
|
|
32049
|
+
` ${w.propertyName}Agent.set(${w.propertyName}Connection(ctx));`
|
|
32050
|
+
);
|
|
32051
|
+
}
|
|
31432
32052
|
if (w.features.command) body.push(commandBlock(w));
|
|
31433
32053
|
if (w.features.about) body.push(aboutCommandBlock(w));
|
|
31434
32054
|
body.push(surfaceBlock(w), railBlock(w));
|
|
@@ -31436,6 +32056,11 @@ function pluginFile(w) {
|
|
|
31436
32056
|
if (w.features.barItem) body.push(barItemBlock(w));
|
|
31437
32057
|
if (w.features.menuSlot) body.push(menuBlock(w));
|
|
31438
32058
|
if (w.features.settings) body.push(settingsBlock(w));
|
|
32059
|
+
if (w.features.agent) body.push(agentSurfaceBlock(w));
|
|
32060
|
+
const deactivate = w.features.agent ? `
|
|
32061
|
+
deactivate() {
|
|
32062
|
+
${w.propertyName}Agent.set(null);
|
|
32063
|
+
},` : "";
|
|
31439
32064
|
return `${imports.join("\n")}
|
|
31440
32065
|
|
|
31441
32066
|
${consts.join("\n")}
|
|
@@ -31448,7 +32073,7 @@ export const ${w.propertyName}Plugin: Plugin = {
|
|
|
31448
32073
|
},
|
|
31449
32074
|
activate(ctx) {
|
|
31450
32075
|
${body.join("\n")}
|
|
31451
|
-
}
|
|
32076
|
+
},${deactivate}
|
|
31452
32077
|
};
|
|
31453
32078
|
`;
|
|
31454
32079
|
}
|
|
@@ -31564,7 +32189,7 @@ function childViewTemplateFile(w, heading) {
|
|
|
31564
32189
|
</div>
|
|
31565
32190
|
`;
|
|
31566
32191
|
}
|
|
31567
|
-
function
|
|
32192
|
+
function specFile2(w) {
|
|
31568
32193
|
return `import { ${w.propertyName}Plugin } from './${w.id}.plugin';
|
|
31569
32194
|
|
|
31570
32195
|
describe('${w.propertyName}Plugin', () => {
|
|
@@ -31575,112 +32200,6 @@ describe('${w.propertyName}Plugin', () => {
|
|
|
31575
32200
|
});
|
|
31576
32201
|
`;
|
|
31577
32202
|
}
|
|
31578
|
-
function surfaceNotes(w) {
|
|
31579
|
-
const railNote = "Rail and bar items reference region ids (`primary`, `status`) that must exist in your layout.";
|
|
31580
|
-
if (w.features.container) {
|
|
31581
|
-
return [
|
|
31582
|
-
`The surface is a **container**: it is routable at \`/${w.id}/:id\`, and its tab holds a`,
|
|
31583
|
-
"nested pane tree of child surfaces. The host draws the inner tabs, splits and drag targets; this",
|
|
31584
|
-
"weaver only declares which children it offers.",
|
|
31585
|
-
"",
|
|
31586
|
-
`- \`children\` is what the inner "new tab" picker lists \u2014 the host access-gates it for you.`,
|
|
31587
|
-
"- `initial` is what a freshly opened container tab starts with.",
|
|
31588
|
-
`- The children declare \`docks: []\`. That is the container-only convention: they are never seeded`,
|
|
31589
|
-
" into a sidebar, they exist solely inside this container.",
|
|
31590
|
-
`- Each child reads the container's \`:id\` from an injected \`ActivatedRoute\` \u2014 the host supplies a`,
|
|
31591
|
-
" synthetic one, so a child needs no knowledge of where it is mounted. Two open container tabs are",
|
|
31592
|
-
" two independent trees, each scoped to its own id.",
|
|
31593
|
-
`- The inner tree is **sealed**: a child cannot be dragged out, and nothing can be dragged in. It`,
|
|
31594
|
-
" travels with the tab, including into a sidebar or a pop-out window.",
|
|
31595
|
-
"",
|
|
31596
|
-
`The rail item opens the fixed id \`${CONTAINER_EXAMPLE_ID}\`. Replace that with whatever the user`,
|
|
31597
|
-
"actually picked \u2014 a document, a run, a project.",
|
|
31598
|
-
"",
|
|
31599
|
-
railNote
|
|
31600
|
-
];
|
|
31601
|
-
}
|
|
31602
|
-
if (w.features.instanceable) {
|
|
31603
|
-
return [
|
|
31604
|
-
`The surface is **docked** into the \`primary\` region and marked \`instanceable\`, so the host shows a`,
|
|
31605
|
-
"switcher for saving, naming, renaming and deleting several configurations of it, each with its own",
|
|
31606
|
-
"`VIEW_STATE` blob.",
|
|
31607
|
-
"",
|
|
31608
|
-
"It is deliberately **not** routable. Named instances exist only for a docked surface \u2014 a routable",
|
|
31609
|
-
"one holds the URL pane instead, and the host drops `instanceable` on that path. The rail item",
|
|
31610
|
-
"therefore reveals the surface (`ctx.revealSurface`) rather than navigating to a URL, which focuses",
|
|
31611
|
-
"it wherever the user has since moved it.",
|
|
31612
|
-
"",
|
|
31613
|
-
"The generated view already uses that blob for its sort order, because a hidden surface is destroyed",
|
|
31614
|
-
"as soon as it is clean: state kept in a component field survives neither a tab switch nor",
|
|
31615
|
-
"a collapsed sidebar, and never survived a reload. The rule is *evictable = reload-safe* \u2014 anything",
|
|
31616
|
-
"that must not be lost goes through `VIEW_STATE`, and `set()` replaces the whole blob, so spread it.",
|
|
31617
|
-
"",
|
|
31618
|
-
railNote
|
|
31619
|
-
];
|
|
31620
|
-
}
|
|
31621
|
-
return [
|
|
31622
|
-
`The surface is routable at \`/${w.id}\`; ${railNote.charAt(0).toLowerCase()}${railNote.slice(1)}`,
|
|
31623
|
-
"",
|
|
31624
|
-
"A routable surface has **no `VIEW_STATE` handle** \u2014 injecting the token there throws. It owns a URL,",
|
|
31625
|
-
"so anything shareable (a filter, the active sub-tab) belongs in route params or `subRoutes`, where it",
|
|
31626
|
-
"survives a deep link too; unsaved edits are `DirtySurface`, and an instance that is expensive to",
|
|
31627
|
-
"rebuild declares `retain: 'always'`. Generate with `--instanceable` for the docked, `VIEW_STATE`",
|
|
31628
|
-
"flavour instead."
|
|
31629
|
-
];
|
|
31630
|
-
}
|
|
31631
|
-
function readmeFile(w) {
|
|
31632
|
-
return [
|
|
31633
|
-
`# ${w.name} weaver`,
|
|
31634
|
-
"",
|
|
31635
|
-
`A LoomWeaver weaver (a domain plugin bundle). It consumes only the public \`@loomweaver/plugin-sdk\` contract.`,
|
|
31636
|
-
"",
|
|
31637
|
-
"## Wire it into a distribution",
|
|
31638
|
-
"",
|
|
31639
|
-
`1. Add the plugin to \`providePlugins\` in \`src/app/app.config.ts\`. It is **variadic** and`,
|
|
31640
|
-
` returns an array, so spread it:`,
|
|
31641
|
-
"",
|
|
31642
|
-
" ```ts",
|
|
31643
|
-
` import { ${w.propertyName}Plugin } from '${w.importPath}'; // Nx: the workspace alias; without one, a relative path to this library's src/index.ts`,
|
|
31644
|
-
` ...providePlugins(${w.propertyName}Plugin),`,
|
|
31645
|
-
" ```",
|
|
31646
|
-
"",
|
|
31647
|
-
`2. Grant its capabilities (default-deny) via \`provideCapabilityGrants\`:`,
|
|
31648
|
-
"",
|
|
31649
|
-
" ```ts",
|
|
31650
|
-
` provideCapabilityGrants({ '${w.id}': [${capabilityItems(w.capabilities)}] });`,
|
|
31651
|
-
" ```",
|
|
31652
|
-
"",
|
|
31653
|
-
`3. Compose its translations with \`provideTranslationNamespaces('${w.id}')\` \u2014 and serve the`,
|
|
31654
|
-
` bundle by adding an assets glob to your application's build target, so the loader can fetch`,
|
|
31655
|
-
` \`/i18n/${w.id}/<lang>.json\` (the Nx generator adds this glob for you):`,
|
|
31656
|
-
"",
|
|
31657
|
-
" ```json",
|
|
31658
|
-
` { "glob": "**/*.json", "input": "<path to this library>/src/lib/i18n", "output": "i18n/${w.id}" }`,
|
|
31659
|
-
" ```",
|
|
31660
|
-
"",
|
|
31661
|
-
`4. If your application compiles the shell's theme with Tailwind, name this library as a source`,
|
|
31662
|
-
` for it, so the utility classes in these templates are emitted. Tailwind also detects sources`,
|
|
31663
|
-
` by itself, but that depends on where it resolves the project root and on \`.gitignore\`, and`,
|
|
31664
|
-
` what the scaffold names covers the application alone (the Nx generator adds this line for`,
|
|
31665
|
-
` you). Applications scaffolded with \`--styles precompiled\` run no Tailwind and need nothing:`,
|
|
31666
|
-
"",
|
|
31667
|
-
" ```css",
|
|
31668
|
-
` @source '<path from that stylesheet to this library>/src';`,
|
|
31669
|
-
" ```",
|
|
31670
|
-
"",
|
|
31671
|
-
...surfaceNotes(w),
|
|
31672
|
-
"",
|
|
31673
|
-
"## After scaffolding",
|
|
31674
|
-
"",
|
|
31675
|
-
"- `src/lib/i18n/de.json` starts as a copy of the English strings \u2014 translate it.",
|
|
31676
|
-
`- A scaffolded command defaults its shortcut to \`mod+shift+<first letter of the id>\` \u2014 two weavers whose ids share a first letter collide; pass \`--shortcut\` or edit the command.`,
|
|
31677
|
-
"- The project is generated **untagged**: Nx tags belong to your `depConstraints`, and inventing",
|
|
31678
|
-
" one would fail a lint policy you never opted this project into. If your workspace enforces",
|
|
31679
|
-
" module boundaries, give it tags your constraints allow \u2014 `--tags` at generation time, or",
|
|
31680
|
-
" `tags` in `project.json` afterwards.",
|
|
31681
|
-
""
|
|
31682
|
-
].join("\n");
|
|
31683
|
-
}
|
|
31684
32203
|
var angularWeaver = {
|
|
31685
32204
|
id: "angular-weaver",
|
|
31686
32205
|
build(input) {
|
|
@@ -31719,8 +32238,11 @@ var angularWeaver = {
|
|
|
31719
32238
|
files[`src/lib/dialogs/${w.id}-about-dialog.ts`] = aboutDialogFile(w);
|
|
31720
32239
|
files[`src/lib/dialogs/${w.id}-about-dialog.html`] = aboutDialogTemplateFile(w);
|
|
31721
32240
|
}
|
|
32241
|
+
if (w.features.agent) {
|
|
32242
|
+
Object.assign(files, agentFiles(w));
|
|
32243
|
+
}
|
|
31722
32244
|
if (w.features.spec) {
|
|
31723
|
-
files[`src/lib/plugin/${w.id}.plugin.spec.ts`] =
|
|
32245
|
+
files[`src/lib/plugin/${w.id}.plugin.spec.ts`] = specFile2(w);
|
|
31724
32246
|
}
|
|
31725
32247
|
return files;
|
|
31726
32248
|
}
|
|
@@ -32177,7 +32699,7 @@ import { provideProductIdentity } from '@loomweaver/plugin-sdk';
|
|
|
32177
32699
|
'status-bar' (bar) are what the scaffolded weaver targets. */
|
|
32178
32700
|
export const layout: ShellLayout = {
|
|
32179
32701
|
regions: [
|
|
32180
|
-
${renderRegions("
|
|
32702
|
+
${renderRegions(" ".repeat(4))}
|
|
32181
32703
|
],
|
|
32182
32704
|
};
|
|
32183
32705
|
|
|
@@ -32362,7 +32884,7 @@ var angularDistribution = {
|
|
|
32362
32884
|
return {
|
|
32363
32885
|
"src/main.ts": mainTs(),
|
|
32364
32886
|
"src/app/app.config.ts": appConfigTs(d),
|
|
32365
|
-
...d.withTests
|
|
32887
|
+
...d.withTests && { "src/app/app.config.spec.ts": appConfigSpec() },
|
|
32366
32888
|
"src/app/app.ts": appTs(),
|
|
32367
32889
|
"src/app/app.html": appHtml(),
|
|
32368
32890
|
"src/index.html": indexHtml(d),
|
|
@@ -32634,7 +33156,7 @@ import { ShellLayout } from '@loomweaver/shell';
|
|
|
32634
33156
|
|
|
32635
33157
|
export const ${l.propertyName}Layout: ShellLayout = {
|
|
32636
33158
|
regions: [
|
|
32637
|
-
${renderRegions("
|
|
33159
|
+
${renderRegions(" ".repeat(4))}
|
|
32638
33160
|
],
|
|
32639
33161
|
};
|
|
32640
33162
|
`;
|
|
@@ -32650,11 +33172,24 @@ var layout = {
|
|
|
32650
33172
|
// ../devkit/src/recipes/angular-weaver/amendments.ts
|
|
32651
33173
|
function weaverAmendments(input, where) {
|
|
32652
33174
|
const w = resolveWeaverInput(input);
|
|
32653
|
-
const
|
|
33175
|
+
const packages = w.features.agent ? [
|
|
33176
|
+
{
|
|
33177
|
+
kind: "package",
|
|
33178
|
+
name: "@loomweaver/ag-ui",
|
|
33179
|
+
version: `^${AG_UI_ADAPTER_VERSION}`
|
|
33180
|
+
},
|
|
33181
|
+
{
|
|
33182
|
+
kind: "package",
|
|
33183
|
+
name: "@ag-ui/core",
|
|
33184
|
+
version: AG_UI_PROTOCOL_VERSION
|
|
33185
|
+
}
|
|
33186
|
+
] : [];
|
|
33187
|
+
const directory = normalizeProjectRoot(where ?? "");
|
|
32654
33188
|
if (!directory) {
|
|
32655
|
-
return
|
|
33189
|
+
return packages;
|
|
32656
33190
|
}
|
|
32657
33191
|
return [
|
|
33192
|
+
...packages,
|
|
32658
33193
|
{
|
|
32659
33194
|
kind: "build-target",
|
|
32660
33195
|
styles: [],
|
|
@@ -32694,6 +33229,7 @@ function weaverInput(values) {
|
|
|
32694
33229
|
about: bool(values, "about"),
|
|
32695
33230
|
instanceable: bool(values, "instanceable"),
|
|
32696
33231
|
container: bool(values, "container"),
|
|
33232
|
+
agent: bool(values, "agent"),
|
|
32697
33233
|
access: str(values, "access"),
|
|
32698
33234
|
spec: bool(values, "spec")
|
|
32699
33235
|
}
|
|
@@ -32817,6 +33353,12 @@ var SCAFFOLDS = [
|
|
|
32817
33353
|
description: "Make the surface a container: a routable tab at '<id>/:id' holding a nested pane tree of child surfaces, which the host draws. Not combinable with --instanceable.",
|
|
32818
33354
|
default: false
|
|
32819
33355
|
},
|
|
33356
|
+
{
|
|
33357
|
+
name: "agent",
|
|
33358
|
+
type: "boolean",
|
|
33359
|
+
description: "Also scaffold the connection that lets an AG-UI agent run the commands this workbench offers: a docked panel, the seam where a call is decided before it runs, and a local stand-in that speaks the protocol so the whole path works on the first serve. Implies --command.",
|
|
33360
|
+
default: false
|
|
33361
|
+
},
|
|
32820
33362
|
{
|
|
32821
33363
|
name: "access",
|
|
32822
33364
|
type: "string",
|
|
@@ -33117,6 +33659,14 @@ function validateEntry(raw, index, known) {
|
|
|
33117
33659
|
}
|
|
33118
33660
|
];
|
|
33119
33661
|
}
|
|
33662
|
+
return [
|
|
33663
|
+
...validateEntryIdentity(raw, index),
|
|
33664
|
+
...validateCapabilities2(raw["capabilities"], index, known),
|
|
33665
|
+
...validateEntryMetadata(raw, index),
|
|
33666
|
+
...validateEntryKeys(raw, index)
|
|
33667
|
+
];
|
|
33668
|
+
}
|
|
33669
|
+
function validateEntryIdentity(raw, index) {
|
|
33120
33670
|
const findings = [];
|
|
33121
33671
|
if (typeof raw["id"] !== "string" || raw["id"].length === 0) {
|
|
33122
33672
|
findings.push({
|
|
@@ -33158,7 +33708,10 @@ function validateEntry(raw, index, known) {
|
|
|
33158
33708
|
path: at(index, "version")
|
|
33159
33709
|
});
|
|
33160
33710
|
}
|
|
33161
|
-
findings
|
|
33711
|
+
return findings;
|
|
33712
|
+
}
|
|
33713
|
+
function validateEntryMetadata(raw, index) {
|
|
33714
|
+
const findings = [];
|
|
33162
33715
|
if (raw["downloads"] !== void 0 && (typeof raw["downloads"] !== "number" || raw["downloads"] < 0)) {
|
|
33163
33716
|
findings.push({
|
|
33164
33717
|
level: "warning",
|
|
@@ -33183,6 +33736,10 @@ function validateEntry(raw, index, known) {
|
|
|
33183
33736
|
path: at(index, "repository")
|
|
33184
33737
|
});
|
|
33185
33738
|
}
|
|
33739
|
+
return findings;
|
|
33740
|
+
}
|
|
33741
|
+
function validateEntryKeys(raw, index) {
|
|
33742
|
+
const findings = [];
|
|
33186
33743
|
for (const key of Object.keys(raw)) {
|
|
33187
33744
|
if (!CATALOG_ENTRY_KEYS.includes(key)) {
|
|
33188
33745
|
findings.push({
|
|
@@ -33222,7 +33779,7 @@ function validateCatalog(catalog, known = KNOWN_CAPABILITIES) {
|
|
|
33222
33779
|
}
|
|
33223
33780
|
const findings = [];
|
|
33224
33781
|
const seen = /* @__PURE__ */ new Set();
|
|
33225
|
-
catalog.
|
|
33782
|
+
for (const [index, entry] of catalog.entries()) {
|
|
33226
33783
|
findings.push(...validateEntry(entry, index, known));
|
|
33227
33784
|
const id = isPlainObject3(entry) ? entry["id"] : void 0;
|
|
33228
33785
|
if (typeof id === "string" && id.length > 0) {
|
|
@@ -33236,7 +33793,7 @@ function validateCatalog(catalog, known = KNOWN_CAPABILITIES) {
|
|
|
33236
33793
|
}
|
|
33237
33794
|
seen.add(id);
|
|
33238
33795
|
}
|
|
33239
|
-
}
|
|
33796
|
+
}
|
|
33240
33797
|
return findings;
|
|
33241
33798
|
}
|
|
33242
33799
|
|
|
@@ -33248,7 +33805,7 @@ function ok(data) {
|
|
|
33248
33805
|
};
|
|
33249
33806
|
}
|
|
33250
33807
|
function toolName(scaffold2) {
|
|
33251
|
-
return `scaffold_${scaffold2.name.
|
|
33808
|
+
return `scaffold_${scaffold2.name.replaceAll("-", "_")}`;
|
|
33252
33809
|
}
|
|
33253
33810
|
function listGenerators() {
|
|
33254
33811
|
return ok({
|
|
@@ -33273,10 +33830,10 @@ function valuesFor(scaffold2, args) {
|
|
|
33273
33830
|
}
|
|
33274
33831
|
function scaffold(scaffold2, args) {
|
|
33275
33832
|
const values = valuesFor(scaffold2, args);
|
|
33276
|
-
const remaining = (scaffold2.amend?.(values) ?? []).map(describeAmendment);
|
|
33833
|
+
const remaining = (scaffold2.amend?.(values) ?? []).map((amendment) => describeAmendment(amendment));
|
|
33277
33834
|
return ok({
|
|
33278
33835
|
files: scaffold2.build(values),
|
|
33279
|
-
...remaining.length > 0
|
|
33836
|
+
...remaining.length > 0 && { remaining }
|
|
33280
33837
|
});
|
|
33281
33838
|
}
|
|
33282
33839
|
function validateManifestTool(a) {
|
|
@@ -33298,10 +33855,16 @@ function validateI18nTool(a) {
|
|
|
33298
33855
|
|
|
33299
33856
|
// src/lib/server.ts
|
|
33300
33857
|
var FILE_MAP_NOTE = 'Returns a file map (relative path -> content); write the files into your project. May also return "remaining": steps the workspace needs that this route cannot perform, each saying what it costs to skip. Carry them out, or the generated output builds and does not work.';
|
|
33858
|
+
function optionSchema(option) {
|
|
33859
|
+
if (option.type === "boolean") {
|
|
33860
|
+
return external_exports.boolean();
|
|
33861
|
+
}
|
|
33862
|
+
return option.choices ? external_exports.enum(option.choices) : external_exports.string();
|
|
33863
|
+
}
|
|
33301
33864
|
function inputSchema(descriptor) {
|
|
33302
33865
|
const shape = {};
|
|
33303
33866
|
for (const option of portableOptions(descriptor)) {
|
|
33304
|
-
const base =
|
|
33867
|
+
const base = optionSchema(option);
|
|
33305
33868
|
const described = base.describe(option.description);
|
|
33306
33869
|
shape[option.name] = option.required ? described : described.optional();
|
|
33307
33870
|
}
|
|
@@ -33310,7 +33873,7 @@ function inputSchema(descriptor) {
|
|
|
33310
33873
|
function createMcpServer() {
|
|
33311
33874
|
const server = new McpServer({
|
|
33312
33875
|
name: "loomweaver-devkit",
|
|
33313
|
-
version: "0.7.
|
|
33876
|
+
version: "0.7.7"
|
|
33314
33877
|
});
|
|
33315
33878
|
server.registerTool(
|
|
33316
33879
|
"list_generators",
|
|
@@ -33370,7 +33933,9 @@ async function main() {
|
|
|
33370
33933
|
const server = createMcpServer();
|
|
33371
33934
|
await server.connect(new StdioServerTransport());
|
|
33372
33935
|
}
|
|
33373
|
-
|
|
33936
|
+
try {
|
|
33937
|
+
await main();
|
|
33938
|
+
} catch (error51) {
|
|
33374
33939
|
console.error(error51);
|
|
33375
33940
|
process.exit(1);
|
|
33376
|
-
}
|
|
33941
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomweaver/mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "LoomWeaver MCP server: exposes LoomWeaver's scaffolding and validation as MCP tools, so an AI assistant in any product repo can scaffold and validate weavers, distributions and integrations without a LoomWeaver checkout.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"loomweaver",
|