@polymorphism-tech/morph-spec 4.17.0 → 4.19.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/CLAUDE.md +36 -0
- package/README.md +2 -2
- package/bin/task-manager.js +12 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents/architecture/architecture-expert.md +86 -0
- package/framework/agents/architecture/morph-spec-architect.md +66 -0
- package/framework/agents/architecture/packages-validator.md +77 -0
- package/framework/agents/architecture/security-expert.md +67 -0
- package/framework/agents/backend/ms-agent-expert.md +1 -1
- package/framework/agents/backend/neon-expert.md +9 -27
- package/framework/agents/frontend/design-system-validator.md +88 -0
- package/framework/agents/frontend/nextjs-component-validator.md +85 -0
- package/framework/agents/frontend/{ui-ux-designer.md → ui-designer.md} +31 -0
- package/framework/agents/integrations/neon-auth.md +151 -0
- package/framework/agents.json +22 -14
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +39 -0
- package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +78 -44
- package/framework/hooks/claude-code/post-tool-use/loop-detect.js +103 -0
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +21 -2
- package/framework/hooks/claude-code/stop/validate-completion.js +48 -1
- package/framework/hooks/shared/activity-logger.js +56 -1
- package/framework/hooks/shared/build-check.js +79 -0
- package/framework/hooks/shared/payload-utils.js +39 -0
- package/framework/hooks/shared/session-snapshot.js +105 -0
- package/framework/phases.json +1 -1
- package/framework/rules/neon-auth-standards.md +13 -0
- package/framework/rules/neon-standards.md +1 -0
- package/framework/skills/level-0-meta/mcp-registry.json +34 -1
- package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +3 -3
- package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
- package/framework/skills/level-0-meta/morph-init/SKILL.md +4 -4
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +11 -1
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +12 -1
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +119 -11
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +13 -9
- package/framework/standards/ai-agents/production.md +5 -2
- package/framework/standards/ai-agents/setup.md +13 -14
- package/framework/standards/ai-agents/workflows.md +8 -6
- package/framework/standards/backend/database/postgresql/database.md +5 -5
- package/framework/standards/backend/database/vector-search-rag.md +1 -1
- package/framework/standards/backend/integrations/asaas/asaas-api.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +364 -0
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +9 -9
- package/framework/templates/README.md +2 -2
- package/framework/templates/REGISTRY.json +5 -5
- package/framework/templates/code/dotnet/jobs/agent.cs +0 -1
- package/framework/templates/code/sql/postgresql-migration.sql +10 -10
- package/framework/templates/code/sql/postgresql-migration.template.sql +12 -14
- package/framework/templates/code/sql/postgresql-rls-policy.sql +6 -9
- package/framework/templates/feature/decisions.md +8 -7
- package/framework/templates/integrations/neon-auth-config.cs +133 -0
- package/framework/workflows/docs/full-morph.md +1 -1
- package/package.json +1 -1
- package/src/commands/mcp/mcp-setup.js +21 -0
- package/src/commands/project/init.js +11 -0
- package/src/lib/contracts/sprint-contract-generator.js +137 -0
- package/src/lib/detectors/index.js +2 -2
- package/src/lib/generators/architecture-scanner.js +2 -3
- package/src/lib/installers/mcp-installer.js +25 -0
- package/src/utils/hooks-installer.js +12 -1
- package/framework/agents/integrations/clerk-auth.md +0 -109
- package/framework/rules/clerk-standards.md +0 -13
- package/framework/standards/backend/integrations/clerk/clerk-auth.md +0 -292
- package/framework/templates/integrations/clerk-config.cs +0 -217
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// MORPH-SPEC - Neon Auth Configuration Template
|
|
3
|
+
// Configuracao de autenticacao com Neon Auth (Better Auth) para .NET
|
|
4
|
+
// ==============================================================================
|
|
5
|
+
|
|
6
|
+
using Microsoft.AspNetCore.Authentication;
|
|
7
|
+
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
8
|
+
using Microsoft.IdentityModel.Tokens;
|
|
9
|
+
using System.Security.Claims;
|
|
10
|
+
|
|
11
|
+
namespace {{Namespace}}.Infrastructure.Auth;
|
|
12
|
+
|
|
13
|
+
// ==============================================================================
|
|
14
|
+
// OPTIONS
|
|
15
|
+
// ==============================================================================
|
|
16
|
+
|
|
17
|
+
public class NeonAuthOptions
|
|
18
|
+
{
|
|
19
|
+
public const string SectionName = "NeonAuth";
|
|
20
|
+
|
|
21
|
+
/// <summary>
|
|
22
|
+
/// Neon Auth Base URL (e.g., https://ep-xxx.neonauth.us-east-2.aws.neon.build/neondb/auth)
|
|
23
|
+
/// </summary>
|
|
24
|
+
public string BaseUrl { get; set; } = string.Empty;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ==============================================================================
|
|
28
|
+
// SERVICE EXTENSIONS
|
|
29
|
+
// ==============================================================================
|
|
30
|
+
|
|
31
|
+
public static class NeonAuthServiceExtensions
|
|
32
|
+
{
|
|
33
|
+
/// <summary>
|
|
34
|
+
/// Adds Neon Auth JWT Bearer authentication to the project.
|
|
35
|
+
/// Validates JWTs issued by Neon Auth service.
|
|
36
|
+
/// </summary>
|
|
37
|
+
public static IServiceCollection AddNeonAuthentication(
|
|
38
|
+
this IServiceCollection services,
|
|
39
|
+
IConfiguration configuration)
|
|
40
|
+
{
|
|
41
|
+
services.Configure<NeonAuthOptions>(configuration.GetSection(NeonAuthOptions.SectionName));
|
|
42
|
+
|
|
43
|
+
var neonAuthOptions = configuration.GetSection(NeonAuthOptions.SectionName).Get<NeonAuthOptions>()!;
|
|
44
|
+
|
|
45
|
+
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
46
|
+
.AddJwtBearer(options =>
|
|
47
|
+
{
|
|
48
|
+
options.Authority = neonAuthOptions.BaseUrl;
|
|
49
|
+
options.TokenValidationParameters = new TokenValidationParameters
|
|
50
|
+
{
|
|
51
|
+
ValidateIssuer = true,
|
|
52
|
+
ValidateAudience = false,
|
|
53
|
+
ValidateLifetime = true,
|
|
54
|
+
ValidateIssuerSigningKey = true,
|
|
55
|
+
NameClaimType = "sub"
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
services.AddAuthorization();
|
|
60
|
+
|
|
61
|
+
return services;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ==============================================================================
|
|
66
|
+
// USER CONTEXT SERVICE
|
|
67
|
+
// ==============================================================================
|
|
68
|
+
|
|
69
|
+
public interface ICurrentUserService
|
|
70
|
+
{
|
|
71
|
+
string? UserId { get; }
|
|
72
|
+
string? Email { get; }
|
|
73
|
+
bool IsAuthenticated { get; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public class CurrentUserService(IHttpContextAccessor httpContextAccessor) : ICurrentUserService
|
|
77
|
+
{
|
|
78
|
+
public string? UserId =>
|
|
79
|
+
httpContextAccessor.HttpContext?.User.FindFirstValue("sub");
|
|
80
|
+
|
|
81
|
+
public string? Email =>
|
|
82
|
+
httpContextAccessor.HttpContext?.User.FindFirstValue("email");
|
|
83
|
+
|
|
84
|
+
public bool IsAuthenticated =>
|
|
85
|
+
httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ==============================================================================
|
|
89
|
+
// MIDDLEWARE PIPELINE
|
|
90
|
+
// ==============================================================================
|
|
91
|
+
|
|
92
|
+
public static class NeonAuthMiddlewareExtensions
|
|
93
|
+
{
|
|
94
|
+
public static IApplicationBuilder UseNeonAuthentication(this IApplicationBuilder app)
|
|
95
|
+
{
|
|
96
|
+
app.UseAuthentication();
|
|
97
|
+
app.UseAuthorization();
|
|
98
|
+
return app;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ==============================================================================
|
|
103
|
+
// APPSETTINGS EXAMPLE
|
|
104
|
+
// ==============================================================================
|
|
105
|
+
|
|
106
|
+
/*
|
|
107
|
+
{
|
|
108
|
+
"NeonAuth": {
|
|
109
|
+
"BaseUrl": "https://ep-xxx.neonauth.us-east-2.aws.neon.build/neondb/auth"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
// ==============================================================================
|
|
115
|
+
// PROGRAM.CS EXAMPLE
|
|
116
|
+
// ==============================================================================
|
|
117
|
+
|
|
118
|
+
/*
|
|
119
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
120
|
+
|
|
121
|
+
// Add Neon Auth authentication
|
|
122
|
+
builder.Services.AddNeonAuthentication(builder.Configuration);
|
|
123
|
+
builder.Services.AddHttpContextAccessor();
|
|
124
|
+
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
|
|
125
|
+
|
|
126
|
+
var app = builder.Build();
|
|
127
|
+
|
|
128
|
+
app.UseNeonAuthentication();
|
|
129
|
+
|
|
130
|
+
app.MapControllers().RequireAuthorization();
|
|
131
|
+
|
|
132
|
+
app.Run();
|
|
133
|
+
*/
|
|
@@ -63,7 +63,7 @@ proposal.md, design-system.md, mockups.md, components.md, flows.md, spec.md, con
|
|
|
63
63
|
|
|
64
64
|
## Exemplos
|
|
65
65
|
- "Sistema de relatórios com jobs agendados Hangfire"
|
|
66
|
-
- "Autenticação completa com
|
|
66
|
+
- "Autenticação completa com Neon Auth (login, signup, OAuth)"
|
|
67
67
|
- "Dashboard com gráficos, tabelas e filtros complexos"
|
|
68
68
|
- "RAG pipeline com AI Search e embeddings"
|
|
69
69
|
- "Infraestrutura completa com Docker/Easypanel"
|
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ import ora from 'ora';
|
|
|
16
16
|
import {
|
|
17
17
|
orchestrateMcpSetup,
|
|
18
18
|
installAutoMcps,
|
|
19
|
+
installCompanionSkills,
|
|
19
20
|
installMcpWithCredentials,
|
|
20
21
|
generateSetupInstructions,
|
|
21
22
|
formatMcpStatusTable,
|
|
@@ -120,6 +121,16 @@ async function autoInstall(targetPath, stack, existingMcps) {
|
|
|
120
121
|
console.log(chalk.gray(` Skipped (already configured): ${result.skipped.join(', ')}`));
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
// Install companion skills for MCPs that define them
|
|
125
|
+
const skillResults = installCompanionSkills(orchestration.autoInstallable);
|
|
126
|
+
for (const r of skillResults) {
|
|
127
|
+
if (r.success) {
|
|
128
|
+
console.log(chalk.green(` ✓ Companion skills installed: ${r.repo}`));
|
|
129
|
+
} else {
|
|
130
|
+
console.log(chalk.yellow(` ⚠ Failed to install companion skills: ${r.repo}`));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
123
134
|
console.log('');
|
|
124
135
|
}
|
|
125
136
|
|
|
@@ -305,6 +316,16 @@ async function interactiveWizard(targetPath, stack, existingMcps) {
|
|
|
305
316
|
const spinner = ora('Installing...').start();
|
|
306
317
|
const result = await installAutoMcps(targetPath, orchestration.autoInstallable);
|
|
307
318
|
spinner.succeed(`Installed: ${result.added.join(', ')}`);
|
|
319
|
+
|
|
320
|
+
// Install companion skills for MCPs that define them
|
|
321
|
+
const skillResults = installCompanionSkills(orchestration.autoInstallable);
|
|
322
|
+
for (const r of skillResults) {
|
|
323
|
+
if (r.success) {
|
|
324
|
+
console.log(chalk.green(` ✓ Companion skills: ${r.repo}`));
|
|
325
|
+
} else {
|
|
326
|
+
console.log(chalk.yellow(` ⚠ Companion skills failed: ${r.repo}`));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
308
329
|
}
|
|
309
330
|
}
|
|
310
331
|
|
|
@@ -20,6 +20,7 @@ import { generateSettings, saveIntegrations } from '../../lib/generators/setting
|
|
|
20
20
|
import {
|
|
21
21
|
orchestrateMcpSetup,
|
|
22
22
|
installAutoMcps,
|
|
23
|
+
installCompanionSkills,
|
|
23
24
|
installMcpWithCredentials,
|
|
24
25
|
generateSetupInstructions,
|
|
25
26
|
formatMcpStatusTable,
|
|
@@ -256,6 +257,16 @@ export async function initCommand(options) {
|
|
|
256
257
|
for (const name of result.added) {
|
|
257
258
|
autoInstalled[name] = orchestration.autoInstallable[name];
|
|
258
259
|
}
|
|
260
|
+
|
|
261
|
+
// Install companion skills for MCPs that define them (e.g., Stitch agent skills)
|
|
262
|
+
const skillResults = installCompanionSkills(orchestration.autoInstallable);
|
|
263
|
+
for (const r of skillResults) {
|
|
264
|
+
if (r.success) {
|
|
265
|
+
logger.info(`✓ Companion skills installed: ${r.repo}`);
|
|
266
|
+
} else {
|
|
267
|
+
logger.warn(`Failed to install companion skills: ${r.repo}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
259
270
|
}
|
|
260
271
|
}
|
|
261
272
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprint Contract Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates sprint contracts from tasks.md + spec.md for the evaluator.
|
|
5
|
+
* Contracts define objective "definition of done" criteria.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { getBuildCommand } from '../../../framework/hooks/shared/build-check.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse a task entry from tasks.md markdown.
|
|
14
|
+
*
|
|
15
|
+
* Expected format: ## T001: Task Title [FR001, FR003]
|
|
16
|
+
*
|
|
17
|
+
* @param {string} content - tasks.md content
|
|
18
|
+
* @param {string} taskId - Task ID (e.g., 'T001')
|
|
19
|
+
* @returns {{ id: string, title: string, frs: string[], description: string }|null}
|
|
20
|
+
*/
|
|
21
|
+
export function parseTaskFromMarkdown(content, taskId) {
|
|
22
|
+
const pattern = new RegExp(
|
|
23
|
+
`##\\s+${taskId}:\\s+(.+?)(?:\\s+\\[([^\\]]+)\\])?\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`,
|
|
24
|
+
'i'
|
|
25
|
+
);
|
|
26
|
+
const match = content.match(pattern);
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
|
|
29
|
+
const title = match[1].trim();
|
|
30
|
+
const frString = match[2] || '';
|
|
31
|
+
const description = match[3].trim();
|
|
32
|
+
|
|
33
|
+
const frs = frString
|
|
34
|
+
? frString.split(',').map(fr => fr.trim()).filter(Boolean)
|
|
35
|
+
: [];
|
|
36
|
+
|
|
37
|
+
return { id: taskId, title, frs, description };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate a sprint contract for a task.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} projectPath - Project root
|
|
44
|
+
* @param {string} featureName - Feature name
|
|
45
|
+
* @param {string} taskId - Task ID
|
|
46
|
+
* @returns {Object|null} Contract object or null if unable to generate
|
|
47
|
+
*/
|
|
48
|
+
export function generateSprintContract(projectPath, featureName, taskId) {
|
|
49
|
+
const featureDir = join(projectPath, '.morph/features', featureName);
|
|
50
|
+
const tasksPath = join(featureDir, '4-tasks/tasks.md');
|
|
51
|
+
|
|
52
|
+
if (!existsSync(tasksPath)) return null;
|
|
53
|
+
|
|
54
|
+
const tasksContent = readFileSync(tasksPath, 'utf-8');
|
|
55
|
+
const task = parseTaskFromMarkdown(tasksContent, taskId);
|
|
56
|
+
if (!task) return null;
|
|
57
|
+
|
|
58
|
+
// Read spec for FR-based criteria
|
|
59
|
+
const specPath = join(featureDir, '2-design/spec.md');
|
|
60
|
+
let definitionOfDone = [];
|
|
61
|
+
if (task.frs.length > 0 && existsSync(specPath)) {
|
|
62
|
+
const specContent = readFileSync(specPath, 'utf-8');
|
|
63
|
+
for (const fr of task.frs) {
|
|
64
|
+
const frPattern = new RegExp(`##\\s+${fr}[:\\s]([\\s\\S]*?)(?=\\n##\\s|$)`, 'i');
|
|
65
|
+
const frMatch = specContent.match(frPattern);
|
|
66
|
+
if (frMatch) {
|
|
67
|
+
const bullets = frMatch[1].match(/^[\s]*[-*]\s+(.+)/gm);
|
|
68
|
+
if (bullets) {
|
|
69
|
+
definitionOfDone.push(...bullets.map(b => b.replace(/^[\s]*[-*]\s+/, '').trim()));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (definitionOfDone.length === 0) {
|
|
76
|
+
definitionOfDone = [task.description || `Complete ${task.title}`];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Read stack for build commands
|
|
80
|
+
let stack = '';
|
|
81
|
+
try {
|
|
82
|
+
const configPath = join(projectPath, '.morph/config/config.json');
|
|
83
|
+
if (existsSync(configPath)) {
|
|
84
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
85
|
+
stack = config?.project?.stack || '';
|
|
86
|
+
}
|
|
87
|
+
} catch { /* ignore */ }
|
|
88
|
+
|
|
89
|
+
const hasTsConfig = existsSync(join(projectPath, 'tsconfig.json'));
|
|
90
|
+
const buildCmd = getBuildCommand(stack, hasTsConfig);
|
|
91
|
+
|
|
92
|
+
const contract = {
|
|
93
|
+
task: taskId,
|
|
94
|
+
title: task.title,
|
|
95
|
+
functionalRequirements: task.frs,
|
|
96
|
+
definitionOfDone,
|
|
97
|
+
verification: {
|
|
98
|
+
build: buildCmd || 'echo "No build command configured"',
|
|
99
|
+
test: stack.includes('dotnet') ? `dotnet test --filter ${task.title.replace(/\s+/g, '')}` : 'npm test',
|
|
100
|
+
e2e: null
|
|
101
|
+
},
|
|
102
|
+
maxAttempts: 3,
|
|
103
|
+
generatedAt: new Date().toISOString()
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Write contract to disk
|
|
107
|
+
const contractsDir = join(featureDir, '5-implement/contracts');
|
|
108
|
+
mkdirSync(contractsDir, { recursive: true });
|
|
109
|
+
const yamlContent = contractToYaml(contract);
|
|
110
|
+
writeFileSync(join(contractsDir, `${taskId}-contract.yaml`), yamlContent, 'utf-8');
|
|
111
|
+
|
|
112
|
+
return contract;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Simple YAML serializer for contracts (no dependency needed).
|
|
117
|
+
*
|
|
118
|
+
* @param {Object} contract - Contract object
|
|
119
|
+
* @returns {string} YAML string
|
|
120
|
+
*/
|
|
121
|
+
function contractToYaml(contract) {
|
|
122
|
+
const lines = [
|
|
123
|
+
`task: ${contract.task}`,
|
|
124
|
+
`title: "${contract.title}"`,
|
|
125
|
+
`functionalRequirements:`,
|
|
126
|
+
...contract.functionalRequirements.map(fr => ` - ${fr}`),
|
|
127
|
+
`definitionOfDone:`,
|
|
128
|
+
...contract.definitionOfDone.map(d => ` - "${d}"`),
|
|
129
|
+
`verification:`,
|
|
130
|
+
` build: "${contract.verification.build}"`,
|
|
131
|
+
` test: "${contract.verification.test}"`,
|
|
132
|
+
` e2e: ${contract.verification.e2e ? `"${contract.verification.e2e}"` : 'null'}`,
|
|
133
|
+
`maxAttempts: ${contract.maxAttempts}`,
|
|
134
|
+
`generatedAt: "${contract.generatedAt}"`,
|
|
135
|
+
];
|
|
136
|
+
return lines.join('\n') + '\n';
|
|
137
|
+
}
|
|
@@ -74,8 +74,8 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
74
74
|
if (depNames.includes('@neondatabase/serverless') || depNames.includes('postgres') || depNames.includes('pg')) {
|
|
75
75
|
structure.integrations.push('neon');
|
|
76
76
|
}
|
|
77
|
-
if (depNames.includes('@
|
|
78
|
-
structure.integrations.push('
|
|
77
|
+
if (depNames.includes('@neondatabase/auth')) {
|
|
78
|
+
structure.integrations.push('neon-auth');
|
|
79
79
|
}
|
|
80
80
|
if (depNames.includes('stripe')) {
|
|
81
81
|
structure.integrations.push('stripe');
|
|
@@ -223,7 +223,7 @@ export async function scanIntegrations(projectPath) {
|
|
|
223
223
|
);
|
|
224
224
|
|
|
225
225
|
const knownServices = {
|
|
226
|
-
'
|
|
226
|
+
'NeonAuth': 'auth',
|
|
227
227
|
'AsaaS': 'payment',
|
|
228
228
|
'Resend': 'email',
|
|
229
229
|
'Hangfire': 'background-jobs',
|
|
@@ -266,8 +266,7 @@ export async function scanIntegrations(projectPath) {
|
|
|
266
266
|
|
|
267
267
|
// Scan .env files for known prefixes
|
|
268
268
|
const envPrefixes = {
|
|
269
|
-
'
|
|
270
|
-
'CLERK_': 'Clerk',
|
|
269
|
+
'NEON_AUTH_': 'NeonAuth',
|
|
271
270
|
'DATABASE_URL': 'Neon',
|
|
272
271
|
'NEON_': 'Neon',
|
|
273
272
|
'RESEND_': 'Resend',
|
|
@@ -148,6 +148,31 @@ export async function installAutoMcps(targetPath, mcpsToInstall) {
|
|
|
148
148
|
return installMcpServers(targetPath, servers);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Install companion skills for MCPs that define them (e.g., Stitch agent skills).
|
|
153
|
+
* Reads the `companionSkills` field from registry entries and runs the install command.
|
|
154
|
+
* @param {Object} mcpsInstalled - Map of mcpName → mcpEntry that were just installed
|
|
155
|
+
* @param {Function} exec - execSync function (for testing)
|
|
156
|
+
* @returns {Array<{name: string, repo: string, success: boolean}>}
|
|
157
|
+
*/
|
|
158
|
+
export function installCompanionSkills(mcpsInstalled, exec = execSync) {
|
|
159
|
+
const results = [];
|
|
160
|
+
|
|
161
|
+
for (const [name, entry] of Object.entries(mcpsInstalled)) {
|
|
162
|
+
const companion = entry.companionSkills;
|
|
163
|
+
if (!companion?.installCommand) continue;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
exec(companion.installCommand, { stdio: 'inherit', timeout: 30000 });
|
|
167
|
+
results.push({ name, repo: companion.repo, success: true });
|
|
168
|
+
} catch {
|
|
169
|
+
results.push({ name, repo: companion.repo, success: false });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
|
|
151
176
|
/**
|
|
152
177
|
* Install a single MCP with provided credentials
|
|
153
178
|
* @param {string} targetPath - Project root directory
|
|
@@ -15,7 +15,7 @@ import { homedir } from 'os';
|
|
|
15
15
|
import { execSync } from 'child_process';
|
|
16
16
|
|
|
17
17
|
/** Current hooks schema version — bump when hook definitions change */
|
|
18
|
-
const HOOKS_VERSION = '
|
|
18
|
+
const HOOKS_VERSION = '3.0.0';
|
|
19
19
|
|
|
20
20
|
/** Marker for old dispatch.js (v1) */
|
|
21
21
|
const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
|
|
@@ -155,6 +155,17 @@ Otherwise respond: {"ok": true}`
|
|
|
155
155
|
]
|
|
156
156
|
},
|
|
157
157
|
|
|
158
|
+
// === PostToolUse: All — loop detection ===
|
|
159
|
+
{
|
|
160
|
+
event: 'PostToolUse',
|
|
161
|
+
matcher: null,
|
|
162
|
+
hooks: [{
|
|
163
|
+
type: 'command',
|
|
164
|
+
command: 'node framework/hooks/claude-code/post-tool-use/loop-detect.js',
|
|
165
|
+
timeout: 5000
|
|
166
|
+
}]
|
|
167
|
+
},
|
|
168
|
+
|
|
158
169
|
// === Stop ===
|
|
159
170
|
{
|
|
160
171
|
event: 'Stop',
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: clerk-auth
|
|
3
|
-
description: Clerk Auth specialist for Clerk SDK integration, social login, JWT session management, and user management in Next.js and .NET projects. Use when integrating Clerk authentication, implementing social login flows, managing user sessions with Clerk JWT, or protecting routes with Clerk middleware.
|
|
4
|
-
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
-
stacks:
|
|
6
|
-
- clerk
|
|
7
|
-
color: teal
|
|
8
|
-
effort: medium
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Clerk Auth
|
|
12
|
-
|
|
13
|
-
Autenticação SaaS com Clerk para .NET e Next.js. SDK: `Clerk.Net.AspNetCore.Security` (.NET) / `@clerk/nextjs` (Next.js).
|
|
14
|
-
|
|
15
|
-
## Setup
|
|
16
|
-
|
|
17
|
-
```csharp
|
|
18
|
-
// appsettings.json
|
|
19
|
-
{ "Clerk": { "SecretKey": "${CLERK_SECRET_KEY}", "PublishableKey": "pk_test_xxx" } }
|
|
20
|
-
|
|
21
|
-
// Program.cs
|
|
22
|
-
builder.Services.AddClerk(builder.Configuration);
|
|
23
|
-
builder.Services.AddAuthentication(ClerkAuthenticationDefaults.AuthenticationScheme)
|
|
24
|
-
.AddClerk(o => { o.Authority = "https://clerk.{instance}.com"; o.ValidAudiences = ["your-app-id"]; });
|
|
25
|
-
builder.Services.AddAuthorization();
|
|
26
|
-
app.UseAuthentication();
|
|
27
|
-
app.UseAuthorization();
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Protected Endpoints
|
|
31
|
-
|
|
32
|
-
```csharp
|
|
33
|
-
// Minimal API
|
|
34
|
-
app.MapGet("/api/profile", async (ClaimsPrincipal user, IClerkClient clerk) =>
|
|
35
|
-
{
|
|
36
|
-
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
37
|
-
if (userId is null) return Results.Unauthorized();
|
|
38
|
-
var u = await clerk.Users.GetUserAsync(userId);
|
|
39
|
-
return Results.Ok(new { u.Id, Email = u.EmailAddresses.FirstOrDefault()?.EmailAddress });
|
|
40
|
-
}).RequireAuthorization();
|
|
41
|
-
|
|
42
|
-
// Controller: same pattern with [Authorize] + User.FindFirstValue()
|
|
43
|
-
// Role-based: [Authorize(Roles = "admin")]
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Next.js Integration
|
|
47
|
-
|
|
48
|
-
```tsx
|
|
49
|
-
// middleware.ts
|
|
50
|
-
import { clerkMiddleware } from '@clerk/nextjs/server';
|
|
51
|
-
export default clerkMiddleware();
|
|
52
|
-
export const config = { matcher: ['/((?!.*\\..*|_next).*)', '/'] };
|
|
53
|
-
|
|
54
|
-
// Protected page
|
|
55
|
-
import { auth } from '@clerk/nextjs/server';
|
|
56
|
-
export default async function DashboardPage() {
|
|
57
|
-
const { userId } = await auth();
|
|
58
|
-
if (!userId) redirect('/sign-in');
|
|
59
|
-
return <Dashboard />;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Client component
|
|
63
|
-
'use client';
|
|
64
|
-
import { useUser } from '@clerk/nextjs';
|
|
65
|
-
const { user, isLoaded } = useUser();
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Client Operations (.NET)
|
|
69
|
-
|
|
70
|
-
```csharp
|
|
71
|
-
var user = await _clerk.Users.GetUserAsync(userId); // Get
|
|
72
|
-
var users = await _clerk.Users.GetUserListAsync(new() { Limit = 10 }); // List
|
|
73
|
-
await _clerk.Users.UpdateUserMetadataAsync(userId, new() // Update metadata
|
|
74
|
-
{ PublicMetadata = new Dictionary<string, object> { ["plan"] = "pro" } });
|
|
75
|
-
await _clerk.Users.DeleteUserAsync(userId); // Delete
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Webhooks
|
|
79
|
-
|
|
80
|
-
```csharp
|
|
81
|
-
[ApiController, Route("api/webhooks/clerk")]
|
|
82
|
-
public class ClerkWebhookController(IUserService users) : ControllerBase
|
|
83
|
-
{
|
|
84
|
-
[HttpPost]
|
|
85
|
-
public async Task<IActionResult> Handle([FromBody] ClerkWebhookPayload payload)
|
|
86
|
-
{
|
|
87
|
-
switch (payload.Type)
|
|
88
|
-
{
|
|
89
|
-
case "user.created": await users.SyncUserAsync(payload.Data); break;
|
|
90
|
-
case "user.updated": await users.UpdateUserAsync(payload.Data); break;
|
|
91
|
-
case "user.deleted": await users.DeleteUserAsync(payload.Data.Id); break;
|
|
92
|
-
}
|
|
93
|
-
return Ok();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Checklist
|
|
99
|
-
|
|
100
|
-
- [ ] Secret Key in environment variables (not hardcoded)
|
|
101
|
-
- [ ] Authentication scheme configured
|
|
102
|
-
- [ ] Authorization policies defined
|
|
103
|
-
- [ ] Webhook endpoint + signature validation
|
|
104
|
-
- [ ] Redirect after login configured
|
|
105
|
-
- [ ] Error handling for expired sessions
|
|
106
|
-
|
|
107
|
-
---
|
|
108
|
-
|
|
109
|
-
*MORPH-SPEC by Polymorphism Tech*
|