@loomweaver/mcp 0.8.3 → 0.9.0-preview.2

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.
Files changed (2) hide show
  1. package/dist/main.mjs +319 -43
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -2984,7 +2984,7 @@ var require_compile = __commonJS({
2984
2984
  const schOrFunc = root.refs[ref];
2985
2985
  if (schOrFunc)
2986
2986
  return schOrFunc;
2987
- let _sch = resolve.call(this, root, ref);
2987
+ let _sch = resolve2.call(this, root, ref);
2988
2988
  if (_sch === void 0) {
2989
2989
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
2990
2990
  const { schemaId } = this.opts;
@@ -3011,7 +3011,7 @@ var require_compile = __commonJS({
3011
3011
  function sameSchemaEnv(s1, s2) {
3012
3012
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3013
3013
  }
3014
- function resolve(root, ref) {
3014
+ function resolve2(root, ref) {
3015
3015
  let sch;
3016
3016
  while (typeof (sch = this.refs[ref]) == "string")
3017
3017
  ref = sch;
@@ -3642,7 +3642,7 @@ var require_fast_uri = __commonJS({
3642
3642
  }
3643
3643
  return uri;
3644
3644
  }
3645
- function resolve(baseURI, relativeURI, options) {
3645
+ function resolve2(baseURI, relativeURI, options) {
3646
3646
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3647
3647
  const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3648
3648
  const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
@@ -3926,7 +3926,7 @@ var require_fast_uri = __commonJS({
3926
3926
  var fastUri = {
3927
3927
  SCHEMES,
3928
3928
  normalize,
3929
- resolve,
3929
+ resolve: resolve2,
3930
3930
  resolveComponent,
3931
3931
  equal,
3932
3932
  serialize,
@@ -23056,12 +23056,12 @@ var StdioServerTransport = class {
23056
23056
  this.onclose?.();
23057
23057
  }
23058
23058
  send(message) {
23059
- return new Promise((resolve) => {
23059
+ return new Promise((resolve2) => {
23060
23060
  const json2 = serializeMessage(message);
23061
23061
  if (this._stdout.write(json2)) {
23062
- resolve();
23062
+ resolve2();
23063
23063
  } else {
23064
- this._stdout.once("drain", resolve);
23064
+ this._stdout.once("drain", resolve2);
23065
23065
  }
23066
23066
  });
23067
23067
  }
@@ -28980,7 +28980,7 @@ var Protocol = class {
28980
28980
  return;
28981
28981
  }
28982
28982
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
28983
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
28983
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
28984
28984
  options?.signal?.throwIfAborted();
28985
28985
  }
28986
28986
  } catch (error51) {
@@ -28997,7 +28997,7 @@ var Protocol = class {
28997
28997
  */
28998
28998
  request(request, resultSchema, options) {
28999
28999
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
29000
- return new Promise((resolve, reject) => {
29000
+ return new Promise((resolve2, reject) => {
29001
29001
  const earlyReject = (error51) => {
29002
29002
  reject(error51);
29003
29003
  };
@@ -29075,7 +29075,7 @@ var Protocol = class {
29075
29075
  if (!parseResult.success) {
29076
29076
  reject(parseResult.error);
29077
29077
  } else {
29078
- resolve(parseResult.data);
29078
+ resolve2(parseResult.data);
29079
29079
  }
29080
29080
  } catch (error51) {
29081
29081
  reject(error51);
@@ -29336,12 +29336,12 @@ var Protocol = class {
29336
29336
  }
29337
29337
  } catch {
29338
29338
  }
29339
- return new Promise((resolve, reject) => {
29339
+ return new Promise((resolve2, reject) => {
29340
29340
  if (signal.aborted) {
29341
29341
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
29342
29342
  return;
29343
29343
  }
29344
- const timeoutId = setTimeout(resolve, interval);
29344
+ const timeoutId = setTimeout(resolve2, interval);
29345
29345
  signal.addEventListener("abort", () => {
29346
29346
  clearTimeout(timeoutId);
29347
29347
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -30432,7 +30432,7 @@ var McpServer = class {
30432
30432
  let task = createTaskResult.task;
30433
30433
  const pollInterval = task.pollInterval ?? 5e3;
30434
30434
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
30435
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
30435
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
30436
30436
  const updatedTask = await extra.taskStore.getTask(taskId);
30437
30437
  if (!updatedTask) {
30438
30438
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -31175,6 +31175,12 @@ export class ${w.className}AgentPanel {
31175
31175
  private runs = 0;
31176
31176
 
31177
31177
  constructor() {
31178
+ this.refreshOffered();
31179
+ }
31180
+
31181
+ // The list is read again whenever the panel gains focus, because the weaver's own translations
31182
+ // and other plugins' commands may arrive after this component was built.
31183
+ protected refreshOffered(): void {
31178
31184
  this.offered.set(${w.propertyName}Agent()?.list() ?? []);
31179
31185
  }
31180
31186
 
@@ -31265,7 +31271,7 @@ export class ${w.className}AgentPanel {
31265
31271
  `;
31266
31272
  }
31267
31273
  function panelTemplateFile(w) {
31268
- return `<div class="flex h-full flex-col gap-3">
31274
+ return `<div class="flex h-full flex-col gap-3" (focusin)="refreshOffered()">
31269
31275
  <p
31270
31276
  class="shrink-0 rounded-md border border-border bg-surface-raised px-3 py-2 text-xs text-content-muted"
31271
31277
  >
@@ -31381,10 +31387,10 @@ export async function* askAgent(
31381
31387
  toolCallName: picked.name,
31382
31388
  });
31383
31389
  // 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)) {
31390
+ // nothing downstream ever sees a half-written call. The values come from the JSON Schema the
31391
+ // workbench described in picked.parameters: the first declared choice of the first argument that
31392
+ // has choices, and nothing for the rest, so the command applies its own defaults there.
31393
+ for (const piece of chunks(JSON.stringify(sampleArguments(picked)), 4)) {
31388
31394
  yield event(EventType.TOOL_CALL_ARGS, { toolCallId, delta: piece });
31389
31395
  await pause();
31390
31396
  }
@@ -31409,6 +31415,18 @@ async function* speak(
31409
31415
  yield event(EventType.TEXT_MESSAGE_END, { messageId });
31410
31416
  }
31411
31417
 
31418
+ function sampleArguments(tool: Tool): Record<string, unknown> {
31419
+ const schema = tool.parameters as
31420
+ | { properties?: Record<string, { enum?: readonly unknown[] }> }
31421
+ | undefined;
31422
+ for (const [name, property] of Object.entries(schema?.properties ?? {})) {
31423
+ if (property.enum && property.enum.length > 0) {
31424
+ return { [name]: property.enum[0] };
31425
+ }
31426
+ }
31427
+ return {};
31428
+ }
31429
+
31412
31430
  function chunks(text: string, size: number): readonly string[] {
31413
31431
  const pieces: string[] = [];
31414
31432
  for (let at = 0; at < text.length; at += size) {
@@ -31428,7 +31446,7 @@ function pause(): Promise<void> {
31428
31446
  }
31429
31447
 
31430
31448
  // ../devkit/src/recipes/angular-weaver/agent-files.ts
31431
- var AG_UI_ADAPTER_VERSION = "0.8.3";
31449
+ var AG_UI_ADAPTER_VERSION = "0.9.0-preview.2";
31432
31450
  var AG_UI_PROTOCOL_VERSION = "0.0.x";
31433
31451
  function connectionFile(w) {
31434
31452
  return `import { signal } from '@angular/core';
@@ -31494,7 +31512,7 @@ function contextThat(confirms: boolean, ran: Asked[]): PluginContext {
31494
31512
  ],
31495
31513
  invokeCommand: (id: string, args?: CommandArguments) => {
31496
31514
  ran.push({ id, args });
31497
- return Promise.resolve({ outcome: 'answered', value: 'it ran' });
31515
+ return Promise.resolve({ outcome: 'answered', value: { tone: args?.['tone'] ?? 'info' } });
31498
31516
  },
31499
31517
  ui: { confirm: () => Promise.resolve(confirms) },
31500
31518
  } as unknown as PluginContext;
@@ -31523,17 +31541,17 @@ describe('${w.propertyName}Connection', () => {
31523
31541
  ),
31524
31542
  ).toBeNull();
31525
31543
  await tools.receive(
31526
- event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: '{"who"' }),
31544
+ event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: '{"tone"' }),
31527
31545
  );
31528
31546
  await tools.receive(
31529
- event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: ':"you"}' }),
31547
+ event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: ':"success"}' }),
31530
31548
  );
31531
31549
  const answer = await tools.receive(
31532
31550
  event(EventType.TOOL_CALL_END, { toolCallId: 'c1' }),
31533
31551
  );
31534
31552
 
31535
- expect(ran).toEqual([{ id: '${w.id}.hello', args: { who: 'you' } }]);
31536
- expect(answer?.content).toBe('it ran');
31553
+ expect(ran).toEqual([{ id: '${w.id}.hello', args: { tone: 'success' } }]);
31554
+ expect(JSON.parse(answer?.content ?? '{}').tone).toBe('success');
31537
31555
  expect(answer?.error).toBeUndefined();
31538
31556
  });
31539
31557
 
@@ -31738,7 +31756,9 @@ function i18nBundle(w) {
31738
31756
  }
31739
31757
  if (w.features.command) {
31740
31758
  bundle["action"] = `${w.name} action`;
31741
- bundle["actionDescription"] = `Shows a short ${w.name} message.`;
31759
+ bundle["actionDescription"] = `Shows a short ${w.name} message in the tone the caller chooses.`;
31760
+ bundle["actionTone"] = "How the message is shown: info, success or warning. Use info unless the caller asked for emphasis.";
31761
+ bundle["actionAnswers"] = "The tone the message was shown in.";
31742
31762
  }
31743
31763
  if (w.features.about) bundle["about"] = `About ${w.name}`;
31744
31764
  if (w.features.agent) {
@@ -31760,6 +31780,38 @@ function i18nFile(w) {
31760
31780
  return JSON.stringify(i18nBundle(w), null, 2) + "\n";
31761
31781
  }
31762
31782
 
31783
+ // ../devkit/src/recipes/angular-weaver/weaver-command.ts
31784
+ function commandBlock(w) {
31785
+ return [
31786
+ " ctx.registerCommand({",
31787
+ ` id: '${w.id}.hello',`,
31788
+ ` title: '${w.id}.action',`,
31789
+ ` description: '${w.id}.actionDescription',`,
31790
+ " arguments: [",
31791
+ ` { name: 'tone', kind: 'choice', choices: ['info', 'success', 'warning'], description: '${w.id}.actionTone' },`,
31792
+ " ],",
31793
+ ` answers: '${w.id}.actionAnswers',`,
31794
+ ` shortcut: '${w.features.shortcut}',`,
31795
+ " callable: true,",
31796
+ " run: (_context, args) => {",
31797
+ " const tone = toneOf(args?.['tone']);",
31798
+ ` ctx.ui.toast({ message: '${w.id}.action', kind: tone });`,
31799
+ " return { tone };",
31800
+ " },",
31801
+ " });"
31802
+ ].join("\n");
31803
+ }
31804
+ function toneHelper() {
31805
+ return [
31806
+ "type Tone = 'info' | 'success' | 'warning';",
31807
+ "",
31808
+ "// The workbench already refused anything outside the declared choices; this narrows the type.",
31809
+ "function toneOf(value: unknown): Tone {",
31810
+ " return value === 'success' || value === 'warning' ? value : 'info';",
31811
+ "}"
31812
+ ].join("\n");
31813
+ }
31814
+
31763
31815
  // ../devkit/src/recipes/angular-weaver/recipe.ts
31764
31816
  var DEFAULT_MENU_SLOT = "content/tab/context";
31765
31817
  var SURFACE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 9h8M8 13h8M8 17h5"/></svg>';
@@ -31938,18 +31990,6 @@ function railBlock(w) {
31938
31990
  lines.push(" });");
31939
31991
  return lines.join("\n");
31940
31992
  }
31941
- function commandBlock(w) {
31942
- return [
31943
- " ctx.registerCommand({",
31944
- ` id: '${w.id}.hello',`,
31945
- ` title: '${w.id}.action',`,
31946
- ` description: '${w.id}.actionDescription',`,
31947
- ` shortcut: '${w.features.shortcut}',`,
31948
- " callable: true,",
31949
- ` run: () => ctx.ui.toast({ message: '${w.id}.action', kind: 'info' }),`,
31950
- " });"
31951
- ].join("\n");
31952
- }
31953
31993
  function barItemBlock(w) {
31954
31994
  return [
31955
31995
  " ctx.registerBarItem({",
@@ -32061,9 +32101,10 @@ function pluginFile(w) {
32061
32101
  deactivate() {
32062
32102
  ${w.propertyName}Agent.set(null);
32063
32103
  },` : "";
32104
+ if (w.features.command) consts.push(toneHelper());
32064
32105
  return `${imports.join("\n")}
32065
32106
 
32066
- ${consts.join("\n")}
32107
+ ${consts.join("\n\n")}
32067
32108
 
32068
32109
  export const ${w.propertyName}Plugin: Plugin = {
32069
32110
  manifest: {
@@ -32502,9 +32543,9 @@ function readme2(d) {
32502
32543
  "project's `project.json`, keeping the targets, `implicitDependencies` and tags it already had.",
32503
32544
  "",
32504
32545
  "Two leftovers from `ng new` are no longer referenced and can go: `src/app/app.routes.ts` (the",
32505
- "shell owns content routing via `provideShellRouter()`) and `src/app/app.css`. The generated",
32506
- "`src/app/app.spec.ts` now fails \u2014 `App` pulls the whole shell into a bare `TestBed` \u2014 so delete",
32507
- "it and test your own components instead of the composition root.",
32546
+ "shell owns content routing via `provideShellRouter()`) and `src/app/app.css`. The starter test",
32547
+ "`src/app/app.spec.ts` was replaced by one that boots the shell with this composition root, so",
32548
+ "`ng test` is green as generated; test your own components beside it.",
32508
32549
  "",
32509
32550
  "The project is generated **untagged**: Nx tags belong to your `depConstraints`, and inventing",
32510
32551
  "one would fail a lint policy you never opted this project into. If your workspace enforces",
@@ -32758,6 +32799,31 @@ describe('layout', () => {
32758
32799
  });
32759
32800
  `;
32760
32801
  }
32802
+ function appSpec() {
32803
+ return `import { TestBed } from '@angular/core/testing';
32804
+ import { App } from './app';
32805
+ import { appConfig } from './app.config';
32806
+
32807
+ /* The application's starter test, rewritten for a root component that renders the shell: it boots
32808
+ with the composition root's own providers, so whatever you compose in is what the test boots
32809
+ with, and it asserts only that the shell mounted. */
32810
+ describe('App', () => {
32811
+ beforeEach(async () => {
32812
+ await TestBed.configureTestingModule({
32813
+ imports: [App],
32814
+ providers: appConfig.providers,
32815
+ }).compileComponents();
32816
+ });
32817
+
32818
+ it('boots the shell', async () => {
32819
+ const fixture = TestBed.createComponent(App);
32820
+ await fixture.whenStable();
32821
+ const compiled = fixture.nativeElement as HTMLElement;
32822
+ expect(compiled.querySelector('lw-shell')).toBeTruthy();
32823
+ });
32824
+ });
32825
+ `;
32826
+ }
32761
32827
  function indexHtml(d) {
32762
32828
  return `<!DOCTYPE html>
32763
32829
  <html lang="en">
@@ -32884,7 +32950,10 @@ var angularDistribution = {
32884
32950
  return {
32885
32951
  "src/main.ts": mainTs(),
32886
32952
  "src/app/app.config.ts": appConfigTs(d),
32887
- ...d.withTests && { "src/app/app.config.spec.ts": appConfigSpec() },
32953
+ ...d.withTests && {
32954
+ "src/app/app.config.spec.ts": appConfigSpec(),
32955
+ "src/app/app.spec.ts": appSpec()
32956
+ },
32888
32957
  "src/app/app.ts": appTs(),
32889
32958
  "src/app/app.html": appHtml(),
32890
32959
  "src/index.html": indexHtml(d),
@@ -33124,6 +33193,8 @@ function bootstrapFile(t) {
33124
33193
  --lw-on-info: #000;
33125
33194
 
33126
33195
  --lw-scrim: rgb(0 0 0 / 0.5);
33196
+ --lw-scroll-thumb: rgb(0 0 0 / 0.3);
33197
+ --lw-scroll-track: transparent;
33127
33198
 
33128
33199
  --lw-font-sans: var(--bs-body-font-family);
33129
33200
  --lw-font-mono: var(--bs-font-monospace);
@@ -33797,6 +33868,184 @@ function validateCatalog(catalog, known = KNOWN_CAPABILITIES) {
33797
33868
  return findings;
33798
33869
  }
33799
33870
 
33871
+ // ../devkit/src/lib/validate/commands.ts
33872
+ var RUNTIME_NOTE = "Grants, access and the window decide what is finally offered at runtime; this check judged the registrations alone.";
33873
+ function validateCommands(sources, ts) {
33874
+ const findings = [];
33875
+ for (const source of sources) {
33876
+ const file2 = ts.createSourceFile(source.path, source.text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
33877
+ for (const call of registerCommandCalls(ts, file2)) {
33878
+ findings.push(...judge(read(ts, file2, call)));
33879
+ }
33880
+ }
33881
+ findings.push({ level: "info", code: "commands.runtime", message: RUNTIME_NOTE });
33882
+ return findings;
33883
+ }
33884
+ function registerCommandCalls(ts, file2) {
33885
+ const calls = [];
33886
+ const visit = (node) => {
33887
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "registerCommand") {
33888
+ calls.push(node);
33889
+ }
33890
+ ts.forEachChild(node, visit);
33891
+ };
33892
+ visit(file2);
33893
+ return calls;
33894
+ }
33895
+ function read(ts, file2, call) {
33896
+ const line = file2.getLineAndCharacterOfPosition(call.getStart(file2)).line + 1;
33897
+ const at2 = `${file2.fileName}:${line}`;
33898
+ const literal2 = call.arguments[0];
33899
+ const empty = { at: at2, callable: false, described: false, answers: false, returnsValue: false, arguments: [] };
33900
+ if (!literal2 || !ts.isObjectLiteralExpression(literal2)) {
33901
+ return { ...empty, unreadable: "its argument is not an object literal" };
33902
+ }
33903
+ const properties = /* @__PURE__ */ new Map();
33904
+ for (const property of literal2.properties) {
33905
+ if (ts.isSpreadAssignment(property)) {
33906
+ return { ...empty, unreadable: "it spreads another value into the registration" };
33907
+ }
33908
+ if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
33909
+ properties.set(property.name.text, property.initializer);
33910
+ } else if (ts.isMethodDeclaration(property) && ts.isIdentifier(property.name)) {
33911
+ properties.set(property.name.text, property);
33912
+ }
33913
+ }
33914
+ const id = properties.get("id");
33915
+ if (!id || !ts.isStringLiteral(id)) {
33916
+ return { ...empty, unreadable: "its id is not a string literal" };
33917
+ }
33918
+ const callable = properties.get("callable");
33919
+ return {
33920
+ at: at2,
33921
+ id: id.text,
33922
+ callable: callable !== void 0 && callable.kind === ts.SyntaxKind.TrueKeyword,
33923
+ described: properties.has("description"),
33924
+ answers: properties.has("answers"),
33925
+ returnsValue: returnsValue(ts, properties.get("run")),
33926
+ arguments: readArguments(ts, properties.get("arguments"))
33927
+ };
33928
+ }
33929
+ function readArguments(ts, value) {
33930
+ if (!value || !ts.isArrayLiteralExpression(value)) {
33931
+ return [];
33932
+ }
33933
+ return value.elements.map((element, index) => {
33934
+ if (!ts.isObjectLiteralExpression(element)) {
33935
+ return { name: `#${index + 1}`, described: false };
33936
+ }
33937
+ let name = `#${index + 1}`;
33938
+ let described = false;
33939
+ for (const property of element.properties) {
33940
+ if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
33941
+ continue;
33942
+ }
33943
+ if (property.name.text === "name" && ts.isStringLiteral(property.initializer)) {
33944
+ name = property.initializer.text;
33945
+ }
33946
+ if (property.name.text === "description") {
33947
+ described = true;
33948
+ }
33949
+ }
33950
+ return { name, described };
33951
+ });
33952
+ }
33953
+ function returnsValue(ts, run) {
33954
+ if (!run) {
33955
+ return false;
33956
+ }
33957
+ if (ts.isArrowFunction(run) && !ts.isBlock(run.body)) {
33958
+ return ts.isObjectLiteralExpression(run.body) || ts.isArrayLiteralExpression(run.body);
33959
+ }
33960
+ let found = false;
33961
+ const visit = (node) => {
33962
+ if (found || ts.isFunctionLike(node) && node !== run) {
33963
+ return;
33964
+ }
33965
+ if (ts.isReturnStatement(node) && node.expression && node.expression.kind !== ts.SyntaxKind.UndefinedKeyword) {
33966
+ found = true;
33967
+ return;
33968
+ }
33969
+ ts.forEachChild(node, visit);
33970
+ };
33971
+ visit(run);
33972
+ return found;
33973
+ }
33974
+ function judge(registration) {
33975
+ const { at: at2, id } = registration;
33976
+ if (registration.unreadable) {
33977
+ return [
33978
+ {
33979
+ level: "info",
33980
+ code: "command.unreadable",
33981
+ path: at2,
33982
+ message: `A registerCommand call at ${at2} could not be read: ${registration.unreadable}. What it offers an agent is unknown here.`
33983
+ }
33984
+ ];
33985
+ }
33986
+ if (!registration.callable) {
33987
+ return [
33988
+ {
33989
+ level: "info",
33990
+ code: "command.private",
33991
+ path: at2,
33992
+ message: `${id}: not offered to an agent, because it is not callable; an agent never sees it.`
33993
+ }
33994
+ ];
33995
+ }
33996
+ const findings = [];
33997
+ if (!registration.described) {
33998
+ findings.push({
33999
+ level: "warning",
34000
+ code: "command.description",
34001
+ path: at2,
34002
+ message: `${id}: offered to an agent without a description; the agent sees only the id and has to guess what it does.`
34003
+ });
34004
+ }
34005
+ for (const argument of registration.arguments) {
34006
+ if (!argument.described) {
34007
+ findings.push({
34008
+ level: "info",
34009
+ code: "command.argument",
34010
+ path: at2,
34011
+ message: `${id}: argument "${argument.name}" has no description; an agent sees a bare name and guesses its value.`
34012
+ });
34013
+ }
34014
+ }
34015
+ if (registration.returnsValue && !registration.answers) {
34016
+ findings.push({
34017
+ level: "info",
34018
+ code: "command.answers",
34019
+ path: at2,
34020
+ message: `${id}: run returns a value but declares no answers; an agent hears that the command ran, not what it answered.`
34021
+ });
34022
+ }
34023
+ if (findings.length === 0) {
34024
+ const count = registration.arguments.length;
34025
+ findings.push({
34026
+ level: "info",
34027
+ code: "command.offered",
34028
+ path: at2,
34029
+ message: `${id}: offered to an agent, with ${count} described argument${count === 1 ? "" : "s"}${registration.answers ? " and a declared answer" : ""}.`
34030
+ });
34031
+ }
34032
+ return findings;
34033
+ }
34034
+
34035
+ // ../devkit/src/lib/validate/typescript.ts
34036
+ import { createRequire } from "node:module";
34037
+ import { resolve } from "node:path";
34038
+ function loadTypeScript(fromDir) {
34039
+ for (const from of [resolve(fromDir, "package.json"), resolve(process.cwd(), "package.json")]) {
34040
+ try {
34041
+ return createRequire(from)("typescript");
34042
+ } catch {
34043
+ continue;
34044
+ }
34045
+ }
34046
+ return null;
34047
+ }
34048
+
33800
34049
  // src/lib/tools.ts
33801
34050
  function ok(data) {
33802
34051
  return {
@@ -33852,6 +34101,23 @@ function validateI18nTool(a) {
33852
34101
  const bundles = a["bundles"] ?? {};
33853
34102
  return ok({ findings: validateI18nParity(bundles) });
33854
34103
  }
34104
+ function validateCommandsTool(a) {
34105
+ const files = a["files"] ?? {};
34106
+ const ts = loadTypeScript(process.cwd());
34107
+ if (!ts) {
34108
+ return ok({
34109
+ findings: [
34110
+ {
34111
+ level: "error",
34112
+ code: "commands.typescript",
34113
+ message: "typescript is not installed where this server runs; the check reads sources with the TypeScript compiler API, so start the server inside the project."
34114
+ }
34115
+ ]
34116
+ });
34117
+ }
34118
+ const sources = Object.entries(files).map(([path, text]) => ({ path, text }));
34119
+ return ok({ findings: validateCommands(sources, ts), note: RUNTIME_NOTE });
34120
+ }
33855
34121
 
33856
34122
  // src/lib/server.ts
33857
34123
  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.';
@@ -33873,7 +34139,7 @@ function inputSchema(descriptor) {
33873
34139
  function createMcpServer() {
33874
34140
  const server = new McpServer({
33875
34141
  name: "loomweaver-devkit",
33876
- version: "0.8.3"
34142
+ version: "0.9.0-preview.2"
33877
34143
  });
33878
34144
  server.registerTool(
33879
34145
  "list_generators",
@@ -33915,6 +34181,16 @@ function createMcpServer() {
33915
34181
  },
33916
34182
  async (args) => validateI18nTool(args)
33917
34183
  );
34184
+ server.registerTool(
34185
+ "validate_commands",
34186
+ {
34187
+ description: "Say, per command a plugin registers, whether an agent is offered it and what would leave the agent guessing. Input: files = { <path>: <TypeScript source> }. Only a callable command without a description is a warning; the rest is information.",
34188
+ inputSchema: {
34189
+ files: external_exports.record(external_exports.string(), external_exports.string())
34190
+ }
34191
+ },
34192
+ async (args) => validateCommandsTool(args)
34193
+ );
33918
34194
  server.registerTool(
33919
34195
  "validate_catalog",
33920
34196
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomweaver/mcp",
3
- "version": "0.8.3",
3
+ "version": "0.9.0-preview.2",
4
4
  "description": "LoomWeaver: open-source plugin platform for Angular workbenches. The MCP server: scaffolding and validation as tools, so an AI assistant in any product repo can build weavers without a checkout.",
5
5
  "keywords": [
6
6
  "loomweaver",