safeword 0.12.2 → 0.14.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.
Files changed (44) hide show
  1. package/dist/{check-3X75X2JL.js → check-X7NR4WAM.js} +5 -4
  2. package/dist/check-X7NR4WAM.js.map +1 -0
  3. package/dist/{chunk-3R26BJXN.js → chunk-TJOHD7CV.js} +56 -5
  4. package/dist/{chunk-3R26BJXN.js.map → chunk-TJOHD7CV.js.map} +1 -1
  5. package/dist/{chunk-O4LAXZK3.js → chunk-XLOXGDJG.js} +103 -41
  6. package/dist/chunk-XLOXGDJG.js.map +1 -0
  7. package/dist/cli.js +6 -6
  8. package/dist/{diff-6HFT7BLG.js → diff-3USPMFT2.js} +2 -2
  9. package/dist/{reset-XFXLQXOC.js → reset-CM3BNT5S.js} +2 -2
  10. package/dist/{setup-C6NF3YJ5.js → setup-C6MJRBGY.js} +5 -18
  11. package/dist/setup-C6MJRBGY.js.map +1 -0
  12. package/dist/{sync-config-PPTR3JPA.js → sync-config-KZE4R47T.js} +2 -2
  13. package/dist/{upgrade-C2I22FAB.js → upgrade-BIMRJENC.js} +12 -6
  14. package/dist/upgrade-BIMRJENC.js.map +1 -0
  15. package/package.json +2 -2
  16. package/templates/hooks/cursor/after-file-edit.ts +47 -0
  17. package/templates/hooks/cursor/stop.ts +73 -0
  18. package/templates/hooks/lib/lint.ts +49 -0
  19. package/templates/hooks/lib/quality.ts +18 -0
  20. package/templates/hooks/post-tool-lint.ts +33 -0
  21. package/templates/hooks/prompt-questions.ts +17 -0
  22. package/templates/hooks/prompt-timestamp.ts +30 -0
  23. package/templates/hooks/session-lint-check.ts +62 -0
  24. package/templates/hooks/session-verify-agents.ts +32 -0
  25. package/templates/hooks/session-version.ts +18 -0
  26. package/templates/hooks/stop-quality.ts +168 -0
  27. package/dist/check-3X75X2JL.js.map +0 -1
  28. package/dist/chunk-O4LAXZK3.js.map +0 -1
  29. package/dist/setup-C6NF3YJ5.js.map +0 -1
  30. package/dist/upgrade-C2I22FAB.js.map +0 -1
  31. package/templates/hooks/cursor/after-file-edit.sh +0 -58
  32. package/templates/hooks/cursor/stop.sh +0 -50
  33. package/templates/hooks/post-tool-lint.sh +0 -51
  34. package/templates/hooks/prompt-questions.sh +0 -27
  35. package/templates/hooks/prompt-timestamp.sh +0 -13
  36. package/templates/hooks/session-lint-check.sh +0 -42
  37. package/templates/hooks/session-verify-agents.sh +0 -31
  38. package/templates/hooks/session-version.sh +0 -17
  39. package/templates/hooks/stop-quality.sh +0 -91
  40. package/templates/lib/common.sh +0 -26
  41. package/templates/lib/jq-fallback.sh +0 -20
  42. /package/dist/{diff-6HFT7BLG.js.map → diff-3USPMFT2.js.map} +0 -0
  43. /package/dist/{reset-XFXLQXOC.js.map → reset-CM3BNT5S.js.map} +0 -0
  44. /package/dist/{sync-config-PPTR3JPA.js.map → sync-config-KZE4R47T.js.map} +0 -0
