@happyvertical/smrt-cli 0.52.0 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/AGENTS.md +1 -0
  2. package/README.md +1 -0
  3. package/dist/{ApiKey-BZmbOTnL-CMUo3WLf.js → ApiKey-BZmbOTnL-CNdRPt4L.js} +2 -2
  4. package/dist/ApiKeyCollection-Bfb_nw81-C41xeWjP.js +2 -0
  5. package/dist/AuditLogCollection-B1YonCAP-CuxT42P9.js +2 -0
  6. package/dist/{NostrIdentity-n70E0vck-BD7uutxz.js → NostrIdentity-n70E0vck-DyvmAKWA.js} +3 -3
  7. package/dist/NostrIdentityCollection-Ca74dkaL-BYOCASkU.js +2 -0
  8. package/dist/{OidcIdentity-Cb1bxreM-DY0aRLu-.js → OidcIdentity-Cb1bxreM-EFGNe-9i.js} +2 -2
  9. package/dist/OidcIdentityCollection-TV8VdK69-DMFHjlKf.js +2 -0
  10. package/dist/ProfileAssetCollection-B9hacUJa-B1DI2fld.js +2 -0
  11. package/dist/{ProfileCollection-DuJT7Mum-D_ScbSMu.js → ProfileCollection-DuJT7Mum-CjXI7sxS.js} +26 -26
  12. package/dist/ProfileLinkCollection-4uNURz32-BSG4g55p.js +2 -0
  13. package/dist/ProfileMetadataCollection-DQhbfEfa-BmQ59oGm.js +2 -0
  14. package/dist/{ProfileMetafieldCollection-CoBuM_5G-BDwo4x3z.js → ProfileMetafieldCollection-CoBuM_5G-Cid3HNFI.js} +1 -1
  15. package/dist/{ProfileRelationshipCollection-DUVgaIqf-BRrBeg1Q.js → ProfileRelationshipCollection-DUVgaIqf-D-VeP3Xp.js} +1 -1
  16. package/dist/{ProfileRelationshipTermCollection-DtA9OM23-D4WMfN9a.js → ProfileRelationshipTermCollection-DtA9OM23-BscMcndN.js} +1 -1
  17. package/dist/{ProfileRelationshipType-BRiRka6t-B32A4UFT.js → ProfileRelationshipType-BRiRka6t-BP33exAz.js} +1 -1
  18. package/dist/{ProfileRelationshipTypeCollection-BdXwd7Kf-BqQa8Awc.js → ProfileRelationshipTypeCollection-BdXwd7Kf-CSMHKaaN.js} +1 -1
  19. package/dist/cli-generator.d.ts.map +1 -1
  20. package/dist/commands/db-qualified-names.d.ts +39 -0
  21. package/dist/commands/db-qualified-names.d.ts.map +1 -0
  22. package/dist/commands/gnode.d.ts.map +1 -1
  23. package/dist/commands/index.d.ts +1 -0
  24. package/dist/commands/index.d.ts.map +1 -1
  25. package/dist/commands/mcp-apps-packaging.d.ts +24 -0
  26. package/dist/commands/mcp-apps-packaging.d.ts.map +1 -0
  27. package/dist/commands/utilities.d.ts.map +1 -1
  28. package/dist/{commands-BvcMfEoO.js → commands-DISJzBlT.js} +487 -6
  29. package/dist/{dist-Dm4q4uB4.js → dist-7S5I_gWk.js} +4 -4
  30. package/dist/{dist-BTxdRxKP.js → dist-BGMJi2g1.js} +2 -2
  31. package/dist/{dist-qJw7ALwV.js → dist-BZd_JjEL.js} +10 -6
  32. package/dist/{dist-vJJL0WMc.js → dist-Ws7B05wT.js} +26 -26
  33. package/dist/index.js +17 -11
  34. package/dist/{oidcProvisioningPrimitives-D9OYu5-F-D9DQdiwS.js → oidcProvisioningPrimitives-D9OYu5-F-BqS3T8Kt.js} +4 -4
  35. package/dist/resolveIdentity-B-PK6RwU-cgVjM6Kk.js +2 -0
  36. package/package.json +16 -15
  37. package/dist/ApiKeyCollection-Bfb_nw81-C3Xz7MUq.js +0 -2
  38. package/dist/AuditLogCollection-B1YonCAP-DmmboC_X.js +0 -2
  39. package/dist/NostrIdentityCollection-Ca74dkaL--KYlM_ou.js +0 -2
  40. package/dist/OidcIdentityCollection-TV8VdK69-DPeFJt3R.js +0 -2
  41. package/dist/ProfileAssetCollection-B9hacUJa-6R8lwjti.js +0 -2
  42. package/dist/ProfileLinkCollection-4uNURz32-lMKBjaik.js +0 -2
  43. package/dist/ProfileMetadataCollection-DQhbfEfa-B1QlmBkX.js +0 -2
  44. package/dist/resolveIdentity-B-PK6RwU-Dvqeq9-I.js +0 -2
@@ -2,7 +2,7 @@ import { t as resolveManifestEntryPackageName } from "./index.js";
2
2
  import { t as DEFAULT_CLI_CONFIG } from "./config-hqNo4lzH.js";
3
3
  import { createRequire } from "node:module";
4
4
  import * as fs from "node:fs";
