@happyvertical/smrt-dev-mcp 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +75 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +323 -0
- package/agent-skills/smrt-code-review/SKILL.md +68 -0
- package/agent-skills/smrt-code-review/references/review-output.md +31 -0
- package/dist/agent-skills.d.ts +19 -0
- package/dist/agent-skills.d.ts.map +1 -0
- package/dist/index-DF0HSB_8.js +1274 -0
- package/dist/index-DF0HSB_8.js.map +1 -0
- package/dist/index.d.ts +853 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2039 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge/index.d.ts +175 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge.d.ts +2 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +2 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/tools/generate-smrt-class.d.ts +54 -0
- package/dist/tools/generate-smrt-class.d.ts.map +1 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/introspect-project.d.ts +14 -0
- package/dist/tools/introspect-project.d.ts.map +1 -0
- package/dist/tools/review-smrt-project.d.ts +13 -0
- package/dist/tools/review-smrt-project.d.ts.map +1 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2039 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join, dirname, resolve, relative, isAbsolute, sep } from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, McpError, ErrorCode, ListResourcesRequestSchema, ReadResourceRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import { c as buildReviewContext, b as buildArchitectureContext, a as buildKnowledgeIndex, s as smrtArchitecture, h as smrtReview, d as checkKnowledgeFreshness, e as checkKnowledgeFreshnessFromIndex } from './index-DF0HSB_8.js';
|
|
9
|
+
import { access, readFile, readdir } from 'node:fs/promises';
|
|
10
|
+
import { ManifestGenerator } from '@happyvertical/smrt-core/scanner';
|
|
11
|
+
import { OxcScanner, ManifestAdapter } from '@happyvertical/smrt-scanner';
|
|
12
|
+
|
|
13
|
+
const AGENT_SKILLS = [
|
|
14
|
+
{
|
|
15
|
+
name: "smrt-code-review",
|
|
16
|
+
description: "Harness-agnostic downstream SMRT code review workflow using smrt-dev-mcp deterministic context and prompt bundles.",
|
|
17
|
+
path: "agent-skills/smrt-code-review/SKILL.md",
|
|
18
|
+
skillFile: "agent-skills/smrt-code-review/SKILL.md",
|
|
19
|
+
references: ["agent-skills/smrt-code-review/references/review-output.md"]
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
function listAgentSkills() {
|
|
23
|
+
return AGENT_SKILLS.map(({ skillFile: _skillFile, ...skill }) => skill);
|
|
24
|
+
}
|
|
25
|
+
function getAgentSkill(options) {
|
|
26
|
+
const skill = AGENT_SKILLS.find((item) => item.name === options.name);
|
|
27
|
+
if (!skill) {
|
|
28
|
+
throw new Error(`Unknown agent skill: ${options.name}`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
name: skill.name,
|
|
32
|
+
description: skill.description,
|
|
33
|
+
path: skill.path,
|
|
34
|
+
references: skill.references,
|
|
35
|
+
skillMarkdown: readPackageFile(skill.skillFile),
|
|
36
|
+
referenceFiles: options.includeReferences === false ? [] : skill.references.map((path) => ({
|
|
37
|
+
path,
|
|
38
|
+
content: readPackageFile(path)
|
|
39
|
+
}))
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function readPackageFile(relativePath) {
|
|
43
|
+
const packageRoot = resolvePackageRoot();
|
|
44
|
+
return readFileSync(join(packageRoot, relativePath), "utf8");
|
|
45
|
+
}
|
|
46
|
+
function resolvePackageRoot() {
|
|
47
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
48
|
+
if (!existsSync(join(packageRoot, "package.json"))) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Unable to resolve smrt-dev-mcp package root from ${import.meta.url}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return packageRoot;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TYPE_MAPPING = {
|
|
57
|
+
text: { tsType: "string", defaultValue: "''" },
|
|
58
|
+
integer: { tsType: "number", defaultValue: "0" },
|
|
59
|
+
decimal: { tsType: "number", defaultValue: "0.0" },
|
|
60
|
+
boolean: { tsType: "boolean", defaultValue: "false" },
|
|
61
|
+
datetime: { tsType: "Date", defaultValue: "new Date()" },
|
|
62
|
+
json: { tsType: "any", defaultValue: "{}" }
|
|
63
|
+
};
|
|
64
|
+
async function generateSmrtClass(args) {
|
|
65
|
+
const normalized = normalizeArgs(args);
|
|
66
|
+
const {
|
|
67
|
+
className,
|
|
68
|
+
properties,
|
|
69
|
+
relationships,
|
|
70
|
+
baseClass,
|
|
71
|
+
tableName,
|
|
72
|
+
conflictColumns,
|
|
73
|
+
tenantScoped,
|
|
74
|
+
includeTenantIdField,
|
|
75
|
+
includeApiConfig,
|
|
76
|
+
includeMcpConfig,
|
|
77
|
+
includeCliConfig,
|
|
78
|
+
includeCompanionSnippets
|
|
79
|
+
} = normalized;
|
|
80
|
+
const coreImports = /* @__PURE__ */ new Set([baseClass, "smrt"]);
|
|
81
|
+
if (needsFieldDecorator(properties)) coreImports.add("field");
|
|
82
|
+
for (const relationship of relationships) {
|
|
83
|
+
coreImports.add(relationship.type);
|
|
84
|
+
}
|
|
85
|
+
const imports = [
|
|
86
|
+
`import { ${Array.from(coreImports).join(", ")} } from '@happyvertical/smrt-core';`
|
|
87
|
+
];
|
|
88
|
+
if (tenantScoped) {
|
|
89
|
+
const tenancyImports = ["TenantScoped"];
|
|
90
|
+
if (includeTenantIdField) tenancyImports.push("tenantId");
|
|
91
|
+
imports.push(
|
|
92
|
+
`import { ${tenancyImports.join(", ")} } from '@happyvertical/smrt-tenancy';`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const decoratorLines = [
|
|
96
|
+
...tenantScoped ? [`@TenantScoped(${renderObjectLiteral({ ...tenantScoped })})`] : [],
|
|
97
|
+
renderSmrtDecorator({
|
|
98
|
+
includeApiConfig,
|
|
99
|
+
includeMcpConfig,
|
|
100
|
+
includeCliConfig,
|
|
101
|
+
tableName,
|
|
102
|
+
conflictColumns
|
|
103
|
+
})
|
|
104
|
+
];
|
|
105
|
+
const classMembers = [
|
|
106
|
+
...includeTenantIdField && tenantScoped ? [renderTenantIdField(tenantScoped)] : [],
|
|
107
|
+
...properties.map(renderProperty),
|
|
108
|
+
...relationships.map(renderRelationship)
|
|
109
|
+
].filter(Boolean);
|
|
110
|
+
const companionSnippets = includeCompanionSnippets ? `
|
|
111
|
+
${renderCompanionSnippets(className, Boolean(tenantScoped))}` : "";
|
|
112
|
+
return `${imports.join("\n")}
|
|
113
|
+
|
|
114
|
+
${decoratorLines.join("\n")}
|
|
115
|
+
export class ${className} extends ${baseClass} {
|
|
116
|
+
${classMembers.join("\n\n")}
|
|
117
|
+
|
|
118
|
+
constructor(options: any = {}) {
|
|
119
|
+
super(options);
|
|
120
|
+
Object.assign(this, options);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
${companionSnippets}`;
|
|
124
|
+
}
|
|
125
|
+
function normalizeArgs(args) {
|
|
126
|
+
const template = args.template ?? "basic";
|
|
127
|
+
const templateDefaults = defaultsForTemplate(template);
|
|
128
|
+
const tenantScoped = args.tenantScoped === true ? templateDefaults.tenantScoped ?? { mode: "required" } : args.tenantScoped === false ? void 0 : args.tenantScoped ?? templateDefaults.tenantScoped;
|
|
129
|
+
return {
|
|
130
|
+
className: args.className,
|
|
131
|
+
properties: args.properties,
|
|
132
|
+
baseClass: args.baseClass ?? "SmrtObject",
|
|
133
|
+
template,
|
|
134
|
+
tableName: args.tableName ?? templateDefaults.tableName,
|
|
135
|
+
conflictColumns: args.conflictColumns ?? templateDefaults.conflictColumns ?? [],
|
|
136
|
+
tenantScoped: normalizeTenantScoped(tenantScoped),
|
|
137
|
+
includeTenantIdField: args.includeTenantIdField ?? templateDefaults.includeTenantIdField ?? Boolean(tenantScoped),
|
|
138
|
+
relationships: args.relationships ?? [],
|
|
139
|
+
includeApiConfig: args.includeApiConfig ?? true,
|
|
140
|
+
includeMcpConfig: args.includeMcpConfig ?? true,
|
|
141
|
+
includeCliConfig: args.includeCliConfig ?? true,
|
|
142
|
+
includeCompanionSnippets: args.includeCompanionSnippets ?? false
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function defaultsForTemplate(template) {
|
|
146
|
+
switch (template) {
|
|
147
|
+
case "optional-catalog":
|
|
148
|
+
return {
|
|
149
|
+
tenantScoped: { mode: "optional" },
|
|
150
|
+
includeTenantIdField: true,
|
|
151
|
+
conflictColumns: ["tenant_id", "slug"]
|
|
152
|
+
};
|
|
153
|
+
case "tenant-project-object":
|
|
154
|
+
return {
|
|
155
|
+
tenantScoped: { mode: "required" },
|
|
156
|
+
includeTenantIdField: true
|
|
157
|
+
};
|
|
158
|
+
case "tenant-event-log-object":
|
|
159
|
+
return {
|
|
160
|
+
tenantScoped: { mode: "optional" },
|
|
161
|
+
includeTenantIdField: true
|
|
162
|
+
};
|
|
163
|
+
case "global-catalog":
|
|
164
|
+
return { conflictColumns: ["slug"] };
|
|
165
|
+
case "cross-package-reference":
|
|
166
|
+
case "basic":
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function normalizeTenantScoped(value) {
|
|
171
|
+
if (!value) return void 0;
|
|
172
|
+
return {
|
|
173
|
+
mode: typeof value === "object" ? value.mode ?? "required" : "required",
|
|
174
|
+
field: typeof value === "object" ? value.field ?? "tenantId" : "tenantId",
|
|
175
|
+
autoFilter: typeof value === "object" ? value.autoFilter : void 0,
|
|
176
|
+
autoPopulate: typeof value === "object" ? value.autoPopulate : void 0,
|
|
177
|
+
allowSuperAdminBypass: typeof value === "object" ? value.allowSuperAdminBypass : void 0
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function renderSmrtDecorator(options) {
|
|
181
|
+
const decoratorConfig = {};
|
|
182
|
+
if (options.tableName) {
|
|
183
|
+
decoratorConfig.tableName = options.tableName;
|
|
184
|
+
}
|
|
185
|
+
if (options.conflictColumns.length > 0) {
|
|
186
|
+
decoratorConfig.conflictColumns = options.conflictColumns;
|
|
187
|
+
}
|
|
188
|
+
if (options.includeApiConfig) {
|
|
189
|
+
decoratorConfig.api = {
|
|
190
|
+
include: ["list", "get", "create", "update"],
|
|
191
|
+
exclude: ["delete"]
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (options.includeMcpConfig) {
|
|
195
|
+
decoratorConfig.mcp = {
|
|
196
|
+
include: ["list", "get"]
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (options.includeCliConfig) {
|
|
200
|
+
decoratorConfig.cli = true;
|
|
201
|
+
}
|
|
202
|
+
return Object.keys(decoratorConfig).length > 0 ? `@smrt(${JSON.stringify(decoratorConfig, null, 2)})` : "@smrt()";
|
|
203
|
+
}
|
|
204
|
+
function renderTenantIdField(tenantScoped) {
|
|
205
|
+
const nullable = tenantScoped.mode === "optional";
|
|
206
|
+
const field = tenantScoped.field ?? "tenantId";
|
|
207
|
+
return nullable ? ` @tenantId({ nullable: true })
|
|
208
|
+
${field}: string | null = null;` : ` @tenantId()
|
|
209
|
+
${field}: string = '';`;
|
|
210
|
+
}
|
|
211
|
+
function renderProperty(prop) {
|
|
212
|
+
const mapping = TYPE_MAPPING[prop.type];
|
|
213
|
+
const nullable = prop.nullable === true;
|
|
214
|
+
const tsType = nullable ? `${mapping.tsType} | null` : mapping.tsType;
|
|
215
|
+
const defaultValue = prop.defaultValue !== void 0 ? renderLiteral(prop.defaultValue) : nullable ? "null" : mapping.defaultValue;
|
|
216
|
+
const fieldOptions = compactObject$1({
|
|
217
|
+
required: prop.required,
|
|
218
|
+
nullable: prop.nullable,
|
|
219
|
+
description: prop.description
|
|
220
|
+
});
|
|
221
|
+
const jsdoc = prop.description ? ` /** ${prop.description} */
|
|
222
|
+
` : "";
|
|
223
|
+
const decorator = Object.keys(fieldOptions).length > 0 ? ` @field(${JSON.stringify(fieldOptions)})
|
|
224
|
+
` : "";
|
|
225
|
+
return `${jsdoc}${decorator} ${prop.name}: ${tsType} = ${defaultValue};`;
|
|
226
|
+
}
|
|
227
|
+
function renderRelationship(relationship) {
|
|
228
|
+
const options = compactObject$1({
|
|
229
|
+
required: relationship.required,
|
|
230
|
+
nullable: relationship.nullable,
|
|
231
|
+
description: relationship.description,
|
|
232
|
+
validate: relationship.validate,
|
|
233
|
+
foreignKey: relationship.foreignKey,
|
|
234
|
+
through: relationship.through,
|
|
235
|
+
sourceKey: relationship.sourceKey,
|
|
236
|
+
targetKey: relationship.targetKey
|
|
237
|
+
});
|
|
238
|
+
const args = [
|
|
239
|
+
renderLiteral(relationship.related),
|
|
240
|
+
...Object.keys(options).length > 0 ? [JSON.stringify(options)] : []
|
|
241
|
+
];
|
|
242
|
+
const decorator = `@${relationship.type}(${args.join(", ")})`;
|
|
243
|
+
const fieldType = relationship.type === "oneToMany" || relationship.type === "manyToMany" ? "unknown[]" : relationship.nullable ? "string | null" : "string";
|
|
244
|
+
const defaultValue = relationship.type === "oneToMany" || relationship.type === "manyToMany" ? "[]" : relationship.nullable ? "null" : "''";
|
|
245
|
+
return ` ${decorator}
|
|
246
|
+
${relationship.name}: ${fieldType} = ${defaultValue};`;
|
|
247
|
+
}
|
|
248
|
+
function needsFieldDecorator(properties) {
|
|
249
|
+
return properties.some(
|
|
250
|
+
(property) => property.required !== void 0 || property.nullable !== void 0 || property.description !== void 0
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
function renderCompanionSnippets(className, usesTenantScoped) {
|
|
254
|
+
const dependencyNote = usesTenantScoped ? `
|
|
255
|
+
* - Ensure package.json declares "@happyvertical/smrt-tenancy".` : "";
|
|
256
|
+
return `/*
|
|
257
|
+
* Package wiring:
|
|
258
|
+
* - Export ${className} from the package entrypoint used by consumers.
|
|
259
|
+
* - Import this module from any package registration file that eagerly loads objects.${dependencyNote}
|
|
260
|
+
*/`;
|
|
261
|
+
}
|
|
262
|
+
function renderObjectLiteral(value) {
|
|
263
|
+
return JSON.stringify(compactObject$1(value), null, 2);
|
|
264
|
+
}
|
|
265
|
+
function renderLiteral(value) {
|
|
266
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
267
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
268
|
+
return String(value);
|
|
269
|
+
}
|
|
270
|
+
if (value === null) return "null";
|
|
271
|
+
return JSON.stringify(value, null, 2);
|
|
272
|
+
}
|
|
273
|
+
function compactObject$1(value) {
|
|
274
|
+
return Object.fromEntries(
|
|
275
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const DEFAULT_MANIFEST_PATHS = [
|
|
280
|
+
".smrt/manifest.json",
|
|
281
|
+
"dist/manifest.json",
|
|
282
|
+
"src/manifest/manifest.json"
|
|
283
|
+
];
|
|
284
|
+
const SCAN_EXCLUDE = [
|
|
285
|
+
"**/node_modules/**",
|
|
286
|
+
"**/dist/**",
|
|
287
|
+
"**/build/**",
|
|
288
|
+
"**/.git/**",
|
|
289
|
+
"**/.smrt/**",
|
|
290
|
+
"**/*.d.ts",
|
|
291
|
+
"**/*.test.ts",
|
|
292
|
+
"**/*.spec.ts",
|
|
293
|
+
"**/__tests__/**"
|
|
294
|
+
];
|
|
295
|
+
const RELATIONSHIP_TYPES = /* @__PURE__ */ new Set([
|
|
296
|
+
"foreignKey",
|
|
297
|
+
"crossPackageRef",
|
|
298
|
+
"oneToMany",
|
|
299
|
+
"manyToMany"
|
|
300
|
+
]);
|
|
301
|
+
async function introspectProject(args) {
|
|
302
|
+
const {
|
|
303
|
+
directory = process.cwd(),
|
|
304
|
+
includeFields = true,
|
|
305
|
+
includeRelationships = true,
|
|
306
|
+
includeMethods = true
|
|
307
|
+
} = args;
|
|
308
|
+
const projectPath = resolve(directory);
|
|
309
|
+
const exists = await pathExists$1(projectPath);
|
|
310
|
+
if (!exists) {
|
|
311
|
+
return JSON.stringify(
|
|
312
|
+
{
|
|
313
|
+
projectPath,
|
|
314
|
+
manifestSource: "none",
|
|
315
|
+
objectCount: 0,
|
|
316
|
+
objects: [],
|
|
317
|
+
diagnostics: [
|
|
318
|
+
{
|
|
319
|
+
severity: "warning",
|
|
320
|
+
message: `Project directory does not exist: ${projectPath}`
|
|
321
|
+
}
|
|
322
|
+
]
|
|
323
|
+
},
|
|
324
|
+
null,
|
|
325
|
+
2
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const packageMetadata = await readPackageMetadata(projectPath);
|
|
329
|
+
const tenantScopes = await scanTenantScopes(projectPath);
|
|
330
|
+
const manifestResult = await loadManifestArtifact(projectPath, args.manifestPath) ?? await scanSourceManifest(projectPath, packageMetadata);
|
|
331
|
+
const objects = Object.entries(manifestResult.manifest.objects ?? {}).map(
|
|
332
|
+
([manifestKey, object]) => formatObject({
|
|
333
|
+
manifestKey,
|
|
334
|
+
object,
|
|
335
|
+
projectPath,
|
|
336
|
+
includeFields,
|
|
337
|
+
includeRelationships,
|
|
338
|
+
includeMethods,
|
|
339
|
+
tenantScope: tenantScopes.get(object.className)
|
|
340
|
+
})
|
|
341
|
+
).sort((left, right) => left.className.localeCompare(right.className));
|
|
342
|
+
const output = {
|
|
343
|
+
projectPath,
|
|
344
|
+
manifestSource: manifestResult.source,
|
|
345
|
+
manifestPath: "path" in manifestResult ? relative(projectPath, manifestResult.path) : void 0,
|
|
346
|
+
packageName: manifestResult.manifest.packageName ?? packageMetadata.name ?? void 0,
|
|
347
|
+
packageVersion: manifestResult.manifest.packageVersion ?? packageMetadata.version ?? void 0,
|
|
348
|
+
objectCount: objects.length,
|
|
349
|
+
scannedFileCount: manifestResult.scannedFileCount,
|
|
350
|
+
parseTimeMs: manifestResult.parseTimeMs,
|
|
351
|
+
objects,
|
|
352
|
+
diagnostics: manifestResult.diagnostics
|
|
353
|
+
};
|
|
354
|
+
return JSON.stringify(output, null, 2);
|
|
355
|
+
}
|
|
356
|
+
async function loadManifestArtifact(projectPath, manifestPath) {
|
|
357
|
+
const candidates = manifestPath ? [resolve(projectPath, manifestPath)] : DEFAULT_MANIFEST_PATHS.map((candidate) => join(projectPath, candidate));
|
|
358
|
+
const diagnostics = [];
|
|
359
|
+
for (const candidate of candidates) {
|
|
360
|
+
if (!await pathExists$1(candidate)) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const parsed = JSON.parse(await readFile(candidate, "utf-8"));
|
|
365
|
+
if (isManifestLike(parsed)) {
|
|
366
|
+
return {
|
|
367
|
+
source: "manifest",
|
|
368
|
+
path: candidate,
|
|
369
|
+
manifest: parsed,
|
|
370
|
+
diagnostics
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
diagnostics.push({
|
|
374
|
+
severity: "warning",
|
|
375
|
+
filePath: candidate,
|
|
376
|
+
message: "Manifest artifact is present but does not contain objects."
|
|
377
|
+
});
|
|
378
|
+
} catch (error) {
|
|
379
|
+
diagnostics.push({
|
|
380
|
+
severity: "error",
|
|
381
|
+
filePath: candidate,
|
|
382
|
+
message: `Unable to parse manifest artifact: ${messageFromError(error)}`
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return manifestPath ? {
|
|
387
|
+
source: "manifest",
|
|
388
|
+
path: resolve(projectPath, manifestPath),
|
|
389
|
+
manifest: { objects: {} },
|
|
390
|
+
diagnostics: [
|
|
391
|
+
...diagnostics,
|
|
392
|
+
{
|
|
393
|
+
severity: "warning",
|
|
394
|
+
filePath: resolve(projectPath, manifestPath),
|
|
395
|
+
message: "Requested manifest artifact was not found."
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
} : void 0;
|
|
399
|
+
}
|
|
400
|
+
async function scanSourceManifest(projectPath, packageMetadata) {
|
|
401
|
+
const scanner = new OxcScanner({
|
|
402
|
+
cwd: projectPath,
|
|
403
|
+
include: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
|
404
|
+
exclude: SCAN_EXCLUDE
|
|
405
|
+
});
|
|
406
|
+
const { results, resolved } = await scanner.scanAndResolve();
|
|
407
|
+
const adapter = new ManifestAdapter();
|
|
408
|
+
const manifest = adapter.toManifest(
|
|
409
|
+
resolved.filter((classDef) => classDef.hasSmartDecorator),
|
|
410
|
+
{
|
|
411
|
+
packageName: packageMetadata.name,
|
|
412
|
+
packageVersion: packageMetadata.version,
|
|
413
|
+
typeAliases: results.typeAliases
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
finalizeScannerManifest(manifest, packageMetadata);
|
|
417
|
+
return {
|
|
418
|
+
source: "scanner",
|
|
419
|
+
manifest,
|
|
420
|
+
diagnostics: results.errors.map(scanErrorToDiagnostic),
|
|
421
|
+
scannedFileCount: results.fileCount,
|
|
422
|
+
parseTimeMs: Math.round(results.totalParseTimeMs)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function finalizeScannerManifest(manifest, packageMetadata) {
|
|
426
|
+
const manifestGen = new ManifestGenerator();
|
|
427
|
+
const fullManifest = manifest;
|
|
428
|
+
withSuppressedConsoleLog(() => {
|
|
429
|
+
manifestGen.injectTenantScopedFields(fullManifest);
|
|
430
|
+
manifestGen.mergeInheritedFields(fullManifest);
|
|
431
|
+
manifestGen.generateValidationRules(fullManifest);
|
|
432
|
+
manifestGen.generateSchemas(fullManifest);
|
|
433
|
+
manifestGen.assertTenantScopedSchemaContract(fullManifest);
|
|
434
|
+
manifestGen.generateAgentManifests(
|
|
435
|
+
fullManifest,
|
|
436
|
+
packageMetadata.name,
|
|
437
|
+
packageMetadata.json
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
function withSuppressedConsoleLog(callback) {
|
|
442
|
+
const originalLog = console.log;
|
|
443
|
+
console.log = () => void 0;
|
|
444
|
+
try {
|
|
445
|
+
return callback();
|
|
446
|
+
} finally {
|
|
447
|
+
console.log = originalLog;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function formatObject({
|
|
451
|
+
manifestKey,
|
|
452
|
+
object,
|
|
453
|
+
projectPath,
|
|
454
|
+
includeFields,
|
|
455
|
+
includeRelationships,
|
|
456
|
+
includeMethods,
|
|
457
|
+
tenantScope
|
|
458
|
+
}) {
|
|
459
|
+
const fieldDetails = Object.entries(object.fields ?? {}).map(
|
|
460
|
+
([name, field]) => ({
|
|
461
|
+
name,
|
|
462
|
+
type: field.type,
|
|
463
|
+
...field.required !== void 0 ? { required: field.required } : {},
|
|
464
|
+
...field.default !== void 0 ? { default: field.default } : {},
|
|
465
|
+
...field.related ? { related: field.related } : {},
|
|
466
|
+
...field.description ? { description: field.description } : {},
|
|
467
|
+
...field._meta ? { meta: field._meta } : {},
|
|
468
|
+
...field.transient !== void 0 ? { transient: field.transient } : {}
|
|
469
|
+
})
|
|
470
|
+
);
|
|
471
|
+
const relationshipDetails = fieldDetails.filter((field) => RELATIONSHIP_TYPES.has(field.type)).map((field) => ({
|
|
472
|
+
field: field.name,
|
|
473
|
+
relatedClass: field.related ?? "",
|
|
474
|
+
type: field.type,
|
|
475
|
+
...field.meta ? { meta: field.meta } : {}
|
|
476
|
+
}));
|
|
477
|
+
const methodDetails = Object.entries(object.methods ?? {}).map(
|
|
478
|
+
([name, method]) => ({
|
|
479
|
+
name: method.name ?? name,
|
|
480
|
+
isAsync: method.async === true,
|
|
481
|
+
isStatic: method.isStatic === true,
|
|
482
|
+
isPublic: method.isPublic !== false,
|
|
483
|
+
parameters: method.parameters ?? [],
|
|
484
|
+
returnType: method.returnType ?? "unknown",
|
|
485
|
+
...method.description ? { description: method.description } : {}
|
|
486
|
+
})
|
|
487
|
+
);
|
|
488
|
+
const decoratorConfig = object.decoratorConfig ?? {};
|
|
489
|
+
const effectiveTenantScope = tenantScope ?? normalizeTenantScopedConfig(decoratorConfig.tenantScoped);
|
|
490
|
+
const schema = object.schema;
|
|
491
|
+
return compactObject({
|
|
492
|
+
manifestKey,
|
|
493
|
+
name: object.name,
|
|
494
|
+
className: object.className,
|
|
495
|
+
qualifiedName: object.qualifiedName,
|
|
496
|
+
filePath: sanitizePath$1(projectPath, object.filePath),
|
|
497
|
+
packageName: object.packageName,
|
|
498
|
+
packageVersion: object.packageVersion,
|
|
499
|
+
importPath: object.importPath,
|
|
500
|
+
modulePath: object.modulePath,
|
|
501
|
+
exportName: object.exportName,
|
|
502
|
+
collectionExportName: object.collectionExportName,
|
|
503
|
+
collection: object.collection,
|
|
504
|
+
extends: object.extends,
|
|
505
|
+
extendsTypeArg: object.extendsTypeArg,
|
|
506
|
+
tableName: schema?.tableName ?? stringFromConfig(decoratorConfig.tableName) ?? object.collection,
|
|
507
|
+
tableStrategy: stringFromConfig(decoratorConfig.tableStrategy) ?? "cti",
|
|
508
|
+
conflictColumns: arrayFromConfig(decoratorConfig.conflictColumns),
|
|
509
|
+
tenantScope: effectiveTenantScope,
|
|
510
|
+
decoratorConfig,
|
|
511
|
+
schema: schema ? {
|
|
512
|
+
tableName: schema.tableName,
|
|
513
|
+
columns: schema.columns,
|
|
514
|
+
indexes: schema.indexes ?? [],
|
|
515
|
+
version: schema.version
|
|
516
|
+
} : void 0,
|
|
517
|
+
indexes: schema?.indexes ?? [],
|
|
518
|
+
staticProperties: object.staticProperties,
|
|
519
|
+
validationRules: object.validationRules,
|
|
520
|
+
...includeFields && {
|
|
521
|
+
fields: fieldDetails.map((field) => `${field.name}: ${field.type}`).join(", "),
|
|
522
|
+
fieldDetails
|
|
523
|
+
},
|
|
524
|
+
...includeRelationships && relationshipDetails.length > 0 && {
|
|
525
|
+
relationships: relationshipDetails.map(
|
|
526
|
+
(relationship) => `${relationship.field} -> ${relationship.relatedClass} (${relationship.type})`
|
|
527
|
+
).join(", "),
|
|
528
|
+
relationshipDetails
|
|
529
|
+
},
|
|
530
|
+
...includeMethods && methodDetails.length > 0 && {
|
|
531
|
+
methods: methodDetails.map((method) => `${method.isAsync ? "async " : ""}${method.name}()`).join(", "),
|
|
532
|
+
methodDetails
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
async function scanTenantScopes(projectPath) {
|
|
537
|
+
const files = await listSourceFiles$1(projectPath);
|
|
538
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
539
|
+
await Promise.all(
|
|
540
|
+
files.map(async (filePath) => {
|
|
541
|
+
const content = await readFile(filePath, "utf-8");
|
|
542
|
+
const classMatches = content.matchAll(/class\s+([A-Za-z_]\w*)\b/g);
|
|
543
|
+
for (const match of classMatches) {
|
|
544
|
+
const className = match[1];
|
|
545
|
+
if (!className || match.index === void 0) continue;
|
|
546
|
+
const prefix = content.slice(
|
|
547
|
+
Math.max(0, match.index - 800),
|
|
548
|
+
match.index
|
|
549
|
+
);
|
|
550
|
+
const tenantMatches = Array.from(
|
|
551
|
+
prefix.matchAll(/@TenantScoped\s*\(([\s\S]*?)\)/g)
|
|
552
|
+
);
|
|
553
|
+
const tenantMatch = tenantMatches.at(-1);
|
|
554
|
+
if (!tenantMatch) continue;
|
|
555
|
+
scopes.set(className, {
|
|
556
|
+
source: "TenantScoped",
|
|
557
|
+
...parseTenantScopedOptions(tenantMatch[1])
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
})
|
|
561
|
+
);
|
|
562
|
+
return scopes;
|
|
563
|
+
}
|
|
564
|
+
async function listSourceFiles$1(projectPath) {
|
|
565
|
+
const files = [];
|
|
566
|
+
async function visit(dir) {
|
|
567
|
+
let entries;
|
|
568
|
+
try {
|
|
569
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
570
|
+
} catch {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
for (const entry of entries) {
|
|
574
|
+
const fullPath = join(dir, entry.name);
|
|
575
|
+
if (entry.isDirectory()) {
|
|
576
|
+
if ([
|
|
577
|
+
"node_modules",
|
|
578
|
+
"dist",
|
|
579
|
+
"build",
|
|
580
|
+
".git",
|
|
581
|
+
".smrt",
|
|
582
|
+
"__tests__"
|
|
583
|
+
].includes(entry.name) || entry.name.startsWith(".")) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
await visit(fullPath);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name) && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".test.ts") && !entry.name.endsWith(".spec.ts")) {
|
|
590
|
+
files.push(fullPath);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
await visit(projectPath);
|
|
595
|
+
return files;
|
|
596
|
+
}
|
|
597
|
+
function parseTenantScopedOptions(raw) {
|
|
598
|
+
const mode = raw.match(/mode\s*:\s*['"`](required|optional)['"`]/)?.[1];
|
|
599
|
+
const field = raw.match(/field\s*:\s*['"`]([A-Za-z_]\w*)['"`]/)?.[1];
|
|
600
|
+
const allowSuperAdminBypass = raw.match(
|
|
601
|
+
/allowSuperAdminBypass\s*:\s*(true|false)/
|
|
602
|
+
)?.[1];
|
|
603
|
+
const autoFilter = raw.match(/autoFilter\s*:\s*(true|false)/)?.[1];
|
|
604
|
+
const autoPopulate = raw.match(/autoPopulate\s*:\s*(true|false)/)?.[1];
|
|
605
|
+
return compactObject({
|
|
606
|
+
mode: mode ?? "required",
|
|
607
|
+
field: field ?? "tenantId",
|
|
608
|
+
autoFilter: autoFilter === void 0 ? void 0 : autoFilter === "true",
|
|
609
|
+
autoPopulate: autoPopulate === void 0 ? void 0 : autoPopulate === "true",
|
|
610
|
+
allowSuperAdminBypass: allowSuperAdminBypass === void 0 ? void 0 : allowSuperAdminBypass === "true"
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
function normalizeTenantScopedConfig(tenantScoped) {
|
|
614
|
+
if (!tenantScoped) return void 0;
|
|
615
|
+
const options = typeof tenantScoped === "object" && !Array.isArray(tenantScoped) ? tenantScoped : {};
|
|
616
|
+
return {
|
|
617
|
+
source: "smrt",
|
|
618
|
+
mode: options.mode ?? "required",
|
|
619
|
+
field: options.field ?? "tenantId",
|
|
620
|
+
autoFilter: options.autoFilter ?? true,
|
|
621
|
+
autoPopulate: options.autoPopulate ?? true,
|
|
622
|
+
allowSuperAdminBypass: options.allowSuperAdminBypass ?? false
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
async function readPackageMetadata(projectPath) {
|
|
626
|
+
const packageJsonPath = join(projectPath, "package.json");
|
|
627
|
+
if (!await pathExists$1(packageJsonPath)) return {};
|
|
628
|
+
try {
|
|
629
|
+
const json = JSON.parse(await readFile(packageJsonPath, "utf-8"));
|
|
630
|
+
return {
|
|
631
|
+
name: typeof json.name === "string" ? json.name : void 0,
|
|
632
|
+
version: typeof json.version === "string" ? json.version : void 0,
|
|
633
|
+
json
|
|
634
|
+
};
|
|
635
|
+
} catch {
|
|
636
|
+
return {};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async function pathExists$1(path) {
|
|
640
|
+
try {
|
|
641
|
+
await access(path);
|
|
642
|
+
return true;
|
|
643
|
+
} catch {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function isManifestLike(value) {
|
|
648
|
+
return !!value && typeof value === "object" && !!value.objects && typeof value.objects === "object";
|
|
649
|
+
}
|
|
650
|
+
function scanErrorToDiagnostic(error) {
|
|
651
|
+
return {
|
|
652
|
+
severity: error.severity,
|
|
653
|
+
message: error.message,
|
|
654
|
+
filePath: error.filePath,
|
|
655
|
+
line: error.line,
|
|
656
|
+
column: error.column
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function sanitizePath$1(projectPath, filePath) {
|
|
660
|
+
const absolute = isAbsolute(filePath) ? filePath : resolve(projectPath, filePath);
|
|
661
|
+
const relativePath = relative(projectPath, absolute);
|
|
662
|
+
if (!relativePath.startsWith("..")) {
|
|
663
|
+
return relativePath || filePath;
|
|
664
|
+
}
|
|
665
|
+
return filePath;
|
|
666
|
+
}
|
|
667
|
+
function stringFromConfig(value) {
|
|
668
|
+
return typeof value === "string" ? value : void 0;
|
|
669
|
+
}
|
|
670
|
+
function arrayFromConfig(value) {
|
|
671
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : void 0;
|
|
672
|
+
}
|
|
673
|
+
function compactObject(value) {
|
|
674
|
+
return Object.fromEntries(
|
|
675
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0)
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
function messageFromError(error) {
|
|
679
|
+
return error instanceof Error ? error.message : String(error);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const SOURCE_EXTENSIONS = /\.(tsx?|jsx?|svelte)$/;
|
|
683
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
684
|
+
"node_modules",
|
|
685
|
+
"dist",
|
|
686
|
+
"build",
|
|
687
|
+
".git",
|
|
688
|
+
".smrt",
|
|
689
|
+
".svelte-kit",
|
|
690
|
+
"coverage"
|
|
691
|
+
]);
|
|
692
|
+
const DEPENDENCY_SECTIONS = [
|
|
693
|
+
"dependencies",
|
|
694
|
+
"devDependencies",
|
|
695
|
+
"peerDependencies",
|
|
696
|
+
"optionalDependencies"
|
|
697
|
+
];
|
|
698
|
+
const SMRT_PACKAGE_PREFIX = "@happyvertical/smrt-";
|
|
699
|
+
const HAPPYVERTICAL_PACKAGE_PREFIX = "@happyvertical/";
|
|
700
|
+
async function reviewSmrtProject(args) {
|
|
701
|
+
const projectPath = resolve(args.directory ?? args.rootDir ?? process.cwd());
|
|
702
|
+
if (!await pathExists(projectPath)) {
|
|
703
|
+
return JSON.stringify(
|
|
704
|
+
{
|
|
705
|
+
projectPath,
|
|
706
|
+
packageCount: 0,
|
|
707
|
+
packages: [],
|
|
708
|
+
findings: [],
|
|
709
|
+
summary: { high: 0, medium: 0, low: 0 },
|
|
710
|
+
diagnostics: [
|
|
711
|
+
{
|
|
712
|
+
severity: "warning",
|
|
713
|
+
message: `Project directory does not exist: ${projectPath}`
|
|
714
|
+
}
|
|
715
|
+
]
|
|
716
|
+
},
|
|
717
|
+
null,
|
|
718
|
+
2
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
const packageContexts = await buildPackageContexts(projectPath);
|
|
722
|
+
const sourceFiles = await listSourceFiles(projectPath, packageContexts);
|
|
723
|
+
await attachSourceFiles(sourceFiles, packageContexts, projectPath);
|
|
724
|
+
const findings = limitFindings(
|
|
725
|
+
[
|
|
726
|
+
...findMissingHappyVerticalDependencies(packageContexts),
|
|
727
|
+
...findCustomManifestGeneration(sourceFiles, projectPath),
|
|
728
|
+
...findDirectStorageBypasses(sourceFiles, packageContexts, projectPath),
|
|
729
|
+
...findCustomHttpShells(sourceFiles, packageContexts, projectPath),
|
|
730
|
+
...findLocalAuthTenancy(sourceFiles, packageContexts, projectPath),
|
|
731
|
+
...findUiShellDrift(packageContexts, projectPath),
|
|
732
|
+
...findMissingManifestArtifacts(
|
|
733
|
+
sourceFiles,
|
|
734
|
+
packageContexts,
|
|
735
|
+
projectPath
|
|
736
|
+
)
|
|
737
|
+
],
|
|
738
|
+
args.maxFindings
|
|
739
|
+
);
|
|
740
|
+
const packages = packageContexts.map(packageInventory).sort((left, right) => left.path.localeCompare(right.path));
|
|
741
|
+
const summary = summarizeFindings(findings);
|
|
742
|
+
return JSON.stringify(
|
|
743
|
+
{
|
|
744
|
+
projectPath,
|
|
745
|
+
packageCount: packages.length,
|
|
746
|
+
packages,
|
|
747
|
+
findings: args.includeSourceEvidence === false ? findings.map(({ evidence: _evidence, ...finding }) => finding) : findings,
|
|
748
|
+
summary,
|
|
749
|
+
referenceChecks: [
|
|
750
|
+
"Prefer SMRT scanner/runtime manifests over custom manifest builders.",
|
|
751
|
+
"Prefer SvelteKit plus @happyvertical/smrt-svelte for app shells.",
|
|
752
|
+
"Prefer @happyvertical/sql and @happyvertical/files/assets/content over direct durable node:fs storage.",
|
|
753
|
+
"Prefer smrt-users, smrt-tenancy, and profile/audit packages over local static auth seams."
|
|
754
|
+
],
|
|
755
|
+
suggestedFollowUpIssues: findings.map((finding) => ({
|
|
756
|
+
title: finding.suggestedIssueTitle,
|
|
757
|
+
severity: finding.severity,
|
|
758
|
+
area: finding.area
|
|
759
|
+
}))
|
|
760
|
+
},
|
|
761
|
+
null,
|
|
762
|
+
2
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
async function buildPackageContexts(projectPath) {
|
|
766
|
+
const packageJsonPaths = await listPackageJsonFiles(projectPath);
|
|
767
|
+
const contexts = await Promise.all(
|
|
768
|
+
packageJsonPaths.map(async (packageJsonPath) => {
|
|
769
|
+
const json = JSON.parse(
|
|
770
|
+
await readFile(packageJsonPath, "utf-8")
|
|
771
|
+
);
|
|
772
|
+
const directory = dirname(packageJsonPath);
|
|
773
|
+
return {
|
|
774
|
+
directory,
|
|
775
|
+
relativePath: relative(projectPath, directory) || ".",
|
|
776
|
+
packageJsonPath,
|
|
777
|
+
json,
|
|
778
|
+
dependencies: collectDependencies(json),
|
|
779
|
+
scripts: isRecord(json.scripts) ? json.scripts : {},
|
|
780
|
+
imports: /* @__PURE__ */ new Map(),
|
|
781
|
+
sourceFiles: []
|
|
782
|
+
};
|
|
783
|
+
})
|
|
784
|
+
);
|
|
785
|
+
if (contexts.length === 0) {
|
|
786
|
+
contexts.push({
|
|
787
|
+
directory: projectPath,
|
|
788
|
+
relativePath: ".",
|
|
789
|
+
packageJsonPath: join(projectPath, "package.json"),
|
|
790
|
+
json: {},
|
|
791
|
+
dependencies: /* @__PURE__ */ new Set(),
|
|
792
|
+
scripts: {},
|
|
793
|
+
imports: /* @__PURE__ */ new Map(),
|
|
794
|
+
sourceFiles: []
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
return contexts.sort(
|
|
798
|
+
(left, right) => left.directory.length - right.directory.length
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
async function listPackageJsonFiles(projectPath) {
|
|
802
|
+
const files = [];
|
|
803
|
+
await visit(projectPath, async (filePath, entryName) => {
|
|
804
|
+
if (entryName === "package.json") files.push(filePath);
|
|
805
|
+
});
|
|
806
|
+
return files;
|
|
807
|
+
}
|
|
808
|
+
async function listSourceFiles(projectPath, packages) {
|
|
809
|
+
const files = [];
|
|
810
|
+
await visit(projectPath, async (filePath, entryName) => {
|
|
811
|
+
if (!SOURCE_EXTENSIONS.test(entryName)) return;
|
|
812
|
+
if (entryName.endsWith(".d.ts") || entryName.endsWith(".test.ts") || entryName.endsWith(".spec.ts")) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const packageDir = findOwningPackage(filePath, packages).directory;
|
|
816
|
+
const content = await readFile(filePath, "utf-8");
|
|
817
|
+
files.push({ path: filePath, packageDir, content });
|
|
818
|
+
});
|
|
819
|
+
return files;
|
|
820
|
+
}
|
|
821
|
+
async function attachSourceFiles(sourceFiles, packages, projectPath) {
|
|
822
|
+
for (const sourceFile of sourceFiles) {
|
|
823
|
+
const owner = packages.find(
|
|
824
|
+
(pkg) => pkg.directory === sourceFile.packageDir
|
|
825
|
+
);
|
|
826
|
+
if (!owner) continue;
|
|
827
|
+
owner.sourceFiles.push(sourceFile);
|
|
828
|
+
for (const importedPackage of extractImports(sourceFile.content)) {
|
|
829
|
+
if (!importedPackage.startsWith(HAPPYVERTICAL_PACKAGE_PREFIX)) continue;
|
|
830
|
+
const evidence = {
|
|
831
|
+
filePath: relative(projectPath, sourceFile.path),
|
|
832
|
+
line: lineNumber(sourceFile.content, importedPackage),
|
|
833
|
+
detail: `Imports ${importedPackage}`
|
|
834
|
+
};
|
|
835
|
+
const existing = owner.imports.get(importedPackage) ?? [];
|
|
836
|
+
existing.push(evidence);
|
|
837
|
+
owner.imports.set(importedPackage, existing);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function findMissingHappyVerticalDependencies(packages) {
|
|
842
|
+
return packages.flatMap((pkg) => {
|
|
843
|
+
const ownName = typeof pkg.json.name === "string" ? pkg.json.name : void 0;
|
|
844
|
+
const missing = Array.from(pkg.imports.keys()).filter(
|
|
845
|
+
(importedPackage) => importedPackage !== ownName && !pkg.dependencies.has(importedPackage)
|
|
846
|
+
);
|
|
847
|
+
if (missing.length === 0) return [];
|
|
848
|
+
const hasSmrtMissing = missing.some(
|
|
849
|
+
(name) => name.startsWith(SMRT_PACKAGE_PREFIX)
|
|
850
|
+
);
|
|
851
|
+
return [
|
|
852
|
+
{
|
|
853
|
+
severity: hasSmrtMissing ? "high" : "medium",
|
|
854
|
+
area: "dependencies",
|
|
855
|
+
code: "missing-happyvertical-dependencies",
|
|
856
|
+
title: `${packageLabel(pkg)} imports HappyVertical packages that package.json does not declare`,
|
|
857
|
+
evidence: missing.flatMap((name) => pkg.imports.get(name) ?? []),
|
|
858
|
+
recommendation: "Declare every imported @happyvertical package in the owning package manifest so downstream installs and generated knowledge stay reproducible.",
|
|
859
|
+
suggestedIssueTitle: `Declare missing HappyVertical dependencies in ${packageLabel(pkg)}`
|
|
860
|
+
}
|
|
861
|
+
];
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
function findCustomManifestGeneration(sourceFiles, projectPath) {
|
|
865
|
+
return sourceFiles.filter(
|
|
866
|
+
(file) => /manifest\.json/.test(file.content) && /objects\s*:/.test(file.content) && /\b(writeFile|writeFileSync)\b/.test(file.content) && !/@happyvertical\/smrt-scanner/.test(file.content)
|
|
867
|
+
).map((file) => ({
|
|
868
|
+
severity: "high",
|
|
869
|
+
area: "manifest",
|
|
870
|
+
code: "custom-object-manifest-generation",
|
|
871
|
+
title: "Custom SMRT object manifest generation detected",
|
|
872
|
+
evidence: [
|
|
873
|
+
{
|
|
874
|
+
filePath: relative(projectPath, file.path),
|
|
875
|
+
line: lineNumber(file.content, "manifest.json"),
|
|
876
|
+
detail: "Writes a manifest.json object inventory outside the SMRT scanner/runtime path."
|
|
877
|
+
}
|
|
878
|
+
],
|
|
879
|
+
recommendation: "Use the SMRT scanner/runtime manifest path so defaults, relationships, schemas, tenant fields, and cross-package references match framework behavior.",
|
|
880
|
+
suggestedIssueTitle: "Replace custom object manifest generation with SMRT scanner/runtime manifest generation"
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
function findDirectStorageBypasses(sourceFiles, packages, projectPath) {
|
|
884
|
+
return sourceFiles.flatMap((file) => {
|
|
885
|
+
const owner = findOwningPackage(file.path, packages);
|
|
886
|
+
const usesFs = /from\s+['"](?:node:fs|node:fs\/promises|fs|fs\/promises)['"]|require\(['"](?:node:fs|node:fs\/promises|fs|fs\/promises)['"]\)/.test(
|
|
887
|
+
file.content
|
|
888
|
+
);
|
|
889
|
+
const writesDurableData = /\b(writeFile|appendFile|mkdir|rm|rename)\b/.test(file.content) && /(\.json|data|storage|persist|cache|db)/i.test(file.content);
|
|
890
|
+
const usesDirectSql = /from\s+['"](?:better-sqlite3|sqlite3|pg|mysql2?|knex|drizzle-orm)['"]/.test(
|
|
891
|
+
file.content
|
|
892
|
+
);
|
|
893
|
+
if ((!usesFs || !writesDurableData) && !usesDirectSql) return [];
|
|
894
|
+
const hasApprovedStorage = owner.dependencies.has("@happyvertical/sql") || owner.dependencies.has("@happyvertical/files") || owner.dependencies.has("@happyvertical/smrt-assets") || owner.dependencies.has("@happyvertical/smrt-content");
|
|
895
|
+
if (hasApprovedStorage) return [];
|
|
896
|
+
return [
|
|
897
|
+
{
|
|
898
|
+
severity: "medium",
|
|
899
|
+
area: "storage",
|
|
900
|
+
code: "direct-storage-bypass",
|
|
901
|
+
title: `${packageLabel(owner)} appears to bypass HappyVertical storage packages`,
|
|
902
|
+
evidence: [
|
|
903
|
+
{
|
|
904
|
+
filePath: relative(projectPath, file.path),
|
|
905
|
+
line: lineNumber(
|
|
906
|
+
file.content,
|
|
907
|
+
usesDirectSql ? "sqlite" : "writeFile"
|
|
908
|
+
),
|
|
909
|
+
detail: usesDirectSql ? "Imports a direct SQL/storage library without @happyvertical/sql." : "Uses node:fs-style durable writes without @happyvertical/files/assets/content."
|
|
910
|
+
}
|
|
911
|
+
],
|
|
912
|
+
recommendation: "Route durable data through @happyvertical/sql, @happyvertical/files, SMRT assets, or SMRT content unless this is explicitly build-only tooling.",
|
|
913
|
+
suggestedIssueTitle: `Review direct storage usage in ${packageLabel(owner)}`
|
|
914
|
+
}
|
|
915
|
+
];
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
function findCustomHttpShells(sourceFiles, packages, projectPath) {
|
|
919
|
+
const evidenceByPackage = /* @__PURE__ */ new Map();
|
|
920
|
+
for (const pkg of packages) {
|
|
921
|
+
if (pkg.dependencies.has("@sveltejs/kit")) continue;
|
|
922
|
+
const routerDependencies = ["express", "fastify", "hono", "koa"].filter(
|
|
923
|
+
(dep) => pkg.dependencies.has(dep)
|
|
924
|
+
);
|
|
925
|
+
if (routerDependencies.length === 0) continue;
|
|
926
|
+
appendEvidence(evidenceByPackage, pkg, {
|
|
927
|
+
filePath: relative(projectPath, pkg.packageJsonPath),
|
|
928
|
+
detail: `Declares custom router package(s): ${routerDependencies.join(", ")}.`
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
for (const file of sourceFiles) {
|
|
932
|
+
const owner = findOwningPackage(file.path, packages);
|
|
933
|
+
if (owner.dependencies.has("@sveltejs/kit")) continue;
|
|
934
|
+
const usesNodeHttp = /from\s+['"](?:node:http|http|node:https|https)['"]|createServer\s*\(/.test(
|
|
935
|
+
file.content
|
|
936
|
+
);
|
|
937
|
+
if (!usesNodeHttp) continue;
|
|
938
|
+
appendEvidence(evidenceByPackage, owner, {
|
|
939
|
+
filePath: relative(projectPath, file.path),
|
|
940
|
+
line: lineNumber(file.content, "createServer"),
|
|
941
|
+
detail: "Custom HTTP routing found without the SvelteKit/SMRT app-shell dependency pattern."
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return Array.from(evidenceByPackage.entries()).map(([owner, evidence]) => ({
|
|
945
|
+
severity: "medium",
|
|
946
|
+
area: "api-shell",
|
|
947
|
+
code: "custom-http-shell",
|
|
948
|
+
title: `${packageLabel(owner)} uses a custom HTTP shell`,
|
|
949
|
+
evidence,
|
|
950
|
+
recommendation: "Compare the app shell against the Anytown/Ergot SvelteKit + SMRT shell pattern before adding custom HTTP infrastructure.",
|
|
951
|
+
suggestedIssueTitle: `Align ${packageLabel(owner)} app shell with SMRT/SvelteKit conventions`
|
|
952
|
+
}));
|
|
953
|
+
}
|
|
954
|
+
function appendEvidence(evidenceByPackage, pkg, evidence) {
|
|
955
|
+
const existing = evidenceByPackage.get(pkg);
|
|
956
|
+
if (existing) {
|
|
957
|
+
existing.push(evidence);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
evidenceByPackage.set(pkg, [evidence]);
|
|
961
|
+
}
|
|
962
|
+
function findLocalAuthTenancy(sourceFiles, packages, projectPath) {
|
|
963
|
+
return sourceFiles.flatMap((file) => {
|
|
964
|
+
const owner = findOwningPackage(file.path, packages);
|
|
965
|
+
const pathSignal = /(auth|tenant|audit|session|rbac|user)/i.test(file.path);
|
|
966
|
+
const codeSignal = /\b(tenantId|tenant|role|permission|session|auditLog|apiKey)\b/.test(
|
|
967
|
+
file.content
|
|
968
|
+
);
|
|
969
|
+
if (!pathSignal || !codeSignal) return [];
|
|
970
|
+
const hasApprovedPackages = owner.dependencies.has("@happyvertical/smrt-users") || owner.dependencies.has("@happyvertical/smrt-tenancy") || owner.dependencies.has("@happyvertical/smrt-profiles");
|
|
971
|
+
if (hasApprovedPackages) return [];
|
|
972
|
+
return [
|
|
973
|
+
{
|
|
974
|
+
severity: "medium",
|
|
975
|
+
area: "auth-tenancy",
|
|
976
|
+
code: "local-auth-tenancy",
|
|
977
|
+
title: `${packageLabel(owner)} contains local auth/tenancy/audit logic`,
|
|
978
|
+
evidence: [
|
|
979
|
+
{
|
|
980
|
+
filePath: relative(projectPath, file.path),
|
|
981
|
+
line: lineNumber(file.content, "tenant"),
|
|
982
|
+
detail: "Auth, tenancy, session, role, or audit terminology appears without smrt-users/smrt-tenancy/smrt-profiles dependencies."
|
|
983
|
+
}
|
|
984
|
+
],
|
|
985
|
+
recommendation: "Add explicit adapters to smrt-users, smrt-tenancy, and profile/audit models or document why this local implementation is intentionally isolated.",
|
|
986
|
+
suggestedIssueTitle: `Review auth and tenancy adapters in ${packageLabel(owner)}`
|
|
987
|
+
}
|
|
988
|
+
];
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
function findUiShellDrift(packages, projectPath) {
|
|
992
|
+
return packages.flatMap((pkg) => {
|
|
993
|
+
const hasUiSource = pkg.sourceFiles.some(
|
|
994
|
+
(file) => file.path.endsWith(".svelte")
|
|
995
|
+
);
|
|
996
|
+
const likelyUiPackage = hasUiSource || /(?:web|ui|app|site|frontend)/i.test(packageLabel(pkg)) || pkg.dependencies.has("svelte") || pkg.dependencies.has("vite");
|
|
997
|
+
if (!likelyUiPackage) return [];
|
|
998
|
+
if (pkg.dependencies.has("@happyvertical/smrt-svelte")) return [];
|
|
999
|
+
return [
|
|
1000
|
+
{
|
|
1001
|
+
severity: "low",
|
|
1002
|
+
area: "ui-shell",
|
|
1003
|
+
code: "missing-smrt-svelte-shell",
|
|
1004
|
+
title: `${packageLabel(pkg)} looks like UI work without @happyvertical/smrt-svelte`,
|
|
1005
|
+
evidence: [
|
|
1006
|
+
{
|
|
1007
|
+
filePath: relative(projectPath, pkg.packageJsonPath),
|
|
1008
|
+
detail: "UI-facing package does not declare @happyvertical/smrt-svelte."
|
|
1009
|
+
}
|
|
1010
|
+
],
|
|
1011
|
+
recommendation: "Use @happyvertical/smrt-svelte and the SvelteKit shell pattern for downstream app UI unless this package is intentionally framework-agnostic.",
|
|
1012
|
+
suggestedIssueTitle: `Check SMRT Svelte shell alignment for ${packageLabel(pkg)}`
|
|
1013
|
+
}
|
|
1014
|
+
];
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
function findMissingManifestArtifacts(sourceFiles, packages, projectPath) {
|
|
1018
|
+
return packages.flatMap((pkg) => {
|
|
1019
|
+
const hasSmrtSource = pkg.sourceFiles.some(
|
|
1020
|
+
(file) => /@smrt\s*\(/.test(file.content)
|
|
1021
|
+
);
|
|
1022
|
+
if (!hasSmrtSource) return [];
|
|
1023
|
+
const hasManifest = sourceFiles.some(
|
|
1024
|
+
(file) => file.packageDir === pkg.directory && /@happyvertical\/smrt-scanner/.test(file.content)
|
|
1025
|
+
) || pathExistsSyncHint(join(pkg.directory, ".smrt", "manifest.json")) || pathExistsSyncHint(join(pkg.directory, "dist", "manifest.json"));
|
|
1026
|
+
if (hasManifest) return [];
|
|
1027
|
+
return [
|
|
1028
|
+
{
|
|
1029
|
+
severity: "low",
|
|
1030
|
+
area: "manifest",
|
|
1031
|
+
code: "missing-generated-manifest-artifact",
|
|
1032
|
+
title: `${packageLabel(pkg)} has @smrt objects but no generated manifest artifact was found`,
|
|
1033
|
+
evidence: [
|
|
1034
|
+
{
|
|
1035
|
+
filePath: relative(projectPath, pkg.packageJsonPath),
|
|
1036
|
+
detail: "No .smrt/manifest.json or dist/manifest.json was visible during review."
|
|
1037
|
+
}
|
|
1038
|
+
],
|
|
1039
|
+
recommendation: "Run the package build/test manifest generation path and verify it uses the SMRT scanner/runtime manifest pipeline.",
|
|
1040
|
+
suggestedIssueTitle: `Verify SMRT manifest generation for ${packageLabel(pkg)}`
|
|
1041
|
+
}
|
|
1042
|
+
];
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
async function visit(dir, onFile) {
|
|
1046
|
+
let entries;
|
|
1047
|
+
try {
|
|
1048
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1049
|
+
} catch {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
for (const entry of entries) {
|
|
1053
|
+
const fullPath = join(dir, entry.name);
|
|
1054
|
+
if (entry.isDirectory()) {
|
|
1055
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1056
|
+
await visit(fullPath, onFile);
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (entry.isFile()) await onFile(fullPath, entry.name);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function collectDependencies(packageJson) {
|
|
1063
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
1064
|
+
for (const sectionName of DEPENDENCY_SECTIONS) {
|
|
1065
|
+
const section = packageJson[sectionName];
|
|
1066
|
+
if (!isRecord(section)) continue;
|
|
1067
|
+
for (const dependencyName of Object.keys(section)) {
|
|
1068
|
+
dependencies.add(dependencyName);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return dependencies;
|
|
1072
|
+
}
|
|
1073
|
+
function extractImports(content) {
|
|
1074
|
+
const imports = /* @__PURE__ */ new Set();
|
|
1075
|
+
const patterns = [
|
|
1076
|
+
/(?:import|export)\s+(?:type\s+)?(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/g,
|
|
1077
|
+
/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
1078
|
+
/\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
1079
|
+
];
|
|
1080
|
+
for (const pattern of patterns) {
|
|
1081
|
+
for (const match of content.matchAll(pattern)) {
|
|
1082
|
+
const imported = normalizePackageImport(match[1]);
|
|
1083
|
+
if (imported) imports.add(imported);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return Array.from(imports);
|
|
1087
|
+
}
|
|
1088
|
+
function normalizePackageImport(value) {
|
|
1089
|
+
if (!value || value.startsWith(".") || value.startsWith("/"))
|
|
1090
|
+
return void 0;
|
|
1091
|
+
if (value.startsWith("@")) {
|
|
1092
|
+
const [scope, name] = value.split("/");
|
|
1093
|
+
return scope && name ? `${scope}/${name}` : value;
|
|
1094
|
+
}
|
|
1095
|
+
return value.split("/")[0];
|
|
1096
|
+
}
|
|
1097
|
+
function findOwningPackage(filePath, packages) {
|
|
1098
|
+
const normalized = resolve(filePath);
|
|
1099
|
+
const matches = packages.filter(
|
|
1100
|
+
(pkg) => normalized === pkg.directory || normalized.startsWith(`${pkg.directory}${sep}`)
|
|
1101
|
+
).sort((left, right) => right.directory.length - left.directory.length);
|
|
1102
|
+
return matches[0] ?? packages[0];
|
|
1103
|
+
}
|
|
1104
|
+
function packageInventory(pkg) {
|
|
1105
|
+
const declaredHappyVerticalDependencies = Array.from(pkg.dependencies).filter((dependency) => dependency.startsWith(HAPPYVERTICAL_PACKAGE_PREFIX)).sort();
|
|
1106
|
+
const importedHappyVerticalPackages = Array.from(pkg.imports.keys()).sort();
|
|
1107
|
+
const ownName = typeof pkg.json.name === "string" ? pkg.json.name : void 0;
|
|
1108
|
+
return {
|
|
1109
|
+
name: packageLabel(pkg),
|
|
1110
|
+
path: pkg.relativePath,
|
|
1111
|
+
private: typeof pkg.json.private === "boolean" ? pkg.json.private : void 0,
|
|
1112
|
+
scripts: Object.keys(pkg.scripts).sort(),
|
|
1113
|
+
declaredHappyVerticalDependencies,
|
|
1114
|
+
importedHappyVerticalPackages,
|
|
1115
|
+
missingHappyVerticalDependencies: importedHappyVerticalPackages.filter(
|
|
1116
|
+
(name) => name !== ownName && !pkg.dependencies.has(name)
|
|
1117
|
+
),
|
|
1118
|
+
hasSvelteKit: pkg.dependencies.has("@sveltejs/kit"),
|
|
1119
|
+
hasSmrtSvelte: pkg.dependencies.has("@happyvertical/smrt-svelte")
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function summarizeFindings(findings) {
|
|
1123
|
+
return findings.reduce(
|
|
1124
|
+
(summary, finding) => {
|
|
1125
|
+
summary[finding.severity]++;
|
|
1126
|
+
return summary;
|
|
1127
|
+
},
|
|
1128
|
+
{ high: 0, medium: 0, low: 0 }
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
function limitFindings(findings, maxFindings) {
|
|
1132
|
+
const sorted = findings.sort(
|
|
1133
|
+
(left, right) => severityRank(left.severity) - severityRank(right.severity) || left.area.localeCompare(right.area) || left.title.localeCompare(right.title)
|
|
1134
|
+
);
|
|
1135
|
+
return maxFindings && maxFindings > 0 ? sorted.slice(0, maxFindings) : sorted;
|
|
1136
|
+
}
|
|
1137
|
+
function severityRank(severity) {
|
|
1138
|
+
return severity === "high" ? 0 : severity === "medium" ? 1 : 2;
|
|
1139
|
+
}
|
|
1140
|
+
function packageLabel(pkg) {
|
|
1141
|
+
return typeof pkg.json.name === "string" ? pkg.json.name : pkg.relativePath;
|
|
1142
|
+
}
|
|
1143
|
+
function lineNumber(content, needle) {
|
|
1144
|
+
const index = content.indexOf(needle);
|
|
1145
|
+
if (index < 0) return void 0;
|
|
1146
|
+
return content.slice(0, index).split("\n").length;
|
|
1147
|
+
}
|
|
1148
|
+
function isRecord(value) {
|
|
1149
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1150
|
+
}
|
|
1151
|
+
async function pathExists(path) {
|
|
1152
|
+
try {
|
|
1153
|
+
await access(path);
|
|
1154
|
+
return true;
|
|
1155
|
+
} catch {
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function pathExistsSyncHint(path) {
|
|
1160
|
+
return existsSync(path);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const SERVER_NAME = "smrt-dev-mcp";
|
|
1164
|
+
const SERVER_VERSION = readPackageVersion();
|
|
1165
|
+
const DEBUG = process.env.DEBUG === "true";
|
|
1166
|
+
const REVIEW_SKILL_NAME = "smrt-code-review";
|
|
1167
|
+
const REVIEW_SKILL_URI = `smrt-dev-mcp://agent-skills/${REVIEW_SKILL_NAME}`;
|
|
1168
|
+
const DOMAIN_CODE_REVIEW_PROMPT = "domain-code-review";
|
|
1169
|
+
const DOMAIN_ARCHITECTURE_PROMPT = "domain-architecture";
|
|
1170
|
+
const KNOWLEDGE_PROJECT_URI = "smrt://knowledge/project";
|
|
1171
|
+
const KNOWLEDGE_PACKAGE_PREFIX = "smrt://knowledge/package/";
|
|
1172
|
+
const TOOLS = [
|
|
1173
|
+
// Code Generation Tools
|
|
1174
|
+
{
|
|
1175
|
+
name: "generate-smrt-class",
|
|
1176
|
+
description: "Generate a complete SMRT class with @smrt() decorator",
|
|
1177
|
+
inputSchema: {
|
|
1178
|
+
type: "object",
|
|
1179
|
+
properties: {
|
|
1180
|
+
className: {
|
|
1181
|
+
type: "string",
|
|
1182
|
+
description: "Name of the class (PascalCase)"
|
|
1183
|
+
},
|
|
1184
|
+
properties: {
|
|
1185
|
+
type: "array",
|
|
1186
|
+
description: "Array of property definitions",
|
|
1187
|
+
items: {
|
|
1188
|
+
type: "object",
|
|
1189
|
+
properties: {
|
|
1190
|
+
name: { type: "string" },
|
|
1191
|
+
type: {
|
|
1192
|
+
type: "string",
|
|
1193
|
+
enum: [
|
|
1194
|
+
"text",
|
|
1195
|
+
"integer",
|
|
1196
|
+
"decimal",
|
|
1197
|
+
"boolean",
|
|
1198
|
+
"datetime",
|
|
1199
|
+
"json"
|
|
1200
|
+
]
|
|
1201
|
+
},
|
|
1202
|
+
required: { type: "boolean" },
|
|
1203
|
+
nullable: { type: "boolean" },
|
|
1204
|
+
description: { type: "string" },
|
|
1205
|
+
defaultValue: {
|
|
1206
|
+
oneOf: [
|
|
1207
|
+
{ type: "string" },
|
|
1208
|
+
{ type: "number" },
|
|
1209
|
+
{ type: "boolean" },
|
|
1210
|
+
{ type: "object" },
|
|
1211
|
+
{ type: "null" }
|
|
1212
|
+
]
|
|
1213
|
+
}
|
|
1214
|
+
},
|
|
1215
|
+
required: ["name", "type"]
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
baseClass: {
|
|
1219
|
+
type: "string",
|
|
1220
|
+
enum: ["SmrtObject", "SmrtCollection"],
|
|
1221
|
+
default: "SmrtObject"
|
|
1222
|
+
},
|
|
1223
|
+
template: {
|
|
1224
|
+
type: "string",
|
|
1225
|
+
enum: [
|
|
1226
|
+
"basic",
|
|
1227
|
+
"global-catalog",
|
|
1228
|
+
"optional-catalog",
|
|
1229
|
+
"tenant-project-object",
|
|
1230
|
+
"tenant-event-log-object",
|
|
1231
|
+
"cross-package-reference"
|
|
1232
|
+
],
|
|
1233
|
+
default: "basic"
|
|
1234
|
+
},
|
|
1235
|
+
tableName: { type: "string" },
|
|
1236
|
+
conflictColumns: {
|
|
1237
|
+
type: "array",
|
|
1238
|
+
items: { type: "string" }
|
|
1239
|
+
},
|
|
1240
|
+
tenantScoped: {
|
|
1241
|
+
oneOf: [
|
|
1242
|
+
{ type: "boolean" },
|
|
1243
|
+
{
|
|
1244
|
+
type: "object",
|
|
1245
|
+
properties: {
|
|
1246
|
+
mode: { type: "string", enum: ["required", "optional"] },
|
|
1247
|
+
field: { type: "string" },
|
|
1248
|
+
autoFilter: { type: "boolean" },
|
|
1249
|
+
autoPopulate: { type: "boolean" },
|
|
1250
|
+
allowSuperAdminBypass: { type: "boolean" }
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
]
|
|
1254
|
+
},
|
|
1255
|
+
includeTenantIdField: { type: "boolean" },
|
|
1256
|
+
relationships: {
|
|
1257
|
+
type: "array",
|
|
1258
|
+
items: {
|
|
1259
|
+
type: "object",
|
|
1260
|
+
properties: {
|
|
1261
|
+
name: { type: "string" },
|
|
1262
|
+
type: {
|
|
1263
|
+
type: "string",
|
|
1264
|
+
enum: [
|
|
1265
|
+
"foreignKey",
|
|
1266
|
+
"crossPackageRef",
|
|
1267
|
+
"oneToMany",
|
|
1268
|
+
"manyToMany"
|
|
1269
|
+
]
|
|
1270
|
+
},
|
|
1271
|
+
related: { type: "string" },
|
|
1272
|
+
required: { type: "boolean" },
|
|
1273
|
+
nullable: { type: "boolean" },
|
|
1274
|
+
description: { type: "string" },
|
|
1275
|
+
validate: { type: "boolean" },
|
|
1276
|
+
foreignKey: { type: "string" },
|
|
1277
|
+
through: { type: "string" },
|
|
1278
|
+
sourceKey: { type: "string" },
|
|
1279
|
+
targetKey: { type: "string" }
|
|
1280
|
+
},
|
|
1281
|
+
required: ["name", "type", "related"]
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
includeCompanionSnippets: { type: "boolean", default: false },
|
|
1285
|
+
includeApiConfig: { type: "boolean", default: true },
|
|
1286
|
+
includeMcpConfig: { type: "boolean", default: true },
|
|
1287
|
+
includeCliConfig: { type: "boolean", default: true }
|
|
1288
|
+
},
|
|
1289
|
+
required: ["className", "properties"]
|
|
1290
|
+
}
|
|
1291
|
+
},
|
|
1292
|
+
// Project Introspection Tools
|
|
1293
|
+
{
|
|
1294
|
+
name: "introspect-project",
|
|
1295
|
+
description: "Scan current directory for SMRT objects",
|
|
1296
|
+
inputSchema: {
|
|
1297
|
+
type: "object",
|
|
1298
|
+
properties: {
|
|
1299
|
+
directory: {
|
|
1300
|
+
type: "string",
|
|
1301
|
+
description: "Project directory (default: cwd)"
|
|
1302
|
+
},
|
|
1303
|
+
manifestPath: {
|
|
1304
|
+
type: "string",
|
|
1305
|
+
description: "Optional manifest path. Defaults to .smrt/manifest.json, dist/manifest.json, then source scanning."
|
|
1306
|
+
},
|
|
1307
|
+
includeFields: {
|
|
1308
|
+
type: "boolean",
|
|
1309
|
+
description: "Include field details"
|
|
1310
|
+
},
|
|
1311
|
+
includeRelationships: {
|
|
1312
|
+
type: "boolean",
|
|
1313
|
+
description: "Analyze relationships"
|
|
1314
|
+
},
|
|
1315
|
+
includeMethods: {
|
|
1316
|
+
type: "boolean",
|
|
1317
|
+
description: "Include public method details"
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
name: "review-smrt-project",
|
|
1324
|
+
description: "Advisory ecosystem alignment review for downstream SMRT projects",
|
|
1325
|
+
inputSchema: {
|
|
1326
|
+
type: "object",
|
|
1327
|
+
properties: {
|
|
1328
|
+
directory: {
|
|
1329
|
+
type: "string",
|
|
1330
|
+
description: "Project directory (default: cwd)"
|
|
1331
|
+
},
|
|
1332
|
+
rootDir: {
|
|
1333
|
+
type: "string",
|
|
1334
|
+
description: "Compatibility alias for directory"
|
|
1335
|
+
},
|
|
1336
|
+
includeSourceEvidence: {
|
|
1337
|
+
type: "boolean",
|
|
1338
|
+
description: "Include file and line evidence in findings",
|
|
1339
|
+
default: true
|
|
1340
|
+
},
|
|
1341
|
+
maxFindings: {
|
|
1342
|
+
type: "number",
|
|
1343
|
+
description: "Optional maximum number of findings to return"
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
name: "reflect-knowledge",
|
|
1350
|
+
description: "Report deterministic SMRT + HappyVertical SDK knowledge coverage and freshness",
|
|
1351
|
+
inputSchema: {
|
|
1352
|
+
type: "object",
|
|
1353
|
+
properties: {
|
|
1354
|
+
rootDir: {
|
|
1355
|
+
type: "string",
|
|
1356
|
+
description: "Project root directory (default: cwd)"
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
name: "reflect-domain-knowledge",
|
|
1363
|
+
description: "Report domain-scoped SMRT knowledge artifacts, SDK packages, and freshness",
|
|
1364
|
+
inputSchema: {
|
|
1365
|
+
type: "object",
|
|
1366
|
+
properties: {
|
|
1367
|
+
rootDir: { type: "string" },
|
|
1368
|
+
scope: {
|
|
1369
|
+
type: "string",
|
|
1370
|
+
enum: ["project", "local", "package", "sdk"],
|
|
1371
|
+
default: "project"
|
|
1372
|
+
},
|
|
1373
|
+
package: { type: "string" }
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
name: "check-knowledge-freshness",
|
|
1379
|
+
description: "Run deterministic freshness checks for SMRT agent knowledge",
|
|
1380
|
+
inputSchema: {
|
|
1381
|
+
type: "object",
|
|
1382
|
+
properties: {
|
|
1383
|
+
rootDir: { type: "string" },
|
|
1384
|
+
changed: {
|
|
1385
|
+
type: "boolean",
|
|
1386
|
+
description: "Limit stale-pattern checks to changed files"
|
|
1387
|
+
},
|
|
1388
|
+
strict: {
|
|
1389
|
+
type: "boolean",
|
|
1390
|
+
description: "Treat stale-pattern findings as errors"
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
name: "check-domain-knowledge",
|
|
1397
|
+
description: "Run deterministic freshness checks for domain knowledge artifacts",
|
|
1398
|
+
inputSchema: {
|
|
1399
|
+
type: "object",
|
|
1400
|
+
properties: {
|
|
1401
|
+
rootDir: { type: "string" },
|
|
1402
|
+
changed: { type: "boolean" },
|
|
1403
|
+
strict: { type: "boolean" },
|
|
1404
|
+
scope: {
|
|
1405
|
+
type: "string",
|
|
1406
|
+
enum: ["project", "local", "package", "sdk"],
|
|
1407
|
+
default: "project"
|
|
1408
|
+
},
|
|
1409
|
+
package: { type: "string" }
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
name: "build-review-context",
|
|
1415
|
+
description: "Build model-ready SMRT review context from changed files and optional focus text",
|
|
1416
|
+
inputSchema: {
|
|
1417
|
+
type: "object",
|
|
1418
|
+
properties: {
|
|
1419
|
+
rootDir: { type: "string" },
|
|
1420
|
+
changedFiles: { type: "array", items: { type: "string" } },
|
|
1421
|
+
focus: { type: "string" },
|
|
1422
|
+
documentation: { type: "string" }
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
},
|
|
1426
|
+
{
|
|
1427
|
+
name: "build-domain-review-context",
|
|
1428
|
+
description: "Build domain-scoped model-ready SMRT review context and prompt bundle",
|
|
1429
|
+
inputSchema: {
|
|
1430
|
+
type: "object",
|
|
1431
|
+
properties: {
|
|
1432
|
+
rootDir: { type: "string" },
|
|
1433
|
+
changedFiles: { type: "array", items: { type: "string" } },
|
|
1434
|
+
focus: { type: "string" },
|
|
1435
|
+
documentation: { type: "string" },
|
|
1436
|
+
scope: {
|
|
1437
|
+
type: "string",
|
|
1438
|
+
enum: ["project", "local", "package", "sdk"],
|
|
1439
|
+
default: "project"
|
|
1440
|
+
},
|
|
1441
|
+
package: { type: "string" }
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
name: "smrt-review",
|
|
1447
|
+
description: 'Return deterministic review findings and/or a reusable model prompt bundle. For a formal downstream review, first call get-agent-skill with { "name": "smrt-code-review" } or load the smrt-code-review MCP prompt/resource.',
|
|
1448
|
+
inputSchema: {
|
|
1449
|
+
type: "object",
|
|
1450
|
+
properties: {
|
|
1451
|
+
rootDir: { type: "string" },
|
|
1452
|
+
changedFiles: { type: "array", items: { type: "string" } },
|
|
1453
|
+
focus: { type: "string" },
|
|
1454
|
+
documentation: { type: "string" },
|
|
1455
|
+
mode: {
|
|
1456
|
+
type: "string",
|
|
1457
|
+
enum: ["findings", "prompt-bundle", "both"],
|
|
1458
|
+
default: "both"
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
name: "build-architecture-context",
|
|
1465
|
+
description: "Build model-ready SMRT architecture context from an idea or documentation",
|
|
1466
|
+
inputSchema: {
|
|
1467
|
+
type: "object",
|
|
1468
|
+
properties: {
|
|
1469
|
+
rootDir: { type: "string" },
|
|
1470
|
+
idea: { type: "string" },
|
|
1471
|
+
documentation: { type: "string" },
|
|
1472
|
+
focus: { type: "string" }
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
name: "build-domain-architecture-context",
|
|
1478
|
+
description: "Build domain-scoped model-ready SMRT architecture context and prompt bundle",
|
|
1479
|
+
inputSchema: {
|
|
1480
|
+
type: "object",
|
|
1481
|
+
properties: {
|
|
1482
|
+
rootDir: { type: "string" },
|
|
1483
|
+
idea: { type: "string" },
|
|
1484
|
+
documentation: { type: "string" },
|
|
1485
|
+
focus: { type: "string" },
|
|
1486
|
+
scope: {
|
|
1487
|
+
type: "string",
|
|
1488
|
+
enum: ["project", "local", "package", "sdk"],
|
|
1489
|
+
default: "project"
|
|
1490
|
+
},
|
|
1491
|
+
package: { type: "string" }
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
},
|
|
1495
|
+
{
|
|
1496
|
+
name: "smrt-architecture",
|
|
1497
|
+
description: "Suggest SMRT and HappyVertical SDK packages and return an architecture prompt bundle",
|
|
1498
|
+
inputSchema: {
|
|
1499
|
+
type: "object",
|
|
1500
|
+
properties: {
|
|
1501
|
+
rootDir: { type: "string" },
|
|
1502
|
+
idea: { type: "string" },
|
|
1503
|
+
documentation: { type: "string" },
|
|
1504
|
+
focus: { type: "string" }
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
},
|
|
1508
|
+
{
|
|
1509
|
+
name: "list-agent-skills",
|
|
1510
|
+
description: "List bundled harness-agnostic agent skills shipped with smrt-dev-mcp",
|
|
1511
|
+
inputSchema: {
|
|
1512
|
+
type: "object",
|
|
1513
|
+
properties: {}
|
|
1514
|
+
}
|
|
1515
|
+
},
|
|
1516
|
+
{
|
|
1517
|
+
name: "get-agent-skill",
|
|
1518
|
+
description: "Return a bundled harness-agnostic agent skill as Markdown plus optional references",
|
|
1519
|
+
inputSchema: {
|
|
1520
|
+
type: "object",
|
|
1521
|
+
properties: {
|
|
1522
|
+
name: {
|
|
1523
|
+
type: "string",
|
|
1524
|
+
enum: [REVIEW_SKILL_NAME],
|
|
1525
|
+
description: "Bundled agent skill name"
|
|
1526
|
+
},
|
|
1527
|
+
includeReferences: {
|
|
1528
|
+
type: "boolean",
|
|
1529
|
+
default: true,
|
|
1530
|
+
description: "Include referenced files with the skill bundle"
|
|
1531
|
+
}
|
|
1532
|
+
},
|
|
1533
|
+
required: ["name"]
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
];
|
|
1537
|
+
async function main() {
|
|
1538
|
+
if (DEBUG) {
|
|
1539
|
+
console.error(`[${SERVER_NAME}] Starting server v${SERVER_VERSION}`);
|
|
1540
|
+
}
|
|
1541
|
+
const server = new Server(
|
|
1542
|
+
{
|
|
1543
|
+
name: SERVER_NAME,
|
|
1544
|
+
version: SERVER_VERSION
|
|
1545
|
+
},
|
|
1546
|
+
{
|
|
1547
|
+
capabilities: {
|
|
1548
|
+
prompts: {},
|
|
1549
|
+
resources: {},
|
|
1550
|
+
tools: {}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
);
|
|
1554
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1555
|
+
if (DEBUG) {
|
|
1556
|
+
console.error(`[${SERVER_NAME}] ListTools request`);
|
|
1557
|
+
}
|
|
1558
|
+
return { tools: TOOLS };
|
|
1559
|
+
});
|
|
1560
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
1561
|
+
return {
|
|
1562
|
+
prompts: [
|
|
1563
|
+
{
|
|
1564
|
+
name: REVIEW_SKILL_NAME,
|
|
1565
|
+
title: "SMRT Code Review",
|
|
1566
|
+
description: "Harness-agnostic downstream SMRT review procedure that uses smrt-dev-mcp deterministic context and prompt bundles."
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
name: DOMAIN_CODE_REVIEW_PROMPT,
|
|
1570
|
+
title: "Domain Code Review",
|
|
1571
|
+
description: "Model-ready domain-scoped SMRT code review prompt bundle.",
|
|
1572
|
+
arguments: [
|
|
1573
|
+
{
|
|
1574
|
+
name: "rootDir",
|
|
1575
|
+
description: "Project root directory. Defaults to server cwd.",
|
|
1576
|
+
required: false
|
|
1577
|
+
},
|
|
1578
|
+
{
|
|
1579
|
+
name: "changedFiles",
|
|
1580
|
+
description: "Changed file paths as newline-separated, comma-separated, or JSON array text.",
|
|
1581
|
+
required: false
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
name: "focus",
|
|
1585
|
+
description: "Review focus text.",
|
|
1586
|
+
required: false
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
name: "documentation",
|
|
1590
|
+
description: "Additional documentation or notes.",
|
|
1591
|
+
required: false
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
name: "scope",
|
|
1595
|
+
description: "Knowledge scope: project, local, package, or sdk.",
|
|
1596
|
+
required: false
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
name: "package",
|
|
1600
|
+
description: "Package name or short package selector.",
|
|
1601
|
+
required: false
|
|
1602
|
+
}
|
|
1603
|
+
]
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
name: DOMAIN_ARCHITECTURE_PROMPT,
|
|
1607
|
+
title: "Domain Architecture",
|
|
1608
|
+
description: "Model-ready domain-scoped SMRT architecture planning prompt bundle.",
|
|
1609
|
+
arguments: [
|
|
1610
|
+
{
|
|
1611
|
+
name: "rootDir",
|
|
1612
|
+
description: "Project root directory. Defaults to server cwd.",
|
|
1613
|
+
required: false
|
|
1614
|
+
},
|
|
1615
|
+
{
|
|
1616
|
+
name: "idea",
|
|
1617
|
+
description: "Architecture idea or product concept.",
|
|
1618
|
+
required: false
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
name: "documentation",
|
|
1622
|
+
description: "Additional documentation or notes.",
|
|
1623
|
+
required: false
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
name: "focus",
|
|
1627
|
+
description: "Planning focus text.",
|
|
1628
|
+
required: false
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
name: "scope",
|
|
1632
|
+
description: "Knowledge scope: project, local, package, or sdk.",
|
|
1633
|
+
required: false
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
name: "package",
|
|
1637
|
+
description: "Package name or short package selector.",
|
|
1638
|
+
required: false
|
|
1639
|
+
}
|
|
1640
|
+
]
|
|
1641
|
+
}
|
|
1642
|
+
]
|
|
1643
|
+
};
|
|
1644
|
+
});
|
|
1645
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1646
|
+
const { name } = request.params;
|
|
1647
|
+
if (name === REVIEW_SKILL_NAME) {
|
|
1648
|
+
return {
|
|
1649
|
+
description: "Use this procedure when reviewing downstream SMRT projects.",
|
|
1650
|
+
messages: [
|
|
1651
|
+
{
|
|
1652
|
+
role: "user",
|
|
1653
|
+
content: {
|
|
1654
|
+
type: "text",
|
|
1655
|
+
text: renderAgentSkillMarkdown(REVIEW_SKILL_NAME)
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
]
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
if (name === DOMAIN_CODE_REVIEW_PROMPT) {
|
|
1662
|
+
const context = await buildReviewContext(
|
|
1663
|
+
reviewPromptArguments(request.params.arguments)
|
|
1664
|
+
);
|
|
1665
|
+
return {
|
|
1666
|
+
description: "Review downstream SMRT code with domain knowledge.",
|
|
1667
|
+
messages: [
|
|
1668
|
+
{
|
|
1669
|
+
role: "user",
|
|
1670
|
+
content: {
|
|
1671
|
+
type: "text",
|
|
1672
|
+
text: context.promptBundle.contextMarkdown
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
]
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
if (name === DOMAIN_ARCHITECTURE_PROMPT) {
|
|
1679
|
+
const context = await buildArchitectureContext(
|
|
1680
|
+
architecturePromptArguments(request.params.arguments)
|
|
1681
|
+
);
|
|
1682
|
+
return {
|
|
1683
|
+
description: "Plan a downstream SMRT project with domain knowledge.",
|
|
1684
|
+
messages: [
|
|
1685
|
+
{
|
|
1686
|
+
role: "user",
|
|
1687
|
+
content: {
|
|
1688
|
+
type: "text",
|
|
1689
|
+
text: context.promptBundle.contextMarkdown
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
]
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown prompt: ${name}`);
|
|
1696
|
+
});
|
|
1697
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1698
|
+
const index = await buildKnowledgeIndex();
|
|
1699
|
+
return {
|
|
1700
|
+
resources: [
|
|
1701
|
+
{
|
|
1702
|
+
uri: REVIEW_SKILL_URI,
|
|
1703
|
+
name: REVIEW_SKILL_NAME,
|
|
1704
|
+
title: "SMRT Code Review Skill",
|
|
1705
|
+
description: "Bundled Markdown skill for downstream SMRT code reviews.",
|
|
1706
|
+
mimeType: "text/markdown"
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
uri: KNOWLEDGE_PROJECT_URI,
|
|
1710
|
+
name: "smrt-domain-knowledge-project",
|
|
1711
|
+
title: "SMRT Domain Knowledge Project Index",
|
|
1712
|
+
description: "Composed SMRT, downstream domain, and HappyVertical SDK knowledge index.",
|
|
1713
|
+
mimeType: "application/json"
|
|
1714
|
+
},
|
|
1715
|
+
...index.packages.map((pkg) => ({
|
|
1716
|
+
uri: `${KNOWLEDGE_PACKAGE_PREFIX}${encodeURIComponent(pkg.name)}`,
|
|
1717
|
+
name: `smrt-domain-knowledge-${pkg.name}`,
|
|
1718
|
+
title: `SMRT Domain Knowledge: ${pkg.name}`,
|
|
1719
|
+
description: "Package-scoped SMRT domain knowledge, generated surfaces, and authored context.",
|
|
1720
|
+
mimeType: "application/json"
|
|
1721
|
+
}))
|
|
1722
|
+
]
|
|
1723
|
+
};
|
|
1724
|
+
});
|
|
1725
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1726
|
+
const { uri } = request.params;
|
|
1727
|
+
if (uri === REVIEW_SKILL_URI) {
|
|
1728
|
+
return {
|
|
1729
|
+
contents: [
|
|
1730
|
+
{
|
|
1731
|
+
uri,
|
|
1732
|
+
mimeType: "text/markdown",
|
|
1733
|
+
text: renderAgentSkillMarkdown(REVIEW_SKILL_NAME)
|
|
1734
|
+
}
|
|
1735
|
+
]
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
if (uri === KNOWLEDGE_PROJECT_URI) {
|
|
1739
|
+
const index = await buildKnowledgeIndex();
|
|
1740
|
+
return {
|
|
1741
|
+
contents: [
|
|
1742
|
+
{
|
|
1743
|
+
uri,
|
|
1744
|
+
mimeType: "application/json",
|
|
1745
|
+
text: JSON.stringify(sanitizeKnowledgeIndex(index), null, 2)
|
|
1746
|
+
}
|
|
1747
|
+
]
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
if (uri.startsWith(KNOWLEDGE_PACKAGE_PREFIX)) {
|
|
1751
|
+
const packageName = decodeURIComponent(
|
|
1752
|
+
uri.slice(KNOWLEDGE_PACKAGE_PREFIX.length)
|
|
1753
|
+
);
|
|
1754
|
+
const index = await buildKnowledgeIndex();
|
|
1755
|
+
const pkg = index.packages.find((item) => item.name === packageName);
|
|
1756
|
+
if (!pkg) {
|
|
1757
|
+
throw new McpError(
|
|
1758
|
+
ErrorCode.InvalidParams,
|
|
1759
|
+
`Unknown knowledge package: ${packageName}`
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
return {
|
|
1763
|
+
contents: [
|
|
1764
|
+
{
|
|
1765
|
+
uri,
|
|
1766
|
+
mimeType: "application/json",
|
|
1767
|
+
text: JSON.stringify(sanitizeKnowledgePackage(pkg), null, 2)
|
|
1768
|
+
}
|
|
1769
|
+
]
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown resource: ${uri}`);
|
|
1773
|
+
});
|
|
1774
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1775
|
+
const { name, arguments: args } = request.params;
|
|
1776
|
+
if (DEBUG) {
|
|
1777
|
+
console.error(`[${SERVER_NAME}] CallTool: ${name}`);
|
|
1778
|
+
console.error(
|
|
1779
|
+
`[${SERVER_NAME}] Arguments:`,
|
|
1780
|
+
JSON.stringify(args, null, 2)
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
try {
|
|
1784
|
+
let result;
|
|
1785
|
+
switch (name) {
|
|
1786
|
+
case "generate-smrt-class":
|
|
1787
|
+
result = await generateSmrtClass(args);
|
|
1788
|
+
break;
|
|
1789
|
+
case "introspect-project":
|
|
1790
|
+
result = await introspectProject(args);
|
|
1791
|
+
break;
|
|
1792
|
+
case "review-smrt-project":
|
|
1793
|
+
result = await reviewSmrtProject(args);
|
|
1794
|
+
break;
|
|
1795
|
+
case "reflect-knowledge": {
|
|
1796
|
+
const index = await buildKnowledgeIndex(args);
|
|
1797
|
+
const freshness = await checkKnowledgeFreshnessFromIndex(
|
|
1798
|
+
index,
|
|
1799
|
+
args
|
|
1800
|
+
);
|
|
1801
|
+
result = JSON.stringify(
|
|
1802
|
+
{
|
|
1803
|
+
rootDir: index.rootDir,
|
|
1804
|
+
packageCount: index.packages.length,
|
|
1805
|
+
smrtPackageCount: index.smrtPackages.length,
|
|
1806
|
+
sdkPackageCount: index.sdkPackages.length,
|
|
1807
|
+
relationshipsV2: index.relationshipsV2,
|
|
1808
|
+
freshness
|
|
1809
|
+
},
|
|
1810
|
+
null,
|
|
1811
|
+
2
|
|
1812
|
+
);
|
|
1813
|
+
break;
|
|
1814
|
+
}
|
|
1815
|
+
case "reflect-domain-knowledge": {
|
|
1816
|
+
const index = await buildKnowledgeIndex(args);
|
|
1817
|
+
const freshness = await checkKnowledgeFreshnessFromIndex(
|
|
1818
|
+
index,
|
|
1819
|
+
args
|
|
1820
|
+
);
|
|
1821
|
+
result = JSON.stringify(
|
|
1822
|
+
{
|
|
1823
|
+
rootDir: index.rootDir,
|
|
1824
|
+
packageCount: index.packages.length,
|
|
1825
|
+
smrtPackageCount: index.smrtPackages.length,
|
|
1826
|
+
sdkPackageCount: index.sdkPackages.length,
|
|
1827
|
+
domainKnowledgePackageCount: index.packages.filter(
|
|
1828
|
+
(pkg) => pkg.hasDomainKnowledge
|
|
1829
|
+
).length,
|
|
1830
|
+
missingDomainKnowledgePackages: index.packages.filter(
|
|
1831
|
+
(pkg) => pkg.exportKeys.includes("./smrt-knowledge.json") && !pkg.hasDomainKnowledge
|
|
1832
|
+
).map((pkg) => pkg.name),
|
|
1833
|
+
relationshipsV2: index.relationshipsV2,
|
|
1834
|
+
freshness
|
|
1835
|
+
},
|
|
1836
|
+
null,
|
|
1837
|
+
2
|
|
1838
|
+
);
|
|
1839
|
+
break;
|
|
1840
|
+
}
|
|
1841
|
+
case "check-knowledge-freshness":
|
|
1842
|
+
result = JSON.stringify(
|
|
1843
|
+
await checkKnowledgeFreshness(args),
|
|
1844
|
+
null,
|
|
1845
|
+
2
|
|
1846
|
+
);
|
|
1847
|
+
break;
|
|
1848
|
+
case "check-domain-knowledge":
|
|
1849
|
+
result = JSON.stringify(
|
|
1850
|
+
await checkKnowledgeFreshness(args),
|
|
1851
|
+
null,
|
|
1852
|
+
2
|
|
1853
|
+
);
|
|
1854
|
+
break;
|
|
1855
|
+
case "build-review-context":
|
|
1856
|
+
result = JSON.stringify(
|
|
1857
|
+
await buildReviewContext(args),
|
|
1858
|
+
null,
|
|
1859
|
+
2
|
|
1860
|
+
);
|
|
1861
|
+
break;
|
|
1862
|
+
case "build-domain-review-context":
|
|
1863
|
+
result = JSON.stringify(
|
|
1864
|
+
await buildReviewContext(args),
|
|
1865
|
+
null,
|
|
1866
|
+
2
|
|
1867
|
+
);
|
|
1868
|
+
break;
|
|
1869
|
+
case "smrt-review":
|
|
1870
|
+
result = JSON.stringify(await smrtReview(args), null, 2);
|
|
1871
|
+
break;
|
|
1872
|
+
case "build-architecture-context":
|
|
1873
|
+
result = JSON.stringify(
|
|
1874
|
+
await buildArchitectureContext(args),
|
|
1875
|
+
null,
|
|
1876
|
+
2
|
|
1877
|
+
);
|
|
1878
|
+
break;
|
|
1879
|
+
case "build-domain-architecture-context":
|
|
1880
|
+
result = JSON.stringify(
|
|
1881
|
+
await buildArchitectureContext(args),
|
|
1882
|
+
null,
|
|
1883
|
+
2
|
|
1884
|
+
);
|
|
1885
|
+
break;
|
|
1886
|
+
case "smrt-architecture":
|
|
1887
|
+
result = JSON.stringify(await smrtArchitecture(args), null, 2);
|
|
1888
|
+
break;
|
|
1889
|
+
case "list-agent-skills":
|
|
1890
|
+
result = JSON.stringify({ skills: listAgentSkills() }, null, 2);
|
|
1891
|
+
break;
|
|
1892
|
+
case "get-agent-skill":
|
|
1893
|
+
result = JSON.stringify(await getAgentSkill(args), null, 2);
|
|
1894
|
+
break;
|
|
1895
|
+
default:
|
|
1896
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1897
|
+
}
|
|
1898
|
+
return {
|
|
1899
|
+
content: [
|
|
1900
|
+
{
|
|
1901
|
+
type: "text",
|
|
1902
|
+
text: result
|
|
1903
|
+
}
|
|
1904
|
+
]
|
|
1905
|
+
};
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1908
|
+
console.error(`[${SERVER_NAME}] Error:`, error);
|
|
1909
|
+
return {
|
|
1910
|
+
content: [
|
|
1911
|
+
{
|
|
1912
|
+
type: "text",
|
|
1913
|
+
text: `Error executing tool ${name}: ${errorMessage}`
|
|
1914
|
+
}
|
|
1915
|
+
],
|
|
1916
|
+
isError: true
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
const transport = new StdioServerTransport();
|
|
1921
|
+
await server.connect(transport);
|
|
1922
|
+
if (DEBUG) {
|
|
1923
|
+
console.error(`[${SERVER_NAME}] Server connected via stdio`);
|
|
1924
|
+
}
|
|
1925
|
+
process.on("SIGINT", async () => {
|
|
1926
|
+
if (DEBUG) {
|
|
1927
|
+
console.error(`[${SERVER_NAME}] Shutting down...`);
|
|
1928
|
+
}
|
|
1929
|
+
await server.close();
|
|
1930
|
+
process.exit(0);
|
|
1931
|
+
});
|
|
1932
|
+
process.on("SIGTERM", async () => {
|
|
1933
|
+
if (DEBUG) {
|
|
1934
|
+
console.error(`[${SERVER_NAME}] Shutting down...`);
|
|
1935
|
+
}
|
|
1936
|
+
await server.close();
|
|
1937
|
+
process.exit(0);
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
function isEntrypoint() {
|
|
1941
|
+
const entry = process.argv[1];
|
|
1942
|
+
if (!entry) return false;
|
|
1943
|
+
try {
|
|
1944
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(entry);
|
|
1945
|
+
} catch {
|
|
1946
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
function readPackageVersion() {
|
|
1950
|
+
try {
|
|
1951
|
+
const packageRoot = dirname(fileURLToPath(import.meta.url));
|
|
1952
|
+
const packageJsonPath = join(packageRoot, "..", "package.json");
|
|
1953
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
1954
|
+
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
1955
|
+
} catch {
|
|
1956
|
+
return "0.0.0";
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
function renderAgentSkillMarkdown(name) {
|
|
1960
|
+
const skill = getAgentSkill({ name, includeReferences: true });
|
|
1961
|
+
const references = skill.referenceFiles.map(
|
|
1962
|
+
(file) => `## Reference: ${file.path}
|
|
1963
|
+
|
|
1964
|
+
${file.content.trim()}`
|
|
1965
|
+
);
|
|
1966
|
+
return [skill.skillMarkdown.trim(), ...references].join("\n\n");
|
|
1967
|
+
}
|
|
1968
|
+
function reviewPromptArguments(args) {
|
|
1969
|
+
return compactRecord({
|
|
1970
|
+
rootDir: args?.rootDir,
|
|
1971
|
+
changedFiles: parseStringList(args?.changedFiles),
|
|
1972
|
+
focus: args?.focus,
|
|
1973
|
+
documentation: args?.documentation,
|
|
1974
|
+
scope: args?.scope,
|
|
1975
|
+
package: args?.package
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
function architecturePromptArguments(args) {
|
|
1979
|
+
return compactRecord({
|
|
1980
|
+
rootDir: args?.rootDir,
|
|
1981
|
+
idea: args?.idea,
|
|
1982
|
+
documentation: args?.documentation,
|
|
1983
|
+
focus: args?.focus,
|
|
1984
|
+
scope: args?.scope,
|
|
1985
|
+
package: args?.package
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
function parseStringList(value) {
|
|
1989
|
+
if (!value?.trim()) return void 0;
|
|
1990
|
+
try {
|
|
1991
|
+
const parsed = JSON.parse(value);
|
|
1992
|
+
if (Array.isArray(parsed)) {
|
|
1993
|
+
return parsed.filter((item) => typeof item === "string");
|
|
1994
|
+
}
|
|
1995
|
+
} catch {
|
|
1996
|
+
}
|
|
1997
|
+
return value.split(/[\n,]/).map((item) => item.trim()).filter(Boolean);
|
|
1998
|
+
}
|
|
1999
|
+
function compactRecord(value) {
|
|
2000
|
+
return Object.fromEntries(
|
|
2001
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0)
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
function sanitizeKnowledgeIndex(index) {
|
|
2005
|
+
const packages = index.packages.map((pkg) => sanitizeKnowledgePackage(pkg));
|
|
2006
|
+
return {
|
|
2007
|
+
...index,
|
|
2008
|
+
rootDir: ".",
|
|
2009
|
+
packages,
|
|
2010
|
+
smrtPackages: packages.filter((pkg) => pkg.kind === "smrt"),
|
|
2011
|
+
sdkPackages: packages.filter((pkg) => pkg.kind === "sdk")
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
function sanitizeKnowledgePackage(pkg) {
|
|
2015
|
+
const { directory: _directory, objects, ...rest } = pkg;
|
|
2016
|
+
return {
|
|
2017
|
+
...rest,
|
|
2018
|
+
objects: objects.map((object) => ({
|
|
2019
|
+
...object,
|
|
2020
|
+
filePath: sanitizePath(object.filePath)
|
|
2021
|
+
}))
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
function sanitizePath(path) {
|
|
2025
|
+
if (!path) return path;
|
|
2026
|
+
if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path)) {
|
|
2027
|
+
return "<absolute-path>";
|
|
2028
|
+
}
|
|
2029
|
+
return path;
|
|
2030
|
+
}
|
|
2031
|
+
if (isEntrypoint()) {
|
|
2032
|
+
main().catch((error) => {
|
|
2033
|
+
console.error(`[${SERVER_NAME}] Fatal error:`, error);
|
|
2034
|
+
process.exit(1);
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
export { SERVER_VERSION, TOOLS };
|
|
2039
|
+
//# sourceMappingURL=index.js.map
|