@elevasis/sdk 1.21.0 → 1.22.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/dist/cli.cjs +951 -171
- package/dist/index.d.ts +632 -341
- package/dist/index.js +3102 -142
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +19 -1
- package/dist/test-utils/index.d.ts +313 -4
- package/dist/test-utils/index.js +3246 -281
- package/dist/worker/index.js +3041 -80
- package/package.json +3 -3
- package/reference/claude-config/hooks/post-edit-validate.mjs +98 -98
- package/reference/claude-config/hooks/scaffold-registry-reminder.mjs +188 -188
- package/reference/claude-config/hooks/tool-failure-recovery.mjs +73 -73
- package/reference/claude-config/registries/graph-skills.json +4 -4
- package/reference/claude-config/registries/knowledge-flags.json +0 -2
- package/reference/claude-config/rules/active-change-index.md +80 -80
- package/reference/claude-config/rules/agent-start-here.md +277 -277
- package/reference/claude-config/rules/deployment.md +57 -57
- package/reference/claude-config/rules/error-handling.md +56 -56
- package/reference/claude-config/rules/execution.md +40 -40
- package/reference/claude-config/rules/frontend.md +4 -4
- package/reference/claude-config/rules/observability.md +31 -31
- package/reference/claude-config/rules/operations.md +29 -17
- package/reference/claude-config/rules/organization-model.md +110 -84
- package/reference/claude-config/rules/organization-os.md +115 -113
- package/reference/claude-config/rules/package-taxonomy.md +33 -33
- package/reference/claude-config/rules/platform.md +42 -42
- package/reference/claude-config/rules/shared-types.md +49 -46
- package/reference/claude-config/rules/task-tracking.md +47 -47
- package/reference/claude-config/rules/ui.md +200 -200
- package/reference/claude-config/rules/vibe.md +235 -235
- package/reference/claude-config/scripts/statusline-command.js +18 -18
- package/reference/claude-config/settings.json +34 -34
- package/reference/claude-config/skills/deploy/{SKILL.md → skill.md} +156 -156
- package/reference/claude-config/skills/dsp/SKILL.md +66 -66
- package/reference/claude-config/skills/elevasis/SKILL.md +235 -235
- package/reference/claude-config/skills/explore/SKILL.md +6 -6
- package/reference/claude-config/skills/git-sync/SKILL.md +126 -126
- package/reference/claude-config/skills/knowledge/SKILL.md +314 -299
- package/reference/claude-config/skills/knowledge/operations/codify-level-a.md +100 -100
- package/reference/claude-config/skills/knowledge/operations/codify-level-b.md +159 -159
- package/reference/claude-config/skills/knowledge/operations/customers.md +109 -109
- package/reference/claude-config/skills/knowledge/operations/features.md +76 -76
- package/reference/claude-config/skills/knowledge/operations/goals.md +118 -118
- package/reference/claude-config/skills/knowledge/operations/identity.md +93 -93
- package/reference/claude-config/skills/knowledge/operations/labels.md +94 -94
- package/reference/claude-config/skills/knowledge/operations/offerings.md +109 -109
- package/reference/claude-config/skills/knowledge/operations/roles.md +99 -99
- package/reference/claude-config/skills/knowledge/operations/techStack.md +30 -30
- package/reference/claude-config/skills/project/SKILL.md +1088 -1088
- package/reference/claude-config/skills/run-ui/SKILL.md +73 -73
- package/reference/claude-config/skills/save/SKILL.md +3 -3
- package/reference/claude-config/skills/setup/SKILL.md +275 -275
- package/reference/claude-config/skills/status/SKILL.md +59 -59
- package/reference/claude-config/skills/submit-request/SKILL.md +180 -180
- package/reference/claude-config/skills/sync/SKILL.md +47 -47
- package/reference/claude-config/skills/tutorial/SKILL.md +259 -259
- package/reference/claude-config/skills/tutorial/progress-template.md +74 -74
- package/reference/claude-config/skills/tutorial/technical.md +1303 -1303
- package/reference/claude-config/skills/tutorial/vibe-coder.md +890 -890
- package/reference/claude-config/sync-notes/2026-04-22-git-sync-and-sync-notes.md +27 -27
- package/reference/claude-config/sync-notes/2026-04-22-lead-gen-deliverability-removal.md +30 -30
- package/reference/claude-config/sync-notes/2026-04-24-test-utils-and-template-tests.md +73 -73
- package/reference/claude-config/sync-notes/2026-04-24-ui-consolidation-and-sdk-cli-train.md +86 -86
- package/reference/claude-config/sync-notes/2026-04-25-auth-role-system-and-settings-roles.md +55 -55
- package/reference/claude-config/sync-notes/2026-04-27-crm-hitl-action-layer-cutover.md +97 -97
- package/reference/claude-config/sync-notes/2026-04-27-lead-gen-substrate-train.md +112 -112
- package/reference/claude-config/sync-notes/2026-04-29-crm-state-and-lead-gen-processing-status.md +93 -93
- package/reference/claude-config/sync-notes/2026-05-02-crm-ownership-next-action.md +58 -58
- package/reference/claude-config/sync-notes/2026-05-02-template-hardcode-workos-config.md +56 -56
- package/reference/claude-config/sync-notes/2026-05-04-elevasis-workspace.md +71 -71
- package/reference/claude-config/sync-notes/2026-05-04-knowledge-bundle.md +83 -83
- package/reference/claude-config/sync-notes/2026-05-04-template-skills-run-ui-and-tutorial.md +59 -59
- package/reference/claude-config/sync-notes/2026-05-05-list-builder.md +42 -42
- package/reference/claude-config/sync-notes/2026-05-06-crm-spine.md +60 -60
- package/reference/claude-config/sync-notes/2026-05-06-sdk-changes-release-train.md +37 -37
- package/reference/claude-config/sync-notes/2026-05-07-sdk-changes-release-train.md +34 -34
- package/reference/claude-config/sync-notes/2026-05-08-resource-governance-scaffold-guidance.md +38 -38
- package/reference/claude-config/sync-notes/2026-05-09-clients-domain.md +32 -32
- package/reference/claude-config/sync-notes/2026-05-09-command-system.md +33 -33
- package/reference/claude-config/sync-notes/2026-05-09-resource-governance-and-misc.md +69 -69
- package/reference/claude-config/sync-notes/2026-05-12-sdk-ready-release-train.md +30 -30
- package/reference/claude-config/sync-notes/2026-05-14-organization-model-ontology-refactor.md +42 -0
- package/reference/claude-config/sync-notes/README.md +43 -43
- package/reference/cli.mdx +808 -808
- package/reference/concepts.mdx +146 -146
- package/reference/deployment/api.mdx +297 -297
- package/reference/deployment/command-center.mdx +209 -209
- package/reference/deployment/index.mdx +195 -195
- package/reference/deployment/provided-features.mdx +107 -107
- package/reference/deployment/ui-execution.mdx +250 -250
- package/reference/examples/organization-model.ts +146 -83
- package/reference/framework/agent.mdx +156 -156
- package/reference/framework/index.mdx +195 -195
- package/reference/framework/interaction-guidance.mdx +182 -182
- package/reference/framework/memory.mdx +326 -326
- package/reference/framework/project-structure.mdx +282 -282
- package/reference/framework/tutorial-system.mdx +135 -135
- package/reference/getting-started.mdx +142 -142
- package/reference/index.mdx +106 -106
- package/reference/packages/core/src/README.md +14 -14
- package/reference/packages/core/src/business/README.md +2 -2
- package/reference/packages/core/src/knowledge/README.md +32 -32
- package/reference/packages/core/src/organization-model/README.md +149 -149
- package/reference/packages/core/src/test-utils/README.md +37 -37
- package/reference/packages/ui/src/api/README.md +18 -18
- package/reference/packages/ui/src/app/README.md +24 -24
- package/reference/packages/ui/src/auth/README.md +18 -18
- package/reference/packages/ui/src/components/README.md +24 -24
- package/reference/packages/ui/src/execution/README.md +16 -16
- package/reference/packages/ui/src/features/README.md +28 -28
- package/reference/packages/ui/src/graph/README.md +16 -16
- package/reference/packages/ui/src/hooks/README.md +23 -23
- package/reference/packages/ui/src/initialization/README.md +19 -19
- package/reference/packages/ui/src/knowledge/README.md +31 -31
- package/reference/packages/ui/src/organization/README.md +18 -18
- package/reference/packages/ui/src/profile/README.md +19 -19
- package/reference/packages/ui/src/provider/README.md +32 -32
- package/reference/packages/ui/src/router/README.md +18 -18
- package/reference/packages/ui/src/sse/README.md +13 -13
- package/reference/packages/ui/src/test-utils/README.md +7 -7
- package/reference/packages/ui/src/theme/README.md +23 -23
- package/reference/packages/ui/src/theme/presets/README.md +19 -19
- package/reference/packages/ui/src/types/README.md +16 -16
- package/reference/packages/ui/src/utils/README.md +18 -18
- package/reference/packages/ui/src/zustand/README.md +18 -18
- package/reference/platform-tools/adapters-integration.mdx +301 -301
- package/reference/platform-tools/adapters-platform.mdx +553 -553
- package/reference/platform-tools/index.mdx +217 -217
- package/reference/platform-tools/type-safety.mdx +82 -82
- package/reference/resources/index.mdx +349 -349
- package/reference/resources/patterns.mdx +449 -449
- package/reference/resources/types.mdx +116 -116
- package/reference/roadmap.mdx +165 -165
- package/reference/runtime.mdx +173 -173
- package/reference/scaffold/core/organization-graph.mdx +110 -90
- package/reference/scaffold/core/organization-model.mdx +226 -219
- package/reference/scaffold/index.mdx +67 -67
- package/reference/scaffold/operations/propagation-pipeline.md +77 -77
- package/reference/scaffold/operations/scaffold-maintenance.md +12 -12
- package/reference/scaffold/operations/workflow-recipes.md +138 -138
- package/reference/scaffold/recipes/add-a-feature.md +308 -88
- package/reference/scaffold/recipes/add-a-resource.md +134 -110
- package/reference/scaffold/recipes/customize-knowledge-browser.md +5 -5
- package/reference/scaffold/recipes/customize-organization-model.md +273 -138
- package/reference/scaffold/recipes/extend-a-base-entity.md +8 -8
- package/reference/scaffold/recipes/extend-crm.md +3 -3
- package/reference/scaffold/recipes/extend-lead-gen.md +400 -400
- package/reference/scaffold/recipes/gate-by-feature-or-admin.md +118 -118
- package/reference/scaffold/recipes/index.md +46 -46
- package/reference/scaffold/recipes/query-the-knowledge-graph.md +197 -170
- package/reference/scaffold/reference/contracts.md +2101 -2096
- package/reference/scaffold/reference/glossary.md +76 -76
- package/reference/scaffold/ui/composition-extensibility.mdx +233 -233
- package/reference/scaffold/ui/customization.md +243 -243
- package/reference/scaffold/ui/feature-flags-and-gating.md +46 -46
- package/reference/scaffold/ui/feature-shell.mdx +72 -72
- package/reference/scaffold/ui/recipes.md +221 -214
- package/reference/spine/spine-primer.md +96 -96
- package/reference/templates/index.mdx +47 -47
- package/reference/troubleshooting.mdx +223 -223
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elevasis/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "SDK for building Elevasis organization resources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"tsup": "^8.0.0",
|
|
58
58
|
"typescript": "5.9.2",
|
|
59
59
|
"zod": "^4.1.0",
|
|
60
|
-
"@repo/
|
|
60
|
+
"@repo/eslint-config": "0.0.0",
|
|
61
61
|
"@repo/typescript-config": "0.0.0",
|
|
62
|
-
"@repo/
|
|
62
|
+
"@repo/core": "0.24.0"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"lint": "eslint src --max-warnings 0",
|
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// post-edit-validate.mjs
|
|
3
|
-
// PostToolUse hook — auto-formats with prettier, type-checks .ts/.tsx files.
|
|
4
|
-
// Fires after Edit|Write|MultiEdit succeeds.
|
|
5
|
-
|
|
6
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
7
|
-
import { resolve, normalize, extname, join, dirname, relative } from 'node:path'
|
|
8
|
-
import { execSync } from 'node:child_process'
|
|
9
|
-
|
|
10
|
-
const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
|
|
11
|
-
|
|
12
|
-
// Extensions prettier should format
|
|
13
|
-
const PRETTIER_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.css', '.md'])
|
|
14
|
-
|
|
15
|
-
// Extensions that trigger type-checking
|
|
16
|
-
const TS_EXTENSIONS = new Set(['.ts', '.tsx'])
|
|
17
|
-
|
|
18
|
-
function findNearestTsconfig(startDir) {
|
|
19
|
-
let dir = startDir
|
|
20
|
-
const root = normalize(ROOT)
|
|
21
|
-
while (dir.length >= root.length) {
|
|
22
|
-
const candidate = join(dir, 'tsconfig.json')
|
|
23
|
-
if (existsSync(candidate)) return candidate
|
|
24
|
-
const parent = dirname(dir)
|
|
25
|
-
if (parent === dir) break
|
|
26
|
-
dir = parent
|
|
27
|
-
}
|
|
28
|
-
return null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const chunks = []
|
|
33
|
-
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
34
|
-
const input = JSON.parse(Buffer.concat(chunks).toString())
|
|
35
|
-
|
|
36
|
-
const filePath = input.tool_input?.file_path
|
|
37
|
-
if (!filePath) process.exit(0)
|
|
38
|
-
|
|
39
|
-
const ext = extname(filePath).toLowerCase()
|
|
40
|
-
const absPath = normalize(resolve(filePath))
|
|
41
|
-
if (!existsSync(absPath)) process.exit(0)
|
|
42
|
-
|
|
43
|
-
const results = []
|
|
44
|
-
|
|
45
|
-
// 1. Prettier (skip silently if not installed yet, e.g. before first pnpm install)
|
|
46
|
-
if (PRETTIER_EXTENSIONS.has(ext)) {
|
|
47
|
-
try {
|
|
48
|
-
execSync('pnpm exec prettier --version', { cwd: ROOT, stdio: 'pipe', timeout: 5_000 })
|
|
49
|
-
try {
|
|
50
|
-
execSync('pnpm exec prettier --write "' + absPath + '"', {
|
|
51
|
-
cwd: ROOT,
|
|
52
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
53
|
-
timeout: 10_000
|
|
54
|
-
})
|
|
55
|
-
} catch (err) {
|
|
56
|
-
const stderr = err.stderr?.toString().trim() || ''
|
|
57
|
-
if (stderr && !/ignored/i.test(stderr)) {
|
|
58
|
-
results.push('Prettier error: ' + stderr.slice(0, 300))
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
// prettier not installed yet -- skip silently
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 2. Type-check for .ts/.tsx
|
|
67
|
-
if (TS_EXTENSIONS.has(ext)) {
|
|
68
|
-
const tsconfig = findNearestTsconfig(dirname(absPath))
|
|
69
|
-
if (tsconfig) {
|
|
70
|
-
try {
|
|
71
|
-
execSync('pnpm exec tsc --noEmit -p "' + tsconfig + '"', {
|
|
72
|
-
cwd: ROOT,
|
|
73
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
-
timeout: 30_000,
|
|
75
|
-
env: { ...process.env, NODE_OPTIONS: '--max-old-space-size=4096' }
|
|
76
|
-
})
|
|
77
|
-
} catch (err) {
|
|
78
|
-
if (err.killed) process.exit(0) // Don't block on timeout
|
|
79
|
-
const stdout = err.stdout?.toString() || ''
|
|
80
|
-
if (stdout.includes('error TS')) {
|
|
81
|
-
const errorLines = stdout
|
|
82
|
-
.split('\n')
|
|
83
|
-
.filter((l) => l.includes('error TS'))
|
|
84
|
-
.slice(0, 10)
|
|
85
|
-
results.push('Type errors after editing ' + filePath + ':\n' + errorLines.join('\n'))
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Output errors to Claude's context (silence = success)
|
|
92
|
-
if (results.length > 0) {
|
|
93
|
-
process.stderr.write(results.join('\n\n'))
|
|
94
|
-
process.exit(2) // Exit 2 = send stderr as feedback to Claude
|
|
95
|
-
}
|
|
96
|
-
} catch {}
|
|
97
|
-
|
|
98
|
-
process.exit(0)
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// post-edit-validate.mjs
|
|
3
|
+
// PostToolUse hook — auto-formats with prettier, type-checks .ts/.tsx files.
|
|
4
|
+
// Fires after Edit|Write|MultiEdit succeeds.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
7
|
+
import { resolve, normalize, extname, join, dirname, relative } from 'node:path'
|
|
8
|
+
import { execSync } from 'node:child_process'
|
|
9
|
+
|
|
10
|
+
const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
|
|
11
|
+
|
|
12
|
+
// Extensions prettier should format
|
|
13
|
+
const PRETTIER_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.css', '.md'])
|
|
14
|
+
|
|
15
|
+
// Extensions that trigger type-checking
|
|
16
|
+
const TS_EXTENSIONS = new Set(['.ts', '.tsx'])
|
|
17
|
+
|
|
18
|
+
function findNearestTsconfig(startDir) {
|
|
19
|
+
let dir = startDir
|
|
20
|
+
const root = normalize(ROOT)
|
|
21
|
+
while (dir.length >= root.length) {
|
|
22
|
+
const candidate = join(dir, 'tsconfig.json')
|
|
23
|
+
if (existsSync(candidate)) return candidate
|
|
24
|
+
const parent = dirname(dir)
|
|
25
|
+
if (parent === dir) break
|
|
26
|
+
dir = parent
|
|
27
|
+
}
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const chunks = []
|
|
33
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
34
|
+
const input = JSON.parse(Buffer.concat(chunks).toString())
|
|
35
|
+
|
|
36
|
+
const filePath = input.tool_input?.file_path
|
|
37
|
+
if (!filePath) process.exit(0)
|
|
38
|
+
|
|
39
|
+
const ext = extname(filePath).toLowerCase()
|
|
40
|
+
const absPath = normalize(resolve(filePath))
|
|
41
|
+
if (!existsSync(absPath)) process.exit(0)
|
|
42
|
+
|
|
43
|
+
const results = []
|
|
44
|
+
|
|
45
|
+
// 1. Prettier (skip silently if not installed yet, e.g. before first pnpm install)
|
|
46
|
+
if (PRETTIER_EXTENSIONS.has(ext)) {
|
|
47
|
+
try {
|
|
48
|
+
execSync('pnpm exec prettier --version', { cwd: ROOT, stdio: 'pipe', timeout: 5_000 })
|
|
49
|
+
try {
|
|
50
|
+
execSync('pnpm exec prettier --write "' + absPath + '"', {
|
|
51
|
+
cwd: ROOT,
|
|
52
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
53
|
+
timeout: 10_000
|
|
54
|
+
})
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const stderr = err.stderr?.toString().trim() || ''
|
|
57
|
+
if (stderr && !/ignored/i.test(stderr)) {
|
|
58
|
+
results.push('Prettier error: ' + stderr.slice(0, 300))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// prettier not installed yet -- skip silently
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. Type-check for .ts/.tsx
|
|
67
|
+
if (TS_EXTENSIONS.has(ext)) {
|
|
68
|
+
const tsconfig = findNearestTsconfig(dirname(absPath))
|
|
69
|
+
if (tsconfig) {
|
|
70
|
+
try {
|
|
71
|
+
execSync('pnpm exec tsc --noEmit -p "' + tsconfig + '"', {
|
|
72
|
+
cwd: ROOT,
|
|
73
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
+
timeout: 30_000,
|
|
75
|
+
env: { ...process.env, NODE_OPTIONS: '--max-old-space-size=4096' }
|
|
76
|
+
})
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.killed) process.exit(0) // Don't block on timeout
|
|
79
|
+
const stdout = err.stdout?.toString() || ''
|
|
80
|
+
if (stdout.includes('error TS')) {
|
|
81
|
+
const errorLines = stdout
|
|
82
|
+
.split('\n')
|
|
83
|
+
.filter((l) => l.includes('error TS'))
|
|
84
|
+
.slice(0, 10)
|
|
85
|
+
results.push('Type errors after editing ' + filePath + ':\n' + errorLines.join('\n'))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Output errors to Claude's context (silence = success)
|
|
92
|
+
if (results.length > 0) {
|
|
93
|
+
process.stderr.write(results.join('\n\n'))
|
|
94
|
+
process.exit(2) // Exit 2 = send stderr as feedback to Claude
|
|
95
|
+
}
|
|
96
|
+
} catch {}
|
|
97
|
+
|
|
98
|
+
process.exit(0)
|
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// scaffold-registry-reminder.mjs
|
|
3
|
-
// PostToolUse hook — reads the compiled scaffold registry and emits advisory
|
|
4
|
-
// reminders when an edited file matches a registry source pattern.
|
|
5
|
-
//
|
|
6
|
-
// Template twin of the monorepo hook. Gracefully no-ops when the compiled
|
|
7
|
-
// registry is absent (e.g. before SDK delivers scaffold-registry.compiled.json
|
|
8
|
-
// to external projects — Step 7/SDK milestone).
|
|
9
|
-
//
|
|
10
|
-
// Exit 0 always (advisory hook — never blocks).
|
|
11
|
-
|
|
12
|
-
import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs'
|
|
13
|
-
import { join, normalize, relative } from 'node:path'
|
|
14
|
-
|
|
15
|
-
const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
|
|
16
|
-
const LOG_DIR = join(ROOT, '.claude', 'logs')
|
|
17
|
-
const LOG_FILE = join(LOG_DIR, 'scaffold-registry-reminder.log')
|
|
18
|
-
const STATE_FILE = join(LOG_DIR, 'scaffold-registry-reminder.state.json')
|
|
19
|
-
const REGISTRY_FILE = join(ROOT, '.claude', 'scaffold-registry.compiled.json')
|
|
20
|
-
|
|
21
|
-
const DEFAULT_COOLDOWN_MS = 300_000 // 5 minutes
|
|
22
|
-
|
|
23
|
-
const GENERATED_DIR_SEGMENTS = ['_generated', '_gen']
|
|
24
|
-
const GENERATED_CONTENT_MARKER = '@generated'
|
|
25
|
-
|
|
26
|
-
function log(msg) {
|
|
27
|
-
try {
|
|
28
|
-
mkdirSync(LOG_DIR, { recursive: true })
|
|
29
|
-
appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`)
|
|
30
|
-
} catch {}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function pathMatchesPattern(filePath, pattern) {
|
|
34
|
-
const normalizedFile = filePath.replace(/\\/g, '/')
|
|
35
|
-
const normalizedPattern = pattern.replace(/\\/g, '/')
|
|
36
|
-
|
|
37
|
-
if (normalizedFile === normalizedPattern) return true
|
|
38
|
-
|
|
39
|
-
if (normalizedPattern.endsWith('/**') || normalizedPattern.endsWith('/*')) {
|
|
40
|
-
const prefix = normalizedPattern.slice(0, normalizedPattern.lastIndexOf('/*'))
|
|
41
|
-
return normalizedFile.startsWith(prefix + '/')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (normalizedPattern.includes('*')) {
|
|
45
|
-
const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')
|
|
46
|
-
return new RegExp(`^${escaped}$`).test(normalizedFile)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return normalizedFile.startsWith(normalizedPattern + '/')
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function loadState() {
|
|
53
|
-
try {
|
|
54
|
-
return JSON.parse(readFileSync(STATE_FILE, 'utf-8'))
|
|
55
|
-
} catch {
|
|
56
|
-
return {}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function saveState(state) {
|
|
61
|
-
try {
|
|
62
|
-
mkdirSync(LOG_DIR, { recursive: true })
|
|
63
|
-
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2) + '\n', 'utf-8')
|
|
64
|
-
} catch {}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function throttleKey(entryId, filePath) {
|
|
68
|
-
return `${entryId}:${filePath}`
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function isCoolingDown(state, key, cooldownMs) {
|
|
72
|
-
const last = state[key]
|
|
73
|
-
if (!last) return false
|
|
74
|
-
return Date.now() - last < cooldownMs
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function formatDependentLine(dep) {
|
|
78
|
-
const regen = dep.regen === 'manual' ? 'manual check' : dep.regen
|
|
79
|
-
const hint = dep.hint ? ` [${dep.hint}]` : ''
|
|
80
|
-
return ` - ${dep.path} -> ${regen}${hint}`
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function emitReminder(entry, relFilePath) {
|
|
84
|
-
const lines = [
|
|
85
|
-
`\uD83D\uDD14 Scaffold reminder -- ${entry.id} (${relFilePath})`,
|
|
86
|
-
` Downstream scaffolds that may need updating:`
|
|
87
|
-
]
|
|
88
|
-
for (const dep of entry.dependents) {
|
|
89
|
-
lines.push(formatDependentLine(dep))
|
|
90
|
-
}
|
|
91
|
-
lines.push(
|
|
92
|
-
` If this is a scaffold-sensitive pattern not in the registry, also add an entry to .claude/scaffold-registry.yml.`
|
|
93
|
-
)
|
|
94
|
-
return lines.join('\n')
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function emitMissingEntryHint(relFilePath) {
|
|
98
|
-
return [
|
|
99
|
-
`\uD83D\uDD14 Scaffold reminder -- unregistered generated path (${relFilePath})`,
|
|
100
|
-
` This path looks scaffold-generated but has no registry entry.`,
|
|
101
|
-
` If it is scaffold-sensitive, add a new entry to .claude/scaffold-registry.yml`,
|
|
102
|
-
` so the reminder hook and /work handoff can track it.`
|
|
103
|
-
].join('\n')
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function looksLikeGeneratedPath(filePath) {
|
|
107
|
-
const normalizedFile = filePath.replace(/\\/g, '/')
|
|
108
|
-
const segments = normalizedFile.split('/')
|
|
109
|
-
return segments.some((seg) => GENERATED_DIR_SEGMENTS.includes(seg))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function looksLikeGeneratedContent(absFilePath) {
|
|
113
|
-
try {
|
|
114
|
-
const content = readFileSync(absFilePath, 'utf-8').slice(0, 500)
|
|
115
|
-
return content.includes(GENERATED_CONTENT_MARKER)
|
|
116
|
-
} catch {
|
|
117
|
-
return false
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const chunks = []
|
|
123
|
-
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
124
|
-
const input = JSON.parse(Buffer.concat(chunks).toString())
|
|
125
|
-
|
|
126
|
-
const rawFilePath = input.tool_input?.file_path
|
|
127
|
-
if (!rawFilePath) process.exit(0)
|
|
128
|
-
|
|
129
|
-
const absFilePath = normalize(rawFilePath)
|
|
130
|
-
const relFilePath = relative(ROOT, absFilePath).replace(/\\/g, '/')
|
|
131
|
-
|
|
132
|
-
// Graceful no-op when registry is absent (pre-SDK-delivery state)
|
|
133
|
-
let registry
|
|
134
|
-
try {
|
|
135
|
-
const raw = readFileSync(REGISTRY_FILE, 'utf-8')
|
|
136
|
-
registry = JSON.parse(raw)
|
|
137
|
-
} catch {
|
|
138
|
-
log(`SKIP — registry not found (pre-SDK-delivery)`)
|
|
139
|
-
process.exit(0)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const entries = registry?.entries ?? []
|
|
143
|
-
|
|
144
|
-
const matched = entries.filter((entry) =>
|
|
145
|
-
(entry.sources ?? []).some((pattern) => pathMatchesPattern(relFilePath, pattern))
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
const state = loadState()
|
|
149
|
-
const now = Date.now()
|
|
150
|
-
const messages = []
|
|
151
|
-
|
|
152
|
-
if (matched.length > 0) {
|
|
153
|
-
for (const entry of matched) {
|
|
154
|
-
const key = throttleKey(entry.id, relFilePath)
|
|
155
|
-
const cooldown = entry.cooldown_ms ?? DEFAULT_COOLDOWN_MS
|
|
156
|
-
if (isCoolingDown(state, key, cooldown)) {
|
|
157
|
-
log(`THROTTLED — ${entry.id} for ${relFilePath}`)
|
|
158
|
-
continue
|
|
159
|
-
}
|
|
160
|
-
messages.push(emitReminder(entry, relFilePath))
|
|
161
|
-
state[key] = now
|
|
162
|
-
log(`EMITTED — ${entry.id} for ${relFilePath}`)
|
|
163
|
-
}
|
|
164
|
-
} else {
|
|
165
|
-
const isGenerated = looksLikeGeneratedPath(relFilePath) || looksLikeGeneratedContent(absFilePath)
|
|
166
|
-
|
|
167
|
-
if (isGenerated) {
|
|
168
|
-
const key = throttleKey('__missing__', relFilePath)
|
|
169
|
-
if (!isCoolingDown(state, key, DEFAULT_COOLDOWN_MS)) {
|
|
170
|
-
messages.push(emitMissingEntryHint(relFilePath))
|
|
171
|
-
state[key] = now
|
|
172
|
-
log(`EMITTED missing-entry hint for ${relFilePath}`)
|
|
173
|
-
} else {
|
|
174
|
-
log(`THROTTLED missing-entry hint for ${relFilePath}`)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (messages.length > 0) {
|
|
180
|
-
saveState(state)
|
|
181
|
-
process.stderr.write(messages.join('\n\n') + '\n')
|
|
182
|
-
process.exit(2)
|
|
183
|
-
}
|
|
184
|
-
} catch (err) {
|
|
185
|
-
log(`ERROR: ${err.message}`)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
process.exit(0)
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scaffold-registry-reminder.mjs
|
|
3
|
+
// PostToolUse hook — reads the compiled scaffold registry and emits advisory
|
|
4
|
+
// reminders when an edited file matches a registry source pattern.
|
|
5
|
+
//
|
|
6
|
+
// Template twin of the monorepo hook. Gracefully no-ops when the compiled
|
|
7
|
+
// registry is absent (e.g. before SDK delivers scaffold-registry.compiled.json
|
|
8
|
+
// to external projects — Step 7/SDK milestone).
|
|
9
|
+
//
|
|
10
|
+
// Exit 0 always (advisory hook — never blocks).
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs'
|
|
13
|
+
import { join, normalize, relative } from 'node:path'
|
|
14
|
+
|
|
15
|
+
const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
|
|
16
|
+
const LOG_DIR = join(ROOT, '.claude', 'logs')
|
|
17
|
+
const LOG_FILE = join(LOG_DIR, 'scaffold-registry-reminder.log')
|
|
18
|
+
const STATE_FILE = join(LOG_DIR, 'scaffold-registry-reminder.state.json')
|
|
19
|
+
const REGISTRY_FILE = join(ROOT, '.claude', 'scaffold-registry.compiled.json')
|
|
20
|
+
|
|
21
|
+
const DEFAULT_COOLDOWN_MS = 300_000 // 5 minutes
|
|
22
|
+
|
|
23
|
+
const GENERATED_DIR_SEGMENTS = ['_generated', '_gen']
|
|
24
|
+
const GENERATED_CONTENT_MARKER = '@generated'
|
|
25
|
+
|
|
26
|
+
function log(msg) {
|
|
27
|
+
try {
|
|
28
|
+
mkdirSync(LOG_DIR, { recursive: true })
|
|
29
|
+
appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`)
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pathMatchesPattern(filePath, pattern) {
|
|
34
|
+
const normalizedFile = filePath.replace(/\\/g, '/')
|
|
35
|
+
const normalizedPattern = pattern.replace(/\\/g, '/')
|
|
36
|
+
|
|
37
|
+
if (normalizedFile === normalizedPattern) return true
|
|
38
|
+
|
|
39
|
+
if (normalizedPattern.endsWith('/**') || normalizedPattern.endsWith('/*')) {
|
|
40
|
+
const prefix = normalizedPattern.slice(0, normalizedPattern.lastIndexOf('/*'))
|
|
41
|
+
return normalizedFile.startsWith(prefix + '/')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (normalizedPattern.includes('*')) {
|
|
45
|
+
const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')
|
|
46
|
+
return new RegExp(`^${escaped}$`).test(normalizedFile)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return normalizedFile.startsWith(normalizedPattern + '/')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadState() {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(STATE_FILE, 'utf-8'))
|
|
55
|
+
} catch {
|
|
56
|
+
return {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function saveState(state) {
|
|
61
|
+
try {
|
|
62
|
+
mkdirSync(LOG_DIR, { recursive: true })
|
|
63
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2) + '\n', 'utf-8')
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function throttleKey(entryId, filePath) {
|
|
68
|
+
return `${entryId}:${filePath}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isCoolingDown(state, key, cooldownMs) {
|
|
72
|
+
const last = state[key]
|
|
73
|
+
if (!last) return false
|
|
74
|
+
return Date.now() - last < cooldownMs
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatDependentLine(dep) {
|
|
78
|
+
const regen = dep.regen === 'manual' ? 'manual check' : dep.regen
|
|
79
|
+
const hint = dep.hint ? ` [${dep.hint}]` : ''
|
|
80
|
+
return ` - ${dep.path} -> ${regen}${hint}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function emitReminder(entry, relFilePath) {
|
|
84
|
+
const lines = [
|
|
85
|
+
`\uD83D\uDD14 Scaffold reminder -- ${entry.id} (${relFilePath})`,
|
|
86
|
+
` Downstream scaffolds that may need updating:`
|
|
87
|
+
]
|
|
88
|
+
for (const dep of entry.dependents) {
|
|
89
|
+
lines.push(formatDependentLine(dep))
|
|
90
|
+
}
|
|
91
|
+
lines.push(
|
|
92
|
+
` If this is a scaffold-sensitive pattern not in the registry, also add an entry to .claude/scaffold-registry.yml.`
|
|
93
|
+
)
|
|
94
|
+
return lines.join('\n')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function emitMissingEntryHint(relFilePath) {
|
|
98
|
+
return [
|
|
99
|
+
`\uD83D\uDD14 Scaffold reminder -- unregistered generated path (${relFilePath})`,
|
|
100
|
+
` This path looks scaffold-generated but has no registry entry.`,
|
|
101
|
+
` If it is scaffold-sensitive, add a new entry to .claude/scaffold-registry.yml`,
|
|
102
|
+
` so the reminder hook and /work handoff can track it.`
|
|
103
|
+
].join('\n')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function looksLikeGeneratedPath(filePath) {
|
|
107
|
+
const normalizedFile = filePath.replace(/\\/g, '/')
|
|
108
|
+
const segments = normalizedFile.split('/')
|
|
109
|
+
return segments.some((seg) => GENERATED_DIR_SEGMENTS.includes(seg))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function looksLikeGeneratedContent(absFilePath) {
|
|
113
|
+
try {
|
|
114
|
+
const content = readFileSync(absFilePath, 'utf-8').slice(0, 500)
|
|
115
|
+
return content.includes(GENERATED_CONTENT_MARKER)
|
|
116
|
+
} catch {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const chunks = []
|
|
123
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
124
|
+
const input = JSON.parse(Buffer.concat(chunks).toString())
|
|
125
|
+
|
|
126
|
+
const rawFilePath = input.tool_input?.file_path
|
|
127
|
+
if (!rawFilePath) process.exit(0)
|
|
128
|
+
|
|
129
|
+
const absFilePath = normalize(rawFilePath)
|
|
130
|
+
const relFilePath = relative(ROOT, absFilePath).replace(/\\/g, '/')
|
|
131
|
+
|
|
132
|
+
// Graceful no-op when registry is absent (pre-SDK-delivery state)
|
|
133
|
+
let registry
|
|
134
|
+
try {
|
|
135
|
+
const raw = readFileSync(REGISTRY_FILE, 'utf-8')
|
|
136
|
+
registry = JSON.parse(raw)
|
|
137
|
+
} catch {
|
|
138
|
+
log(`SKIP — registry not found (pre-SDK-delivery)`)
|
|
139
|
+
process.exit(0)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const entries = registry?.entries ?? []
|
|
143
|
+
|
|
144
|
+
const matched = entries.filter((entry) =>
|
|
145
|
+
(entry.sources ?? []).some((pattern) => pathMatchesPattern(relFilePath, pattern))
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const state = loadState()
|
|
149
|
+
const now = Date.now()
|
|
150
|
+
const messages = []
|
|
151
|
+
|
|
152
|
+
if (matched.length > 0) {
|
|
153
|
+
for (const entry of matched) {
|
|
154
|
+
const key = throttleKey(entry.id, relFilePath)
|
|
155
|
+
const cooldown = entry.cooldown_ms ?? DEFAULT_COOLDOWN_MS
|
|
156
|
+
if (isCoolingDown(state, key, cooldown)) {
|
|
157
|
+
log(`THROTTLED — ${entry.id} for ${relFilePath}`)
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
messages.push(emitReminder(entry, relFilePath))
|
|
161
|
+
state[key] = now
|
|
162
|
+
log(`EMITTED — ${entry.id} for ${relFilePath}`)
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const isGenerated = looksLikeGeneratedPath(relFilePath) || looksLikeGeneratedContent(absFilePath)
|
|
166
|
+
|
|
167
|
+
if (isGenerated) {
|
|
168
|
+
const key = throttleKey('__missing__', relFilePath)
|
|
169
|
+
if (!isCoolingDown(state, key, DEFAULT_COOLDOWN_MS)) {
|
|
170
|
+
messages.push(emitMissingEntryHint(relFilePath))
|
|
171
|
+
state[key] = now
|
|
172
|
+
log(`EMITTED missing-entry hint for ${relFilePath}`)
|
|
173
|
+
} else {
|
|
174
|
+
log(`THROTTLED missing-entry hint for ${relFilePath}`)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (messages.length > 0) {
|
|
180
|
+
saveState(state)
|
|
181
|
+
process.stderr.write(messages.join('\n\n') + '\n')
|
|
182
|
+
process.exit(2)
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
log(`ERROR: ${err.message}`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
process.exit(0)
|