5
- import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
5
+ import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -21,9 +21,10 @@ import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
21
21
  import https from "node:https";
22
22
  import { homedir, tmpdir } from "node:os";
23
23
  import { extract } from "tar";
24
+ import Ajv2020 from "ajv/dist/2020.js";
24
25
  import { importWorkspaceModule } from "@happyvertical/smrt-core/utils/import-workspace-module";
25
26
  import { renderForeignKeyAddStatements } from "@happyvertical/smrt-core/schema";
26
- import { FRAMEWORK_BASE_TABLE_NAMES, buildIntegerWidthTableStatements, collectIntegerWidthTargets, collectNullEqualIndexTargets, dropFrameworkBaseTables, migrateNullEqualIndexes, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, preflightIntegerWidthWidening, preflightNullEqualIndexes, widenIntegerColumnsToBigInt } from "@happyvertical/smrt-core/migrations";
27
+ import { FRAMEWORK_BASE_TABLE_NAMES, backfillLegacyQualifiedNames, buildIntegerWidthTableStatements, collectIntegerWidthTargets, collectNullEqualIndexTargets, countLegacyQualifiedNameReferences, dropFrameworkBaseTables, migrateNullEqualIndexes, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, preflightIntegerWidthWidening, preflightNullEqualIndexes, widenIntegerColumnsToBigInt } from "@happyvertical/smrt-core/migrations";
27
28
  import { buildArchitectureContext, buildKnowledgeIndex, buildReviewContext, checkKnowledgeFreshness, compactContextResult, diffKnowledgeIndex, renderFreshnessResult, renderKnowledgeIndexMarkdown } from "@happyvertical/smrt-dev-mcp/knowledge";
28
29
  import { RUNTIME_TOOLS } from "@happyvertical/smrt-dev-mcp/runtime";
29
30
  //#region src/commands/db-command-utils.ts
@@ -5683,6 +5684,19 @@ async function loadGitTemplate(gitUrl) {
5683
5684
  }
5684
5685
  }
5685
5686
  /**
5687
+ * Get the template directory path (for copying overlay files)
5688
+ *
5689
+ * Prefers the subdir-resolved `__templateRoot` so that subdirectory templates
5690
+ * (`github:user/repo/subdir`) overlay from the subdir rather than the repo root.
5691
+ * Falls back to `__tempDir` for configs loaded before the field existed.
5692
+ */
5693
+ function getGitTemplateDir(config) {
5694
+ const gitConfig = config;
5695
+ const templateRoot = gitConfig.__templateRoot ?? gitConfig.__tempDir;
5696
+ if (!templateRoot) throw new Error("Template was not loaded from git repository");
5697
+ return templateRoot;
5698
+ }
5699
+ /**
5686
5700
  * Cleanup temporary directory after template is used
5687
5701
  */