@@ -0,0 +1,49 @@
1
+ // Shared linting logic for Claude Code and Cursor hooks
2
+ // Used by: post-tool-lint.ts, cursor/after-file-edit.ts
3
+
4
+ import { existsSync } from 'node:fs';
5
+
6
+ import { $ } from 'bun';
7
+
8
+ // File extensions for different linting strategies
9
+ const JS_EXTENSIONS = new Set(['js', 'jsx', 'ts', 'tsx', 'mjs', 'mts', 'cjs', 'cts', 'vue', 'svelte', 'astro']);
10
+ const PRETTIER_EXTENSIONS = new Set(['md', 'json', 'css', 'scss', 'html', 'yaml', 'yml', 'graphql']);
11
+
12
+ /**
13
+ * Lint a file based on its extension.
14
+ * Runs ESLint + Prettier for JS/TS, Prettier only for other formats,
15
+ * and shellcheck + Prettier for shell scripts.
16
+ *
17
+ * @param file - Path to the file to lint
18
+ * @param projectDir - Project root directory (for finding prettier-plugin-sh)
19
+ */
20
+ export async function lintFile(file: string, projectDir: string): Promise<void> {
21
+ const extension = file.split('.').pop()?.toLowerCase() ?? '';
22
+
23
+ // JS/TS and framework files - ESLint first (fix code), then Prettier (format)
24
+ if (JS_EXTENSIONS.has(extension)) {
25
+ const eslintResult = await $`npx eslint --fix ${file}`.nothrow().quiet();
26
+ if (eslintResult.exitCode !== 0 && eslintResult.stderr.length > 0) {
27
+ console.log(eslintResult.stderr.toString());
28
+ }
29
+ await $`npx prettier --write ${file}`.nothrow().quiet();
30
+ return;
31
+ }
32
+
33
+ // Other supported formats - prettier only
34
+ if (PRETTIER_EXTENSIONS.has(extension)) {
35
+ await $`npx prettier --write ${file}`.nothrow().quiet();
36
+ return;
37
+ }
38
+
39
+ // Shell scripts - shellcheck (if available), then Prettier (if plugin installed)
40
+ if (extension === 'sh') {
41
+ const shellcheckResult = await $`npx shellcheck ${file}`.nothrow().quiet();
42
+ if (shellcheckResult.exitCode !== 0 && shellcheckResult.stderr.length > 0) {
43
+ console.log(shellcheckResult.stderr.toString());
44
+ }
45
+ if (existsSync(`${projectDir}/node_modules/prettier-plugin-sh`)) {
46
+ await $`npx prettier --write ${file}`.nothrow().quiet();
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,18 @@
1
+ // Shared quality review message for Claude Code and Cursor hooks
2
+ // Used by: stop-quality.ts, cursor/stop.ts
3
+
4
+ /**
5
+ * The quality review prompt shown when changes are made.
6
+ * Used by both Claude Code Stop hook and Cursor stop hook.
7
+ */
8
+ export const QUALITY_REVIEW_MESSAGE = `SAFEWORD Quality Review:
9
+
10
+ Double check and critique your work again just in case.
11
+ Assume you've never seen it before.
12
+
13
+ - Is it correct?
14
+ - Is it elegant?
15
+ - Does it follow latest docs/best practices?
16
+ - If questions remain: research first, then ask targeted questions.
17
+ - Avoid bloat.
18
+ - If you asked a question above that's still relevant after review, re-ask it.`;
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Auto-lint changed files (PostToolUse)
3
+ // Silently auto-fixes, only outputs unfixable errors
4
+
5
+ import { lintFile } from './lib/lint.ts';
6
+
7
+ interface HookInput {
8
+ tool_input?: {
9
+ file_path?: string;
10
+ notebook_path?: string;
11
+ };
12
+ }
13
+
14
+ // Read hook input from stdin
15
+ let input: HookInput;
16
+ try {
17
+ input = await Bun.stdin.json();
18
+ } catch (error) {
19
+ if (process.env.DEBUG) console.error('[post-tool-lint] stdin parse error:', error);
20
+ process.exit(0);
21
+ }
22
+
23
+ const file = input.tool_input?.file_path ?? input.tool_input?.notebook_path;
24
+
25
+ // Exit silently if no file or file doesn't exist
26
+ if (!file || !(await Bun.file(file).exists())) {
27
+ process.exit(0);
28
+ }
29
+
30
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
31
+ process.chdir(projectDir);
32
+
33
+ await lintFile(file, projectDir);
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Question protocol guidance (UserPromptSubmit)
3
+ // Reminds Claude to ask 1-5 clarifying questions for ambiguous tasks
4
+
5
+ import { existsSync } from 'node:fs';
6
+
7
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
8
+ const safewordDir = `${projectDir}/.safeword`;
9
+
10
+ // Not a safeword project, skip silently
11
+ if (!existsSync(safewordDir)) {
12
+ process.exit(0);
13
+ }
14
+
15
+ console.log(
16
+ `SAFEWORD: Research before asking. Debate options (correct? elegant? latest practices?), then ask 1-5 targeted questions about scope, constraints, or success criteria.`
17
+ );
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Inject timestamp (UserPromptSubmit)
3
+ // Outputs current timestamp for Claude's context awareness
4
+ // Helps with accurate ticket timestamps and time-based reasoning
5
+
6
+ const now = new Date();
7
+
8
+ // Natural language day/time in UTC
9
+ const natural = now.toLocaleDateString('en-US', {
10
+ weekday: 'long',
11
+ year: 'numeric',
12
+ month: 'long',
13
+ day: 'numeric',
14
+ hour: '2-digit',
15
+ minute: '2-digit',
16
+ timeZone: 'UTC',
17
+ timeZoneName: 'short',
18
+ });
19
+
20
+ // ISO 8601 UTC
21
+ const iso = now.toISOString();
22
+
23
+ // Local timezone
24
+ const local = now.toLocaleTimeString('en-US', {
25
+ hour: '2-digit',
26
+ minute: '2-digit',
27
+ timeZoneName: 'short',
28
+ });
29
+
30
+ console.log(`Current time: ${natural} (${iso}) | Local: ${local}`);
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Lint configuration sync check (SessionStart)
3
+ // Warns if ESLint or Prettier configs are missing or out of sync
4
+
5
+ import { existsSync } from 'node:fs';
6
+
7
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
8
+ const safewordDir = `${projectDir}/.safeword`;
9
+
10
+ // Not a safeword project, skip silently
11
+ if (!existsSync(safewordDir)) {
12
+ process.exit(0);
13
+ }
14
+
15
+ const warnings: string[] = [];
16
+
17
+ // Check for ESLint config
18
+ const eslintConfigs = [
19
+ 'eslint.config.mjs',
20
+ 'eslint.config.js',
21
+ '.eslintrc.json',
22
+ '.eslintrc.js',
23
+ ];
24
+ const hasEslint = await Promise.all(
25
+ eslintConfigs.map(f => Bun.file(`${projectDir}/${f}`).exists()),
26
+ );
27
+ if (!hasEslint.some(Boolean)) {
28
+ warnings.push("ESLint config not found - run 'npm run lint' may fail");
29
+ }
30
+
31
+ // Check for Prettier config
32
+ const prettierConfigs = ['.prettierrc', '.prettierrc.json', 'prettier.config.js'];
33
+ const hasPrettier = await Promise.all(
34
+ prettierConfigs.map(f => Bun.file(`${projectDir}/${f}`).exists()),
35
+ );
36
+ if (!hasPrettier.some(Boolean)) {
37
+ warnings.push('Prettier config not found - formatting may be inconsistent');
38
+ }
39
+
40
+ // Check for required dependencies in package.json
41
+ const pkgJsonFile = Bun.file(`${projectDir}/package.json`);
42
+ if (await pkgJsonFile.exists()) {
43
+ try {
44
+ const pkgJson = await pkgJsonFile.text();
45
+ if (!pkgJson.includes('"eslint"')) {
46
+ warnings.push("ESLint not in package.json - run 'npm install -D eslint'");
47
+ }
48
+ if (!pkgJson.includes('"prettier"')) {
49
+ warnings.push("Prettier not in package.json - run 'npm install -D prettier'");
50
+ }
51
+ } catch (error) {
52
+ if (process.env.DEBUG) console.error('[session-lint-check] package.json parse error:', error);
53
+ }
54
+ }
55
+
56
+ // Output warnings if any
57
+ if (warnings.length > 0) {
58
+ console.log('SAFEWORD Lint Check:');
59
+ for (const warning of warnings) {
60
+ console.log(` ⚠️ ${warning}`);
61
+ }
62
+ }
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Verify AGENTS.md link (SessionStart)
3
+ // Self-heals by restoring the link if removed
4
+
5
+ import { existsSync } from 'node:fs';
6
+
7
+ const LINK = '**⚠️ ALWAYS READ FIRST:** `.safeword/SAFEWORD.md`';
8
+
9
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
10
+ const safewordDir = `${projectDir}/.safeword`;
11
+
12
+ // Not a safeword project, skip silently
13
+ if (!existsSync(safewordDir)) {
14
+ process.exit(0);
15
+ }
16
+
17
+ const agentsFile = Bun.file(`${projectDir}/AGENTS.md`);
18
+
19
+ if (!(await agentsFile.exists())) {
20
+ // AGENTS.md doesn't exist, create it
21
+ await Bun.write(agentsFile, `${LINK}\n`);
22
+ console.log('SAFEWORD: Created AGENTS.md with safeword link');
23
+ process.exit(0);
24
+ }
25
+
26
+ // Check if link is present
27
+ const content = await agentsFile.text();
28
+ if (!content.includes('.safeword/SAFEWORD.md')) {
29
+ // Link missing, prepend it
30
+ await Bun.write(agentsFile, `${LINK}\n\n${content}`);
31
+ console.log('SAFEWORD: Restored AGENTS.md link (was removed)');
32
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Display version on session start (SessionStart)
3
+ // Shows current safeword version and confirms hooks are active
4
+
5
+ import { existsSync } from 'node:fs';
6
+
7
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
8
+ const safewordDir = `${projectDir}/.safeword`;
9
+
10
+ // Not a safeword project, skip silently
11
+ if (!existsSync(safewordDir)) {
12
+ process.exit(0);
13
+ }
14
+
15
+ const versionFile = Bun.file(`${safewordDir}/version`);
16
+ const version = (await versionFile.exists()) ? (await versionFile.text()).trim() : 'unknown';
17
+
18
+ console.log(`SAFE WORD Claude Config v${version} installed - auto-linting and quality review active`);
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Auto Quality Review Stop Hook
3
+ // Triggers quality review when changes are proposed or made
4
+ // Looks for {"proposedChanges": ..., "madeChanges": ...} JSON blob
5
+
6
+ import { existsSync } from 'node:fs';
7
+
8
+ import { QUALITY_REVIEW_MESSAGE } from './lib/quality.ts';
9
+
10
+ interface HookInput {
11
+ transcript_path?: string;
12
+ }
13
+
14
+ interface TranscriptMessage {
15
+ role: string;
16
+ message?: {
17
+ content?: { type: string; text?: string }[];
18
+ };
19
+ }
20
+
21
+ interface ResponseSummary {
22
+ proposedChanges: boolean;
23
+ madeChanges: boolean;
24
+ }
25
+
26
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
27
+ const safewordDir = `${projectDir}/.safeword`;
28
+
29
+ // Not a safeword project, skip silently
30
+ if (!existsSync(safewordDir)) {
31
+ process.exit(0);
32
+ }
33
+
34
+ // Read hook input from stdin
35
+ let input: HookInput;
36
+ try {
37
+ input = await Bun.stdin.json();
38
+ } catch (error) {
39
+ if (process.env.DEBUG) console.error('[stop-quality] stdin parse error:', error);
40
+ process.exit(0);
41
+ }
42
+
43
+ const transcriptPath = input.transcript_path;
44
+ if (!transcriptPath) {
45
+ process.exit(0);
46
+ }
47
+
48
+ const transcriptFile = Bun.file(transcriptPath);
49
+ if (!(await transcriptFile.exists())) {
50
+ process.exit(0);
51
+ }
52
+
53
+ // Read transcript (JSONL format)
54
+ const transcriptText = await transcriptFile.text();
55
+ const lines = transcriptText.trim().split('\n');
56
+
57
+ // Find last assistant message with text content (search backwards)
58
+ let messageText: string | null = null;
59
+ for (let i = lines.length - 1; i >= 0; i--) {
60
+ try {
61
+ const message: TranscriptMessage = JSON.parse(lines[i]);
62
+ if (message.role === 'assistant' && message.message?.content) {
63
+ const textContent = message.message.content.find(c => c.type === 'text' && c.text);
64
+ if (textContent?.text) {
65
+ messageText = textContent.text;
66
+ break;
67
+ }
68
+ }
69
+ } catch {
70
+ // Skip invalid JSON lines
71
+ }
72
+ }
73
+
74
+ if (!messageText) {
75
+ process.exit(0);
76
+ }
77
+
78
+ /**
79
+ * Extract all JSON objects from text using brace-balanced scanning.
80
+ * Handles nested objects and braces inside strings correctly.
81
+ */
82
+ function extractJsonObjects(text: string): string[] {
83
+ const results: string[] = [];
84
+ let i = 0;
85
+
86
+ while (i < text.length) {
87
+ if (text[i] === '{') {
88
+ let depth = 0;
89
+ let inString = false;
90
+ let escape = false;
91
+ const start = i;
92
+
93
+ for (let j = i; j < text.length; j++) {
94
+ const char = text[j];
95
+
96
+ if (escape) {
97
+ escape = false;
98
+ continue;
99
+ }
100
+
101
+ if (char === '\\' && inString) {
102
+ escape = true;
103
+ continue;
104
+ }
105
+
106
+ if (char === '"') {
107
+ inString = !inString;
108
+ continue;
109
+ }
110
+
111
+ if (!inString) {
112
+ if (char === '{') depth++;
113
+ if (char === '}') depth--;
114
+
115
+ if (depth === 0) {
116
+ results.push(text.slice(start, j + 1));
117
+ i = j;
118
+ break;
119
+ }
120
+ }
121
+ }
122
+ }
123
+ i++;
124
+ }
125
+
126
+ return results;
127
+ }
128
+
129
+ const candidates = extractJsonObjects(messageText);
130
+
131
+ function isValidSummary(object: unknown): object is ResponseSummary {
132
+ return (
133
+ typeof object === 'object' &&
134
+ object !== null &&
135
+ typeof (object as ResponseSummary).proposedChanges === 'boolean' &&
136
+ typeof (object as ResponseSummary).madeChanges === 'boolean'
137
+ );
138
+ }
139
+
140
+ let summary: ResponseSummary | null = null;
141
+ for (const candidate of candidates) {
142
+ try {
143
+ const parsed = JSON.parse(candidate);
144
+ if (isValidSummary(parsed)) {
145
+ summary = parsed;
146
+ }
147
+ } catch {
148
+ // Not valid JSON, skip
149
+ }
150
+ }
151
+
152
+ if (!summary) {
153
+ // No valid JSON blob found - remind about required format (JSON stdout + exit 0 per Claude Code docs)
154
+ console.log(
155
+ JSON.stringify({
156
+ decision: 'block',
157
+ reason:
158
+ 'SAFEWORD: Response missing required JSON summary. Add to end of response:\n{"proposedChanges": boolean, "madeChanges": boolean}',
159
+ })
160
+ );
161
+ process.exit(0);
162
+ }
163
+
164
+ // If either proposed or made changes, trigger quality review (JSON stdout + exit 0 per Claude Code docs)
165
+ if (summary.proposedChanges || summary.madeChanges) {
166
+ console.log(JSON.stringify({ decision: 'block', reason: QUALITY_REVIEW_MESSAGE }));
167
+ process.exit(0);
168
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/check.ts"],"sourcesContent":["/**\n * Check command - Verify project health and configuration\n *\n * Uses reconcile() with dryRun to detect missing files and configuration issues.\n */\n\nimport nodePath from 'node:path';\n\nimport { reconcile } from '../reconcile.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { exists, readFileSafe } from '../utils/fs.js';\nimport { header, info, keyValue, success, warn } from '../utils/output.js';\nimport { isNewerVersion } from '../utils/version.js';\nimport { VERSION } from '../version.js';\n\nexport interface CheckOptions {\n offline?: boolean;\n}\n\n/**\n * Check for missing files from write actions\n * @param cwd\n * @param actions\n */\nfunction findMissingFiles(cwd: string, actions: { type: string; path: string }[]): string[] {\n const issues: string[] = [];\n for (const action of actions) {\n if (action.type === 'write' && !exists(nodePath.join(cwd, action.path))) {\n issues.push(`Missing: ${action.path}`);\n }\n }\n return issues;\n}\n\n/**\n * Check for missing text patch markers\n * @param cwd\n * @param actions\n */\nfunction findMissingPatches(\n cwd: string,\n actions: { type: string; path: string; definition?: { marker: string } }[],\n): string[] {\n const issues: string[] = [];\n for (const action of actions) {\n if (action.type !== 'text-patch') continue;\n\n const fullPath = nodePath.join(cwd, action.path);\n if (exists(fullPath)) {\n const content = readFileSafe(fullPath) ?? '';\n if (action.definition && !content.includes(action.definition.marker)) {\n issues.push(`${action.path} missing safeword link`);\n }\n } else {\n issues.push(`${action.path} file missing`);\n }\n }\n return issues;\n}\n\ninterface HealthStatus {\n configured: boolean;\n projectVersion: string | undefined;\n cliVersion: string;\n updateAvailable: boolean;\n latestVersion: string | undefined;\n issues: string[];\n missingPackages: string[];\n}\n\n/**\n * Check for latest version from npm (with timeout)\n * @param timeout\n */\nasync function checkLatestVersion(timeout = 3000): Promise<string | undefined> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort();\n }, timeout);\n\n const response = await fetch('https://registry.npmjs.org/safeword/latest', {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) return undefined;\n\n const data = (await response.json()) as { version?: string };\n return data.version ?? undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check project configuration health using reconcile dryRun\n * @param cwd\n */\nasync function checkHealth(cwd: string): Promise<HealthStatus> {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if configured\n if (!exists(safewordDirectory)) {\n return {\n configured: false,\n projectVersion: undefined,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: undefined,\n issues: [],\n missingPackages: [],\n };\n }\n\n // Read project version\n const versionPath = nodePath.join(safewordDirectory, 'version');\n const projectVersion = readFileSafe(versionPath)?.trim() ?? undefined;\n\n // Use reconcile with dryRun to detect issues\n const ctx = createProjectContext(cwd);\n const result = await reconcile(SAFEWORD_SCHEMA, 'upgrade', ctx, { dryRun: true });\n\n // Collect issues from write actions and text patches\n // Filter out chmod (paths[] instead of path) and json-merge/unmerge (incompatible definition)\n const actionsWithPath = result.actions.filter(\n (\n a,\n ): a is Exclude<\n (typeof result.actions)[number],\n { type: 'chmod' } | { type: 'json-merge' } | { type: 'json-unmerge' }\n > => a.type !== 'chmod' && a.type !== 'json-merge' && a.type !== 'json-unmerge',\n );\n const issues: string[] = [\n ...findMissingFiles(cwd, actionsWithPath),\n ...findMissingPatches(cwd, actionsWithPath),\n ];\n\n // Check for missing .claude/settings.json\n if (!exists(nodePath.join(cwd, '.claude', 'settings.json'))) {\n issues.push('Missing: .claude/settings.json');\n }\n\n return {\n configured: true,\n projectVersion,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: undefined,\n issues,\n missingPackages: result.packagesToInstall,\n };\n}\n\n/**\n * Check for CLI updates and report status\n * @param health\n */\nasync function reportUpdateStatus(health: HealthStatus): Promise<void> {\n info('\\nChecking for updates...');\n const latestVersion = await checkLatestVersion();\n\n if (!latestVersion) {\n warn(\"Couldn't check for updates (offline?)\");\n return;\n }\n\n health.latestVersion = latestVersion;\n health.updateAvailable = isNewerVersion(health.cliVersion, latestVersion);\n\n if (health.updateAvailable) {\n warn(`Update available: v${latestVersion}`);\n info('Run `npm install -g safeword` to upgrade');\n } else {\n success('CLI is up to date');\n }\n}\n\n/**\n * Compare project version vs CLI version and report\n * @param health\n */\nfunction reportVersionMismatch(health: HealthStatus): void {\n if (!health.projectVersion) return;\n\n if (isNewerVersion(health.cliVersion, health.projectVersion)) {\n warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);\n info('Consider upgrading the CLI');\n } else if (isNewerVersion(health.projectVersion, health.cliVersion)) {\n info(`\\nUpgrade available for project config`);\n info(\n `Run \\`safeword upgrade\\` to update from v${health.projectVersion} to v${health.cliVersion}`,\n );\n }\n}\n\n/**\n * Report issues or success\n * @param health\n */\nfunction reportHealthSummary(health: HealthStatus): void {\n if (health.issues.length > 0) {\n header('Issues Found');\n for (const issue of health.issues) {\n warn(issue);\n }\n info('\\nRun `safeword upgrade` to repair configuration');\n return;\n }\n\n if (health.missingPackages.length > 0) {\n header('Missing Packages');\n info(`${health.missingPackages.length} linting packages not installed`);\n info('Run `safeword upgrade` to install missing packages');\n return;\n }\n\n success('\\nConfiguration is healthy');\n}\n\n/**\n *\n * @param options\n */\nexport async function check(options: CheckOptions): Promise<void> {\n const cwd = process.cwd();\n\n header('Safeword Health Check');\n\n const health = await checkHealth(cwd);\n\n // Not configured\n if (!health.configured) {\n info('Not configured. Run `safeword setup` to initialize.');\n return;\n }\n\n // Show versions\n keyValue('Safeword CLI', `v${health.cliVersion}`);\n keyValue('Project config', health.projectVersion ? `v${health.projectVersion}` : 'unknown');\n\n // Check for updates (unless offline)\n if (options.offline) {\n info('\\nSkipped update check (offline mode)');\n } else {\n await reportUpdateStatus(health);\n }\n\n reportVersionMismatch(health);\n reportHealthSummary(health);\n}\n"],"mappings":"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","names":[]}