5688
5702
  async function cleanupGitTemplate(config) {
@@ -6231,8 +6245,310 @@ async function processPlaceholders(outputDir, config, options) {
6231
6245
  console.log(` Processed placeholders in ${replacedCount} files`);
6232
6246
  }
6233
6247
  //#endregion
6248
+ //#region src/commands/mcp-apps-packaging.ts
6249
+ /**
6250
+ * Portable MCP Apps plugin packaging helpers.
6251
+ *
6252
+ * These helpers intentionally validate distribution metadata only. Runtime
6253
+ * MCP extension metadata is owned by the v2 server adapter and is not an
6254
+ * install-plugin manifest.
6255
+ */
6256
+ var requireSchema = createRequire(import.meta.url);
6257
+ var pluginSchema = requireSchema("@happyvertical/smrt-dev-mcp/schemas/agent-plugins-1.0.0/plugin.schema.json");
6258
+ var mcpSchema = requireSchema("@happyvertical/smrt-dev-mcp/schemas/agent-plugins-1.0.0/mcp.schema.json");
6259
+ var ajv = new Ajv2020({
6260
+ strict: true,
6261
+ allErrors: true
6262
+ });
6263
+ var validatePluginSchema = ajv.compile(pluginSchema);
6264
+ var validateMcpSchema = ajv.compile(mcpSchema);
6265
+ var PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
6266
+ var MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
6267
+ var CANONICAL_SCOPE_REGISTRY = "@happyvertical:registry=https://npm.happyvertical.com/";
6268
+ var SECRET_KEY = /(?:api[_-]?key|secret|token|password|authorization|credential)/i;
6269
+ var ENVIRONMENT_CREDENTIAL_FILE = /^\.env(?:\.|$)/;
6270
+ var PRIVATE_KEY_CARRIER = /^(?:id_(?:rsa|dsa|ecdsa|ed25519)|private(?:[._-]key)?|.*\.key)$/i;
6271
+ var PRIVATE_KEY_HEADER = /-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----/;
6272
+ function add(result, code, message) {
6273
+ result.findings.push({
6274
+ code,
6275
+ message
6276
+ });
6277
+ }
6278
+ function readJson$1(path, result) {
6279
+ try {
6280
+ return JSON.parse(readFileSync(path, "utf8"));
6281
+ } catch (error) {
6282
+ add(result, "json-malformed", `${relative(process.cwd(), path)} is not valid JSON`);
6283
+ return;
6284
+ }
6285
+ }
6286
+ function isObject(value) {
6287
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6288
+ }
6289
+ function safeRelativePath(value, field, root, result) {
6290
+ if (typeof value !== "string" || !value.startsWith("./") || value.includes("\\") || value.split("/").includes("..")) {
6291
+ add(result, "unsafe-path", `${field} must be a root-relative ./ path without parent segments`);
6292
+ return;
6293
+ }
6294
+ const target = resolve(root, value);
6295
+ if (!target.startsWith(`${root}${sep}`)) {
6296
+ add(result, "unsafe-path", `${field} resolves outside the plugin root`);
6297
+ return;
6298
+ }
6299
+ try {
6300
+ const stat = lstatSync(target);
6301
+ if (stat.isSymbolicLink() || !stat.isFile()) add(result, "invalid-resource", `${field} must name a regular file`);
6302
+ } catch {
6303
+ add(result, "missing-resource", `${field} does not exist`);
6304
+ }
6305
+ }
6306
+ function scanTree(root, current, result) {
6307
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
6308
+ const path = join(current, entry.name);
6309
+ const display = relative(root, path);
6310
+ const stat = lstatSync(path);
6311
+ if (stat.isSymbolicLink()) add(result, "symlink", `${display} is a symbolic link; plugin packages must contain regular files`);
6312
+ else if (stat.isDirectory()) scanTree(root, path, result);
6313
+ else if (stat.isFile()) if (ENVIRONMENT_CREDENTIAL_FILE.test(entry.name) || PRIVATE_KEY_CARRIER.test(entry.name)) add(result, "secret-artifact", `${display} is a credential carrier and cannot be packaged`);
6314
+ else try {
6315
+ if (PRIVATE_KEY_HEADER.test(readFileSync(path, "utf8"))) add(result, "secret-artifact", `${display} contains a private key and cannot be packaged`);
6316
+ } catch {
6317
+ add(result, "artifact-unreadable", `${display} could not be inspected and cannot be packaged`);
6318
+ }
6319
+ }
6320
+ }
6321
+ function scanSecrets(value, source, result) {
6322
+ if (Array.isArray(value)) {
6323
+ value.forEach((item) => {
6324
+ scanSecrets(item, source, result);
6325
+ });
6326
+ return;
6327
+ }
6328
+ if (!isObject(value)) return;
6329
+ for (const [key, child] of Object.entries(value)) {
6330
+ if (SECRET_KEY.test(key) && typeof child === "string" && child.trim()) add(result, "secret-artifact", `${source} contains a credential-shaped ${key} field`);
6331
+ scanSecrets(child, source, result);
6332
+ }
6333
+ }
6334
+ function validateServerUrl(value, name, result) {
6335
+ if (typeof value !== "string") return add(result, "server-url", `mcpServers.${name}.url must be a string`);
6336
+ try {
6337
+ const url = new URL(value);
6338
+ const isLoopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]";
6339
+ if (url.username || url.password) add(result, "server-url", `mcpServers.${name}.url must not contain credentials`);
6340
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) add(result, "server-url", `mcpServers.${name}.url must use HTTPS, except loopback development URLs`);
6341
+ } catch {
6342
+ add(result, "server-url", `mcpServers.${name}.url is not a URL`);
6343
+ }
6344
+ }
6345
+ /** Validate portable plugin files and the optional OpenAI install extension. */
6346
+ function validateMcpAppsPackage(rootPath) {
6347
+ const root = resolve(rootPath);
6348
+ const result = {
6349
+ findings: [],
6350
+ valid: false
6351
+ };
6352
+ try {
6353
+ scanTree(root, root, result);
6354
+ } catch {
6355
+ add(result, "plugin-root", "Plugin root must be a readable directory");
6356
+ return result;
6357
+ }
6358
+ const pluginPath = join(root, "plugin.json");
6359
+ const mcpPath = join(root, "mcp.json");
6360
+ if (!lstatExists(pluginPath)) add(result, "plugin-missing", "plugin.json is required at the plugin root");
6361
+ if (!lstatExists(mcpPath)) add(result, "mcp-missing", "mcp.json is required at the plugin root");
6362
+ const plugin = lstatExists(pluginPath) ? readJson$1(pluginPath, result) : void 0;
6363
+ const mcp = lstatExists(mcpPath) ? readJson$1(mcpPath, result) : void 0;
6364
+ if (plugin !== void 0 && !isObject(plugin)) add(result, "plugin-object", "plugin.json must contain an object");
6365
+ if (mcp !== void 0 && !isObject(mcp)) add(result, "mcp-object", "mcp.json must contain an object");
6366
+ if (plugin !== void 0 && !validatePluginSchema(plugin)) add(result, "plugin-format", "plugin.json violates the pinned Agent Plugins 1.0.0 schema");
6367
+ if (mcp !== void 0 && !validateMcpSchema(mcp)) add(result, "mcp-format", "mcp.json violates the pinned Agent Plugins 1.0.0 schema");
6368
+ if (isObject(plugin)) {
6369
+ scanSecrets(plugin, "plugin.json", result);
6370
+ if (plugin.$schema !== PLUGIN_SCHEMA) add(result, "plugin-schema", `plugin.json.$schema must be ${PLUGIN_SCHEMA}`);
6371
+ if (plugin.extensions !== void 0 && !isObject(plugin.extensions)) add(result, "extensions", "plugin.json.extensions must be an object");
6372
+ const openai = (isObject(plugin.extensions) ? plugin.extensions : void 0)?.["com.openai"];
6373
+ if (openai !== void 0 && !isObject(openai)) add(result, "openai-extension", "extensions.com.openai must be an object");
6374
+ if (isObject(openai)) {
6375
+ if (openai.apps !== void 0) safeRelativePath(openai.apps, "extensions.com.openai.apps", root, result);
6376
+ const ui = openai.interface;
6377
+ if (ui !== void 0 && !isObject(ui)) add(result, "openai-interface", "extensions.com.openai.interface must be an object");
6378
+ if (isObject(ui)) {
6379
+ for (const key of ["composerIcon", "logo"]) if (ui[key] !== void 0) safeRelativePath(ui[key], `extensions.com.openai.interface.${key}`, root, result);
6380
+ if (ui.screenshots !== void 0) if (!Array.isArray(ui.screenshots) || ui.screenshots.some((path) => typeof path !== "string")) add(result, "openai-screenshots", "interface.screenshots must be an array of paths");
6381
+ else ui.screenshots.forEach((path, index) => {
6382
+ safeRelativePath(path, `extensions.com.openai.interface.screenshots[${index}]`, root, result);
6383
+ });
6384
+ }
6385
+ }
6386
+ }
6387
+ if (isObject(mcp)) {
6388
+ scanSecrets(mcp, "mcp.json", result);
6389
+ if (mcp.$schema !== MCP_SCHEMA) add(result, "mcp-schema", `mcp.json.$schema must be ${MCP_SCHEMA}`);
6390
+ if (!isObject(mcp.mcpServers) || Object.keys(mcp.mcpServers).length === 0) add(result, "mcp-servers", "mcp.json requires a non-empty mcpServers object");
6391
+ else for (const [name, server] of Object.entries(mcp.mcpServers)) {
6392
+ if (!name.trim() || !isObject(server)) {
6393
+ add(result, "mcp-server", `mcpServers.${name || "<empty>"} must be an object with a name`);
6394
+ continue;
6395
+ }
6396
+ if (server.type !== "streamable-http") add(result, "mcp-transport", `mcpServers.${name}.type must be streamable-http`);
6397
+ validateServerUrl(server.url, name, result);
6398
+ }
6399
+ }
6400
+ result.valid = result.findings.length === 0;
6401
+ return result;
6402
+ }
6403
+ function lstatExists(path) {
6404
+ try {
6405
+ return lstatSync(path).isFile();
6406
+ } catch {
6407
+ return false;
6408
+ }
6409
+ }
6410
+ /**
6411
+ * Add the public, credential-free SMRT package route to a generated app.
6412
+ * A pre-existing scope route is an explicit consumer decision and must not be
6413
+ * silently replaced.
6414
+ */
6415
+ function configureMcpAppsConsumerRegistry(appRoot) {
6416
+ const npmrcPath = join(resolve(appRoot), ".npmrc");
6417
+ let existing = "";
6418
+ try {
6419
+ const stat = lstatSync(npmrcPath);
6420
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new Error("Generated application .npmrc must be a regular file");
6421
+ existing = readFileSync(npmrcPath, "utf8");
6422
+ } catch (error) {
6423
+ if (error.code !== "ENOENT") throw error;
6424
+ }
6425
+ const scopeLines = existing.split(/\r?\n/).filter((line) => /^\s*@happyvertical:registry(?:\s|=|$)/.test(line));
6426
+ if (scopeLines.length > 0) {
6427
+ for (const line of scopeLines) {
6428
+ const match = /^\s*@happyvertical:registry\s*=\s*(\S+?)(?:\s+[;#].*)?\s*$/.exec(line);
6429
+ if (!match || match[1] !== "https://npm.happyvertical.com/") throw new Error("Generated application already declares a different or malformed @happyvertical registry; refusing to replace it");
6430
+ }
6431
+ return;
6432
+ }
6433
+ writeFileSync(npmrcPath, `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${CANONICAL_SCOPE_REGISTRY}\n`);
6434
+ }
6435
+ /** Copy the opt-in app route/resources and merge only its declared dependencies. */
6436
+ function addMcpAppsRuntime(sourceRoot, appRoot) {
6437
+ const resolvedSource = resolve(sourceRoot);
6438
+ const source = join(lstatSync(resolvedSource).isFile() ? dirname(resolvedSource) : resolvedSource, "mcp-apps-template");
6439
+ const dependencies = JSON.parse(readFileSync(join(source, "package.dependencies.json"), "utf8"));
6440
+ cpSync(join(source, "src"), join(resolve(appRoot), "src"), {
6441
+ recursive: true,
6442
+ force: false,
6443
+ errorOnExist: true
6444
+ });
6445
+ const packagePath = join(resolve(appRoot), "package.json");
6446
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
6447
+ packageJson.dependencies = {
6448
+ ...packageJson.dependencies,
6449
+ ...dependencies
6450
+ };
6451
+ writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
6452
+ }
6453
+ function assertMcpAppsPluginName(name) {
6454
+ if (!validatePluginSchema({
6455
+ $schema: PLUGIN_SCHEMA,
6456
+ name
6457
+ })) throw new Error("Plugin name must satisfy Agent Plugins 1.0.0 (1–64 lowercase letters, digits, dots or hyphens; no repeated separators).");
6458
+ }
6459
+ /** Create the minimal local portable package. It intentionally has no .app.json: registered IDs are host-owned. */
6460
+ function scaffoldMcpAppsPackage(rootPath, name, version = "0.1.0") {
6461
+ assertMcpAppsPluginName(name);
6462
+ const root = resolve(rootPath);
6463
+ mkdirSync(root, { recursive: true });
6464
+ const pluginPath = join(root, "plugin.json");
6465
+ const mcpPath = join(root, "mcp.json");
6466
+ if (lstatExists(pluginPath) || lstatExists(mcpPath)) throw new Error("Refusing to overwrite existing MCP Apps package files");
6467
+ writeFileSync(pluginPath, `${JSON.stringify({
6468
+ $schema: PLUGIN_SCHEMA,
6469
+ name,
6470
+ version,
6471
+ description: "SMRT MCP Apps package",
6472
+ extensions: { "com.openai": { interface: {
6473
+ displayName: name,
6474
+ shortDescription: "SMRT MCP Apps package"
6475
+ } } }
6476
+ }, null, 2)}\n`);
6477
+ writeFileSync(mcpPath, `${JSON.stringify({
6478
+ $schema: MCP_SCHEMA,
6479
+ mcpServers: { smrt: {
6480
+ type: "streamable-http",
6481
+ url: "http://127.0.0.1:3000/api/mcp"
6482
+ } }
6483
+ }, null, 2)}\n`);
6484
+ }
6485
+ var mcpAppsCommands = {
6486
+ "mcp-apps scaffold": {
6487
+ name: "mcp-apps scaffold",
6488
+ description: "Create opt-in portable MCP Apps plugin metadata for a local SMRT app",
6489
+ options: {
6490
+ "output-dir": {
6491
+ type: "string",
6492
+ description: "Plugin directory (defaults to ./mcp-apps)"
6493
+ },
6494
+ name: {
6495
+ type: "string",
6496
+ description: "Portable plugin name",
6497
+ default: "smrt-app"
6498
+ },
6499
+ version: {
6500
+ type: "string",
6501
+ description: "Portable plugin version",
6502
+ default: "0.1.0"
6503
+ }
6504
+ },
6505
+ handler: async (_args, options) => {
6506
+ const root = typeof options["output-dir"] === "string" ? options["output-dir"] : "./mcp-apps";
6507
+ scaffoldMcpAppsPackage(root, typeof options.name === "string" ? options.name : "smrt-app", typeof options.version === "string" ? options.version : "0.1.0");
6508
+ console.log(`Created portable MCP Apps metadata in ${root}.`);
6509
+ console.log("Mount and authorize the native v2 /api/mcp endpoint before changing the loopback URL.");
6510
+ }
6511
+ },
6512
+ "mcp-apps validate": {
6513
+ name: "mcp-apps validate",
6514
+ description: "Fail closed when portable MCP Apps package metadata is unsafe or incomplete",
6515
+ options: { "output-dir": {
6516
+ type: "string",
6517
+ description: "Plugin directory (defaults to ./mcp-apps)"
6518
+ } },
6519
+ handler: async (_args, options) => {
6520
+ const root = typeof options["output-dir"] === "string" ? options["output-dir"] : "./mcp-apps";
6521
+ const result = validateMcpAppsPackage(root);
6522
+ for (const finding of result.findings) console.error(`${finding.code}: ${finding.message}`);
6523
+ if (!result.valid) throw new Error(`MCP Apps package validation failed with ${result.findings.length} finding(s)`);
6524
+ console.log(`MCP Apps package at ${root} is valid.`);
6525
+ }
6526
+ }
6527
+ };
6528
+ //#endregion
6234
6529
  //#region src/commands/gnode.ts
6235
6530
  /**
6531
+ * Gnode CLI Commands
6532
+ *
6533
+ * Commands for creating and managing gnodes (federated local knowledge bases)
6534
+ */
6535
+ var SCOPED_PACKAGE_NAME = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/;
6536
+ function projectIdentity(name, outputDir) {
6537
+ const packagePath = join(outputDir, "package.json");
6538
+ if (existsSync(packagePath)) {
6539
+ const existing = JSON.parse(readFileSync(packagePath, "utf8"));
6540
+ if (typeof existing.name !== "string" || !SCOPED_PACKAGE_NAME.test(existing.name)) throw new Error("Generated applications require a scoped package identity (for example @smrt-app/my-app); rename the existing project before generating into it");
6541
+ return existing.name;
6542
+ }
6543
+ if (SCOPED_PACKAGE_NAME.test(name)) return name;
6544
+ const slug = name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
6545
+ if (!slug) throw new Error("MCP Apps project name must contain a package slug");
6546
+ return `@smrt-app/${slug}`;
6547
+ }
6548
+ function mcpAppsPluginName(packageName) {
6549
+ return packageName.split("/").at(-1) ?? packageName;
6550
+ }
6551
+ /**
6236
6552
  * Gnode commands for CLI
6237
6553
  */
6238
6554
  var gnodeCommands = {
@@ -6266,6 +6582,10 @@ var gnodeCommands = {
6266
6582
  type: "string",
6267
6583
  description: "IANA timezone (e.g., \"America/Edmonton\")",
6268
6584
  default: "America/Edmonton"
6585
+ },
6586
+ "mcp-apps": {
6587
+ type: "boolean",
6588
+ description: "Add opt-in portable MCP Apps plugin metadata under mcp-apps/"
6269
6589
  }
6270
6590
  },
6271
6591
  handler: async (args, options) => {
@@ -6273,26 +6593,38 @@ var gnodeCommands = {
6273
6593
  if (!name) throw new Error("Project name is required: smrt gnode create <name>");
6274
6594
  const outputDir = options.outputDir || `./${name}`;
6275
6595
  const templateName = options.template || "sveltekit";
6596
+ const mcpApps = options.mcpApps || options["mcp-apps"];
6597
+ const projectName = projectIdentity(name, outputDir);
6598
+ if (mcpApps) assertMcpAppsPluginName(mcpAppsPluginName(projectName));
6276
6599
  const siteOptions = options.location || options.lat || options.lon ? {
6277
6600
  location: options.location,
6278
6601
  latitude: options.lat ? Number(options.lat) : void 0,
6279
6602
  longitude: options.lon ? Number(options.lon) : void 0,
6280
6603
  timezone: options.timezone
6281
6604
  } : void 0;
6605
+ let gitConfig;
6282
6606
  try {
6283
6607
  console.log(`🔍 Resolving template: ${templateName}...`);
6284
6608
  const source = await resolveTemplate(templateName);
6285
6609
  console.log(`✓ Found template: ${source.type}:${source.location}\n`);
6286
6610
  const config = await loadTemplate(source);
6611
+ if (source.type === "git") gitConfig = config;
6287
6612
  await generate(source, config, {
6288
- name,
6613
+ name: projectName,
6289
6614
  template: templateName,
6290
6615
  outputDir,
6291
6616
  site: siteOptions
6292
6617
  });
6293
- if (source.type === "git") await cleanupGitTemplate(config);
6618
+ if (mcpApps || config.name === "sveltekit") configureMcpAppsConsumerRegistry(outputDir);
6619
+ if (mcpApps) {
6620
+ addMcpAppsRuntime(source.type === "git" ? getGitTemplateDir(config) : source.resolved, outputDir);
6621
+ scaffoldMcpAppsPackage(`${outputDir}/mcp-apps`, mcpAppsPluginName(projectName));
6622
+ console.log("✓ Added MCP Apps metadata and the canonical @happyvertical package registry. Configure the authorized v2 MCP endpoint before deployment.");
6623
+ }
6294
6624
  } catch (error) {
6295
6625
  throw new Error(`Failed to create gnode: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error });
6626
+ } finally {
6627
+ if (gitConfig) await cleanupGitTemplate(gitConfig);
6296
6628
  }
6297
6629
  }
6298
6630
  },
@@ -7077,7 +7409,7 @@ var dbMaterializeTenantHierarchyCommand = {
7077
7409
  if (!config.database?.url || config.database.url === ":memory:") throw new Error("Database configuration required for db:materialize-tenant-hierarchy. Configure database.url in smrt.config.ts (or DATABASE_URL).");
7078
7410
  const dbType = config.database.type || "sqlite";
7079
7411
  const { getDatabase } = await import("@happyvertical/sql");
7080
- const { materializeTenantHierarchy, TenantHierarchyMaterializationError } = await import("./dist-vJJL0WMc.js");
7412
+ const { materializeTenantHierarchy, TenantHierarchyMaterializationError } = await import("./dist-Ws7B05wT.js");
7081
7413
  db = await getDatabase({
7082
7414
  type: dbType,
7083
7415
  url: config.database.url
@@ -7286,7 +7618,7 @@ var dbMigrateLedgerAccountsCommand = {
7286
7618
  if (dbType !== "postgres") throw new Error("db:migrate-ledger-accounts currently supports PostgreSQL only; no changes were made. See the smrt-ledgers README for SQLite.");
7287
7619
  await autoDiscoverAndLoad();
7288
7620
  const { getDatabase } = await import("@happyvertical/sql");
7289
- const { migrateLedgerAccountsTable, planLedgerAccountsTableMove } = await import("./dist-Dm4q4uB4.js");
7621
+ const { migrateLedgerAccountsTable, planLedgerAccountsTableMove } = await import("./dist-7S5I_gWk.js");
7290
7622
  db = await getDatabase({
7291
7623
  type: dbType,
7292
7624
  url: config.database.url
@@ -8354,6 +8686,139 @@ async function columnIsUuid(db, table, column) {
8354
8686
  return rows[0]?.data_type === "uuid";
8355
8687
  }
8356
8688
  //#endregion
8689
+ //#region src/commands/db-qualified-names.ts
8690
+ /**
8691
+ * Stored references to deprecated qualified names (#3338).
8692
+ *
8693
+ * `smrt doctor --db` reports how many rows still store a name a class
8694
+ * declared in `@smrt({ previousQualifiedNames })` — the signal that an alias
8695
+ * can (or cannot yet) be removed. `smrt db:migrate-qualified-names` is the
8696
+ * opt-in, idempotent cleanup; nothing rewrites stored names automatically.
8697
+ */
8698
+ async function connect() {
8699
+ const { getPackageConfig } = await import("@happyvertical/smrt-config");
8700
+ const { DEFAULT_CLI_CONFIG } = await import("./config-hqNo4lzH.js").then((n) => n.n);
8701
+ const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
8702
+ if (!config.database?.url || config.database.url === ":memory:") throw new Error("No persistent database is configured. Set `database.url` in smrt.config.ts (or DATABASE_URL).");
8703
+ const dbType = config.database.type || "sqlite";
8704
+ const { getDatabase } = await import("@happyvertical/sql");
8705
+ return {
8706
+ db: await getDatabase({
8707
+ type: dbType,
8708
+ url: config.database.url
8709
+ }),
8710
+ dbType,
8711
+ url: config.database.url
8712
+ };
8713
+ }
8714
+ /**
8715
+ * Count stored deprecated qualified names in the configured database.
8716
+ * Never throws for an operational problem.
8717
+ */
8718
+ async function runLegacyQualifiedNameReport(options = {}) {
8719
+ let db;
8720
+ try {
8721
+ if (options.discover ?? true) await autoDiscoverAndLoad();
8722
+ const connection = await connect();
8723
+ db = connection.db;
8724
+ return {
8725
+ report: await countLegacyQualifiedNameReferences(db, { engineHint: connection.dbType }),
8726
+ error: null
8727
+ };
8728
+ } catch (error) {
8729
+ return {
8730
+ report: null,
8731
+ error: redactConnectionStringsInText(error instanceof Error ? error.message : String(error))
8732
+ };
8733
+ } finally {
8734
+ await closeDatabaseConnection(db);
8735
+ }
8736
+ }
8737
+ /**
8738
+ * Render a legacy-name report as the lines `smrt doctor --db` and
8739
+ * `smrt db:migrate-qualified-names` print.
8740
+ *
8741
+ * A zero count is a removal signal only when it is global and complete: a
8742
+ * tenant-scoped count (`tenantId`) or one that skipped untenanted tables
8743
+ * never says the aliases are removable. Stored rows are also only half the
8744
+ * picture, so every report reminds the operator that source still naming an
8745
+ * old name must move first.
8746
+ */
8747
+ function formatLegacyQualifiedNameReport(report, options = {}) {
8748
+ if (report.aliases.length === 0) return [" No class declares previousQualifiedNames."];
8749
+ const scope = options.tenantId !== void 0 ? ` for tenant ${options.tenantId}` : "";
8750
+ const lines = [` Declared aliases: ${report.aliases.length}`, ...report.aliases.map(({ alias, current }) => ` - ${alias} → ${current}`)];
8751
+ if (report.total > 0) {
8752
+ lines.push(` ⚠️ ${report.total} stored reference(s)${scope} still use a deprecated name:`);
8753
+ for (const reference of report.references) lines.push(` - ${reference.table}.${reference.column} = ${reference.alias}: ${reference.count} row(s)`);
8754
+ lines.push(" They keep resolving. Rewrite them early with `smrt db:migrate-qualified-names` (opt-in); keep the aliases until this count is zero.");
8755
+ } else if (options.tenantId !== void 0) lines.push(` ✅ No counted row${scope} uses a deprecated name. This is a tenant-scoped count, not a removal signal: other tenants and untenanted tables were not counted. Run without --tenant (or \`smrt doctor --db\`) before deciding to remove an alias.`);
8756
+ else if (report.untenantedTables.length > 0) lines.push(" ✅ No counted row uses a deprecated name, but some tables were skipped (below), so this is not a removal signal.");
8757
+ else lines.push(" ✅ No stored row in this database uses a deprecated name. The aliases may be removed in a breaking release once every deployment reports zero and no source still names them.");
8758
+ if (report.untenantedTables.length > 0) lines.push(` Skipped (no tenant column, not counted): ${report.untenantedTables.join(", ")}`);
8759
+ if (report.missingTables.length > 0) lines.push(` (not migrated in this database: ${report.missingTables.join(", ")})`);
8760
+ lines.push(" Before removing an alias, also update source still naming an old qualified name (@crossPackageRef / relationship targets, playbook step models) to the current name.");
8761
+ return lines;
8762
+ }
8763
+ /** `smrt db:migrate-qualified-names` — the opt-in rewrite. */
8764
+ var dbMigrateQualifiedNamesCommand = {
8765
+ name: "db:migrate-qualified-names",
8766
+ description: "Rewrite stored deprecated qualified names (declared via @smrt({ previousQualifiedNames })) to current names. Opt-in and idempotent (#3338).",
8767
+ aliases: ["migrate-qualified-names"],
8768
+ args: [],
8769
+ options: {
8770
+ "dry-run": {
8771
+ type: "boolean",
8772
+ description: "Report the rows that would be rewritten without writing.",
8773
+ default: false
8774
+ },
8775
+ tenant: {
8776
+ type: "string",
8777
+ description: "Rewrite only this tenant's rows (tables without a tenant column are skipped)."
8778
+ },
8779
+ force: {
8780
+ type: "boolean",
8781
+ description: "Run again even if this alias set was already recorded.",
8782
+ default: false
8783
+ }
8784
+ },
8785
+ handler: async (_args, options) => {
8786
+ let db;
8787
+ try {
8788
+ await autoDiscoverAndLoad();
8789
+ const connection = await connect();
8790
+ db = connection.db;
8791
+ console.log("\n🏷️ Deprecated qualified-name backfill\n");
8792
+ console.log(`✓ Connected to ${formatDatabaseDisplayUrl(connection.dbType, connection.url)}\n`);
8793
+ const result = await backfillLegacyQualifiedNames(db, {
8794
+ engineHint: connection.dbType,
8795
+ dryRun: options["dry-run"] === true,
8796
+ force: options.force === true,
8797
+ ...options.tenant ? { tenantId: options.tenant } : {}
8798
+ });
8799
+ for (const line of formatLegacyQualifiedNameReport(result.before, { ...options.tenant ? { tenantId: options.tenant } : {} })) console.log(line);
8800
+ console.log();
8801
+ if (result.dryRun) {
8802
+ console.log("DRY RUN: no changes made.\n");
8803
+ return;
8804
+ }
8805
+ if (!result.ran) {
8806
+ console.log(result.before.aliases.length === 0 ? "Nothing to do.\n" : `Already applied (${result.backfillName}); pass --force to run again.\n`);
8807
+ return;
8808
+ }
8809
+ for (const reference of result.rewritten) console.log(`✓ ${reference.table}.${reference.column}: ${reference.count} row(s) ${reference.alias} → ${reference.current}`);
8810
+ for (const reference of result.skippedDuplicates) console.log(`⚠️ ${reference.table}.${reference.column}: ${reference.count} row(s) kept as ${reference.alias} — a row with the same identity already uses ${reference.current}. Resolve the duplicates, then re-run.`);
8811
+ if (result.skippedDuplicates.length > 0) process.exitCode = 1;
8812
+ console.log(result.recorded ? `\n✓ Recorded ${result.backfillName}.\n` : "\nNot recorded: references remain.\n");
8813
+ } catch (error) {
8814
+ console.error(`\n❌ Qualified-name backfill failed: ${redactConnectionStringsInText(error instanceof Error ? error.message : String(error))}\n`);
8815
+ process.exitCode = 1;
8816
+ } finally {
8817
+ await closeDatabaseConnection(db);
8818
+ }
8819
+ }
8820
+ };
8821
+ //#endregion
8357
8822
  //#region src/commands/dev-knowledge.ts
8358
8823
  /**
8359
8824
  * dev:knowledge-* commands
@@ -9547,6 +10012,7 @@ function renderAgentSurfaceReport(report) {
9547
10012
  * Utility commands for CLI
9548
10013
  */
9549
10014
  var utilityCommands = {
10015
+ ...mcpAppsCommands,
9550
10016
  introspect: {
9551
10017
  name: "introspect",
9552
10018
  description: "Analyze project and discover SMRT objects",
@@ -10961,6 +11427,11 @@ export default testManifest;
10961
11427
  check("SMRT objects discovered", false, void 0, "Could not discover manifests");
10962
11428
  }
10963
11429
  const runtimeCheck = await runRuntimeCheckSafely(cwd);
11430
+ const mcpAppsPath = resolve(cwd, "mcp-apps");
11431
+ if (existsSync(mcpAppsPath)) {
11432
+ const mcpApps = validateMcpAppsPackage(mcpAppsPath);
11433
+ check("Portable MCP Apps package", mcpApps.valid, mcpApps.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; "));
11434
+ }
10964
11435
  const projectManifestPath = resolve(cwd, ".smrt", "manifest.json");
10965
11436
  const registerPath = resolve(cwd, ".smrt", "register.js");
10966
11437
  if (existsSync(projectManifestPath)) try {
@@ -11026,6 +11497,15 @@ export default testManifest;
11026
11497
  for (const line of formatPermissionsOutcome(permissions)) console.log(line);
11027
11498
  if (!permissions.skipped) check("PostgreSQL role permissions", !permissions.error && permissions.plan?.diagnostics.length === 0, permissions.error ?? "PostgreSQL role permissions differ from the configured contract");
11028
11499
  console.log();
11500
+ console.log("🏷️ Deprecated Qualified Names\n");
11501
+ const legacyNames = await runLegacyQualifiedNameReport({ discover: false });
11502
+ if (!legacyNames.report) check("Deprecated qualified-name references", false, legacyNames.error ?? "Could not count stored references");
11503
+ else {
11504
+ for (const line of formatLegacyQualifiedNameReport(legacyNames.report)) console.log(line);
11505
+ if (legacyNames.report.total > 0) warnings.push(`deprecated qualified names: ${legacyNames.report.total} stored reference(s)`);
11506
+ else passed.push("Deprecated qualified-name references");
11507
+ }
11508
+ console.log();
11029
11509
  }
11030
11510
  console.log("━".repeat(50));
11031
11511
  console.log(`\n📊 Summary\n`);
@@ -11063,6 +11543,7 @@ export default testManifest;
11063
11543
  "db:migrate-null-equal-indexes": dbMigrateNullEqualIndexesCommand,
11064
11544
  "db:migrate-agent-schedule-slugs": dbMigrateAgentScheduleSlugsCommand,
11065
11545
  "db:migrate-ledger-accounts": dbMigrateLedgerAccountsCommand,
11546
+ "db:migrate-qualified-names": dbMigrateQualifiedNamesCommand,
11066
11547
  "db:materialize-tenant-hierarchy": dbMaterializeTenantHierarchyCommand,
11067
11548
  "db:drop-framework-base-tables": dbDropFrameworkBaseTablesCommand,
11068
11549
  "db:orphans": dbOrphansCommand,